Compare commits

...

66 Commits

Author SHA1 Message Date
Riccardo Ferretti
8b7400c20f v0.20.4 2023-01-04 13:07:17 +01:00
Riccardo Ferretti
d2feee8c75 Preparation for next release 2023-01-04 13:07:02 +01:00
allcontributors[bot]
2113705fb6 add badsketch as a contributor for code (#1130)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-01-03 11:16:46 +01:00
Daniel Wang
4758923188 Feat #903: Added support for emoji tags (#1125)
* Added support for emoji tags
2023-01-03 11:13:12 +01:00
Riccardo Ferretti
275028ffa5 v0.20.3 2022-12-19 19:59:31 +01:00
Riccardo Ferretti
8b425ff420 Preparation for next release 2022-12-19 19:59:04 +01:00
Numan Zaheer Ahmed
64638868c0 Fix minor typo in frequently-asked-questions.md (#1121) 2022-12-11 16:32:30 +01:00
Riccardo Ferretti
7a2756eb1d Adding number of entries in title for orphan, placeholder, tag treeviews 2022-12-04 18:14:01 +01:00
Riccardo Ferretti
540371bf96 Setting prettier as default formatter for typescript 2022-12-04 18:12:03 +01:00
allcontributors[bot]
42b32103f5 add jonathanpberger as a contributor for doc (#1114)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-11-28 17:55:37 +01:00
jonathan berger
1d264dcb22 Add mention of FOAM_DATE_WEEK to documentation. (#1113)
Because it was added in code, but hard to search.

Co-authored-by: JPB <jonathan@ltmp.co>
2022-11-28 17:54:08 +01:00
allcontributors[bot]
7f8c58084b add sn3akiwhizper as a contributor for doc (#1109)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-11-22 22:09:42 +01:00
sn3akiwhizper
6b2dda4a71 Documentation updates and cleaning (#1108)
* fixing some unmatched links, daily-note path note, start graphviz/tags/properties clarification

fixed links that didn't actually link to their target, add discussion about creating daily-notes in path based off date, then clarifying notes about styling graph viz and tags while enhancing the note properties descriptions by describing how properties are described and which properties are custom, which are foam-specific, and which are foam-template-specific

* add filter view, default variables to graphviz, and viewing tags in graphviz

graphviz: discuss filter view, add all changeable variables to graph style example, tags: describe viewing tags in the graphviz

* add small note about learning yaml

* last push fixing up some todos

* making recommended changes from PR request
2022-11-22 22:09:04 +01:00
Riccardo
a6cb50f3d6 Fix #907: use definition provider for resource links, link provider for placeholder links (#1106) 2022-11-14 00:34:52 +01:00
Riccardo Ferretti
09c2198928 fixed readme and docs 2022-11-12 17:19:00 +01:00
Riccardo Ferretti
431da1b385 docs: add Lloyd-Jackman-UKPL as a contributor for doc 2022-11-12 17:10:36 +01:00
Lloyd Jackman
57e0621c24 Added open source doc (#1102)
* Added open source doc
2022-11-11 15:22:05 +01:00
Riccardo Ferretti
ebd1008215 v0.20.2 2022-10-26 22:59:12 +02:00
Riccardo Ferretti
0df958c3a4 Preparation for next release 2022-10-26 22:58:31 +02:00
Riccardo Ferretti
14709313ae fix #1094: use default template when none is provided 2022-10-26 22:55:12 +02:00
Riccardo Ferretti
ff2dd23918 Refactored grouped resource tree data provider to use new matcher 2022-10-26 19:18:13 +02:00
Riccardo Ferretti
66e74966ee removed unused asset 2022-10-25 22:06:29 +02:00
Riccardo Ferretti
9b022d0c61 renamed Create Note command 2022-10-25 21:59:12 +02:00
Riccardo Ferretti
e1b301814e removed unused deps 2022-10-25 21:58:31 +02:00
Riccardo Ferretti
2010d8d51e removed unused document 2022-10-25 21:52:47 +02:00
Riccardo Ferretti
22fa977c9b removing unnecessary files from extension bundle 2022-10-25 21:51:46 +02:00
allcontributors[bot]
285293ec23 add elgirafo as a contributor for doc (#1093)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-10-25 09:09:54 +02:00
luca
6497f527ca Updated FAQ (#1092)
Added an additional answer regarding publishing foam notes with graph view.
2022-10-25 09:09:17 +02:00
Riccardo
689c167384 Bootstrap improvements and glob dependency removal (#1084)
* refactored relationship between workspace and providers, plus using VS Code for globbing
* glob dependency is now only for testing
2022-10-19 23:30:50 +02:00
Riccardo Ferretti
18a2d76139 v0.20.1 2022-10-13 18:56:04 +02:00
Riccardo Ferretti
722bef4257 Preparation for next release 2022-10-13 18:53:16 +02:00
Riccardo
8fe869223d Improved support for globs in multiroot workspace (#1083)
* Added path util to resolve absolute path from multiple base directories
* Better support for multiple roots in include/ignore globs
* URI.asAbsolutePath to use path utils
* Lint
2022-10-13 18:49:07 +02:00
allcontributors[bot]
1a961aac70 add Dominic-DallOsto as a contributor for code (#1082)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-10-12 17:36:27 +02:00
Dominic D
81dacef2fe Clicking the placeholder tooltip asks for template for new note (#1061)
* Add a hover tooltip to placeholder links offering to create a new note from template
* Clicking the placeholder tooltip asks for template for new note
* Create new note from template using `CREATE_NOTE` command
* Update tests
2022-10-12 17:35:34 +02:00
Carlo Bonamico
92932cd004 Update recommended-extensions.md (#1079)
Referenced Excalidraw support
2022-10-10 15:28:09 +02:00
Riccardo Ferretti
1f6f8fd720 fix #1073 - improved support for daily note folder in multiroot workspaces 2022-09-30 22:00:36 +02:00
Riccardo Ferretti
507699924f v0.20.0 2022-09-30 19:49:56 +02:00
Riccardo Ferretti
5d1fb2593b Preparation for next release 2022-09-30 19:49:31 +02:00
Riccardo Ferretti
6012cc1b64 Updated docs for creating new notes 2022-09-30 19:43:56 +02:00
Riccardo Ferretti
36f1b8af9e Code lint 2022-09-30 19:32:05 +02:00
Riccardo
6ac9f6d229 Added "create note" command (#1076)
* Added create-note command and refactored some template code

* Added tests

* Added uri resolution logic across multiple folders

* Deprecating create-note-from-default-template command, it is now replaced by the new and more flexible create-note command (which still has the same defaults)
2022-09-30 19:14:24 +02:00
Riccardo Ferretti
3ce4529232 Removed '+' as date snippet trigger 2022-09-27 11:47:18 +02:00
Riccardo Ferretti
d3b8e66b78 Improved attachment support (fixes #915)
- can now configure list of extensions to be treated as attachments
- improved extension matching
2022-09-26 19:11:22 +02:00
Riccardo Ferretti
836623257c Fixed issue when starting Foam without open workspace (fixes #908) 2022-09-26 18:45:50 +02:00
Riccardo Ferretti
27f2bc4050 Support for opening non-text files via link (fixes #915) 2022-09-26 18:45:02 +02:00
Riccardo Ferretti
5a5f3b1ef0 dataviz: simple clicking open target note 2022-09-15 18:08:08 +02:00
Riccardo Ferretti
d374c51e93 fix #1070: also open images/attachments from graph 2022-09-15 18:07:47 +02:00
Riccardo Ferretti
f3a242251b Updated spell checking documentation (#1068) 2022-09-13 12:18:35 +02:00
Riccardo Ferretti
3d6f98351f v0.19.5 2022-09-01 13:11:11 +02:00
Riccardo Ferretti
970c30f08c Preparation for next release 2022-09-01 13:10:41 +02:00
Riccardo Ferretti
8556982b17 #1056: by default don't show images and attachments in graph view 2022-09-01 13:06:20 +02:00
Matthias Thym
1ab9520c5c Fix link to tag explorer in readme (#1060) 2022-09-01 12:34:38 +02:00
Riccardo
2a72bde111 Use note title in link completion (#1059)
* added setting for completion label

* added alias support

* Disable extra commit characters when automatically adding an alias

* added tests
2022-08-20 00:29:08 +02:00
Riccardo Ferretti
2385bd75b5 Keep extension for attachments when generating references 2022-08-19 16:43:49 +02:00
Riccardo
53d2e7aaed refactor: moved panel modules in own directory (#1055) 2022-08-18 17:18:03 +02:00
allcontributors[bot]
48c13ef400 docs: add dmurph as a contributor for code (#1054)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-17 23:47:32 +02:00
Riccardo
3ab30547eb Add FOAM_DATE_WEEK variable (#1053)
* Update variable-resolver.ts to include week number
* Added tests

Co-authored-by: Daniel Murphy <danielmurphy161@gmail.com>
2022-08-17 23:46:14 +02:00
Riccardo Ferretti
48c4dbf4c4 v0.19.4 2022-08-07 18:50:35 +02:00
Riccardo Ferretti
4715bbc2f5 Prepare for next release 2022-08-07 18:50:03 +02:00
Riccardo Ferretti
1bedbf2191 fix #1052 - solved issue with note embed in preview 2022-08-07 18:49:14 +02:00
allcontributors[bot]
9145d5a476 docs: add readingsnail as a contributor for doc (#1051)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-06 10:51:15 +02:00
allcontributors[bot]
fa6b5aae00 docs: add infogulch as a contributor for doc (#1050)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-06 10:50:04 +02:00
Riccardo Ferretti
753d4a9a67 fixed changelog 2022-08-04 16:02:55 +02:00
Riccardo Ferretti
66200e8e4b v0.19.3 2022-08-04 16:00:19 +02:00
Riccardo Ferretti
d45e0cba4b minor tweak to release workflow 2022-08-04 15:59:51 +02:00
Riccardo Ferretti
c977c7eecd fix #1036 embed images anywhere in the workspace 2022-08-04 15:47:48 +02:00
98 changed files with 2654 additions and 1368 deletions

View File

@@ -914,6 +914,87 @@
"contributions": [
"code"
]
},
{
"login": "infogulch",
"name": "Joe Taber",
"avatar_url": "https://avatars.githubusercontent.com/u/133882?v=4",
"profile": "https://github.com/infogulch",
"contributions": [
"doc"
]
},
{
"login": "readingsnail",
"name": "Woosuk Park",
"avatar_url": "https://avatars.githubusercontent.com/u/1904967?v=4",
"profile": "https://www.readingsnail.pe.kr",
"contributions": [
"doc"
]
},
{
"login": "dmurph",
"name": "Daniel Murphy",
"avatar_url": "https://avatars.githubusercontent.com/u/294026?v=4",
"profile": "http://www.dmurph.com",
"contributions": [
"code"
]
},
{
"login": "Dominic-DallOsto",
"name": "Dominic D",
"avatar_url": "https://avatars.githubusercontent.com/u/26859884?v=4",
"profile": "https://github.com/Dominic-DallOsto",
"contributions": [
"code"
]
},
{
"login": "elgirafo",
"name": "luca",
"avatar_url": "https://avatars.githubusercontent.com/u/80516439?v=4",
"profile": "http://elgirafo.xyz",
"contributions": [
"doc"
]
},
{
"login": "Lloyd-Jackman-UKPL",
"name": "Lloyd Jackman",
"avatar_url": "https://avatars.githubusercontent.com/u/55206370?v=4",
"profile": "https://github.com/Lloyd-Jackman-UKPL",
"contributions": [
"doc"
]
},
{
"login": "sn3akiwhizper",
"name": "sn3akiwhizper",
"avatar_url": "https://avatars.githubusercontent.com/u/102705294?v=4",
"profile": "http://sn3akiwhizper.github.io",
"contributions": [
"doc"
]
},
{
"login": "jonathanpberger",
"name": "jonathan berger",
"avatar_url": "https://avatars.githubusercontent.com/u/41085?v=4",
"profile": "http://jonathanpberger.com/",
"contributions": [
"doc"
]
},
{
"login": "badsketch",
"name": "Daniel Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/8953212?v=4",
"profile": "https://github.com/badsketch",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -28,5 +28,8 @@
"jest.rootPath": "packages/foam-vscode",
"jest.jestCommandLine": "yarn jest",
"gitdoc.enabled": false,
"search.mode": "reuseEditor"
"search.mode": "reuseEditor",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

4
docs/.vscode/custom-tag-style.css vendored Normal file
View File

@@ -0,0 +1,4 @@
.foam-tag{
color:#ffffff;
background-color: #000000;
}

View File

@@ -26,5 +26,8 @@
"files.exclude": {
"_site/**": true
},
"files.insertFinalNewline": true
"files.insertFinalNewline": true,
"markdown.styles": [
".vscode/custom-tag-style.css"
]
}

View File

@@ -0,0 +1,29 @@
# Achieving Greater Privacy and Security
Foam, at its heart and committed to in its [Principles](https://foambubble.github.io/foam/principles), allows the user to control their content in a flexible and non-prescriptive manner. This extends to user preferences, or requirements depending on application and context, around both privacy and security. One way that these use cases can be met is through the use of open-source and not-for-profit mechanisms in the user's workflow to provide a functional equivalence.
Here are a few suggestions on increasing privacy and security when using Foam.
## VS Codium: The Open Source build of VS Code
Foam is built upon VS Code, itself a Microsoft product built on top of an open source project.
As can be found [here](https://github.com/Microsoft/vscode/issues/60#issuecomment-161792005) the **VS Code product itself is not fully open source**. This means that its inner workings are not fully transparent, facilitating the collection and distribution of your data, as specified in its [Privacy Statement](https://devblogs.microsoft.com/visualstudio/privacy/).
If you prefer a fully open source editor based on the same core of VS Code (and for most intents and purposes equivalent to it), you can try [VSCodium](https://github.com/VSCodium).
In its own introduction it is described as, "Binary releases of VS Code without MS branding/telemetry/licensing". Installation packages are easily available across Windows, Unix and Linux (or you can build it from source!).
Access to the VS Code marketplace of add-ons remains in place, including the Foam extension.
The change you will notice in using VS Code versus VS Codium - simply speaking, none. It is, in just about every way you will think of, the same IDE, just without the Microsoft proprietary licence and telemetry. Your Foam experience will remain as smooth and productive as before the change.
## Version Control and Replication
In Foam's [Getting Started](https://foambubble.github.io/foam/#getting-started) section, the set up describes how to set up your notes with a GitHub repository in using the template provided. Doing so provides the user with the ability to see commits made and therefore versions of their notes, allows the user to work across devices or collaborate effectively with other users, and makes publishing to GitHub pages easy.
It's important at the same time to point out the closed-source nature of GitHub, being owned by Microsoft.
One alternative approach could be to use [GitLab](https://gitlab.com/), an open source alternative to GitHub. Whilst it improves on the aspect of transparency, it does also collect usage details and sends your content across the internet.
And of course data is still stored in clear in the cloud, making it susceptible to hacks of the service.
A more private approach would manage replication between devices and users with a serverless mechanism like [Syncthing](https://syncthing.net). Its continuous synchronisation means that changes in files are seen almost instantly and offers the choice of using only local network connections or securely using public relays when a local network connection is unavailable. This means that having two connected devices online will have them synchronised, but it is worth noting that the continuous synchronisation could result in corruption if two users worked on the same file simultaneously and it doesn't offer the same kind of version control that git does (though versioning support can be found and is described [here](https://docs.syncthing.net/users/versioning.html)). It is also not advisable to attempt to use a continuous synchronisation tool to sync local git repositories as the risk of corruption on the git files is high (see [here](https://forum.syncthing.net/t/can-syncthing-reliably-sync-local-git-repos-not-github/8404/18)).
If you need the version control and collaboration, but do not want to compromise on your privacy, the best course of action is to host the open source GitLab server software yourself. The steps (well described [here](https://www.techrepublic.com/article/how-to-set-up-a-gitlab-server-and-host-your-own-git-repositories/)) are not especially complex by any means and can be used exclusively on the local network, if required, offering a rich experience of "built-in version control, issue tracking, code review, CI/CD, and more", according to its website, [GitLab / GitLab Community Edition · GitLab](https://gitlab.com/rluna-gitlab/gitlab-ce).

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

View File

@@ -1,7 +1,7 @@
# Developing documentation
The best way to develop docs for the Foam repo is to directly open the `$foam-repo/docs/` as the root folder in a new vscode window.
This automatically configures vscode with the necessary settings enabled (like [[link-reference-definitions]]) to effiniently write this documentation.
This automatically configures vscode with the necessary settings enabled (like [[link-reference-definitions]]) to efficiently write this documentation.
## Organization

View File

@@ -4,7 +4,7 @@ tags: todo, good-first-task
# Contribution Guide
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
This guide aims to help guide new and seasoned contributors getting around the Foam codebase. For a comprehensive guide about contributing to open-source projects in general, [see here](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/).
## Getting Up To Speed
@@ -23,17 +23,18 @@ Finally, the easiest way to help, is to use it and provide feedback by [submitti
## Contributing
If you're interested in contributing, this short guide will help you get things set up locally.
If you're interested in contributing, this short guide will help you get things set up locally (assuming [node.js](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
1. Clone the repo locally:
1. Fork the project to your Github account by clicking the "Fork" button on the top right hand corner of the project's [home repository page](https://github.com/foambubble/foam).
2. Clone your newly forked repo locally:
`git clone https://github.com/foambubble/foam.git`
`git clone https://github.com/your_username/foam.git`
2. Install the necessary dependencies by running this command from the root:
3. Install the necessary dependencies by running this command from the root of the cloned repository:
`yarn install`
3. From the root, run the command:
4. From the repository root, run the command:
`yarn build`
@@ -86,6 +87,18 @@ This guide assumes you read the previous instructions and you're set up to work
3. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
### Submitting a Pull Request (PR)
After you have made your changes to your copy of the project, it is time to try and merge those changes into the public community project.
1. Return to the project's [home repository page](https://github.com/foambubble/foam).
2. Github should show you an button called "Compare & pull request" linking your forked repository to the community repository.
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
4. Add as many relevant details to the PR message to make it clear to the project maintainers and other members of the community what you have accomplished with your new changes. Link to any issues the changes are related to.
5. Your PR will then need to be reviewed and accepted by the other members of the community. Any discussion about the changes will occur in your PR thread.
6. Once reviewed and accept you can complete the merge request!
7. Finally rest and watch the sun rise on a grateful universe... Or start tackling the other open issues ;)
---
Feel free to modify and submit a PR if this guide is out-of-date or contains errors!

View File

@@ -10,13 +10,11 @@
- `git add *`
- `git commit -m"Preparation for next release"`
4. Update version
- `$ cd packages/foam-vscode`
- `foam-vscode$ yarn lerna version <version>` (where `version` is `patch/minor/major`)
- `cd ../..`
- `$ yarn version-extension <version>` (where `version` is `patch/minor/major`)
5. Package extension
- `$ yarn vscode:package-extension`
- `$ yarn package-extension`
6. Publish extension
- `$ yarn vscode:publish-extension`
- `$ yarn publish-extension`
7. Update the release notes in GitHub
- in GitHub, top right, click on "releases"
- select "tags" in top left

View File

@@ -106,135 +106,148 @@ If that sounds like something you're interested in, I'd love to have you along o
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt=""/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt=""/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
<td align="center"><a href="https://nygaard.site"><img src="https://avatars.githubusercontent.com/u/4606342?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nikhil Nygaard</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=njnygaard" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Pearcekieser"><img src="https://avatars.githubusercontent.com/u/5055971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Pearcekieser</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Pearcekieser" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/theowenyoung"><img src="https://avatars.githubusercontent.com/u/62473795?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Owen Young</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=theowenyoung" title="Documentation">📖</a> <a href="#content-theowenyoung" title="Content">🖋</a></td>
<td align="center"><a href="http://www.prashu.com"><img src="https://avatars.githubusercontent.com/u/476729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Prashanth Subrahmanyam</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ksprashu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/JonasSprenger"><img src="https://avatars.githubusercontent.com/u/25108895?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonas SPRENGER</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=JonasSprenger" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Laptop765"><img src="https://avatars.githubusercontent.com/u/1468359?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Laptop765" title="Documentation">📖</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/memeplex"><img src="https://avatars.githubusercontent.com/u/2845433?v=4?s=60" width="60px;" alt=""/><br /><sub><b>memeplex</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=memeplex" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</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>
<tbody>
<tr>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt="Jani Eväkallio"/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt="Joe Previte"/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt="Riccardo"/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt="Janne Ojanaho"/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt="Paul Shen"/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt="coffenbacher"/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt="Mathieu Dutour"/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt="Michael Hansen"/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt="David Nadlinger"/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt="Fernando"/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt="Juan Gonzalez"/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt="Louie Christie"/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt="Sandro"/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt="Simon Knott"/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt="Steven"/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt="Tim"/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt="Saurav Khdoolia"/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt="Ankit Tiwari"/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt="Ayush Baweja"/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt="TaiChi-IO"/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt="Juan F Gonzalez "/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt="Sanket Dasgupta"/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt="Nicholas Stafie"/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt="Francis Hamel"/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt="digiguru"/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt="CHIRAG SINGHAL"/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt="Jonathan Carter"/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt="Julian Elve"/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt="Thomas Koppelaar"/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt="Akshay"/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt="John Lindquist"/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt="Ashwin Ramaswami"/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt="Claudio Canales"/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt="vitaly-pevgonen"/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt="Dmitry Shemetov"/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt="hooncp"/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt="Martin Laws"/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt="Sean K Smith"/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt="Kevin Neely"/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt="Arief Rahmansyah"/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt="Vishesh Handa"/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt="Hitesh Kumar"/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt="Spencer Woo"/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt="ingalless"/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt="José Duarte"/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt="Yenly"/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt="hikerpig"/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt="Sigfried Gold"/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt="Tristan Sokol"/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt="Danil Rodin"/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt="Scott Williams"/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt="jackiexiao"/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt="John B Nelson"/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt="Asif Mehedi"/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt="Tan Li"/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt="Shauna Gordon"/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt="Mike Cluck"/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt="Brandon Pugh"/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt="Max Davitt"/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt="Brian Anglin"/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt="elswork"/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt="léon h"/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
<td align="center"><a href="https://nygaard.site"><img src="https://avatars.githubusercontent.com/u/4606342?v=4?s=60" width="60px;" alt="Nikhil Nygaard"/><br /><sub><b>Nikhil Nygaard</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=njnygaard" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt="Mark Dixon"/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt="Joel James"/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt="Hashiguchi Ryo"/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt="Michael Overmeyer"/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt="Derrick Qin"/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt="Omar López"/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt="Robin King"/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt="Dheepak "/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt="Daniel VG"/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt="Barabas"/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt="Engincan VESKE"/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt="Paul de Raaij"/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt="Scott Bronson"/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt="Rafael Riedel"/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Pearcekieser"><img src="https://avatars.githubusercontent.com/u/5055971?v=4?s=60" width="60px;" alt="Pearcekieser"/><br /><sub><b>Pearcekieser</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Pearcekieser" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/theowenyoung"><img src="https://avatars.githubusercontent.com/u/62473795?v=4?s=60" width="60px;" alt="Owen Young"/><br /><sub><b>Owen Young</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=theowenyoung" title="Documentation">📖</a> <a href="#content-theowenyoung" title="Content">🖋</a></td>
<td align="center"><a href="http://www.prashu.com"><img src="https://avatars.githubusercontent.com/u/476729?v=4?s=60" width="60px;" alt="Prashanth Subrahmanyam"/><br /><sub><b>Prashanth Subrahmanyam</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ksprashu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/JonasSprenger"><img src="https://avatars.githubusercontent.com/u/25108895?v=4?s=60" width="60px;" alt="Jonas SPRENGER"/><br /><sub><b>Jonas SPRENGER</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=JonasSprenger" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Laptop765"><img src="https://avatars.githubusercontent.com/u/1468359?v=4?s=60" width="60px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Laptop765" title="Documentation">📖</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=60" width="60px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/memeplex"><img src="https://avatars.githubusercontent.com/u/2845433?v=4?s=60" width="60px;" alt="memeplex"/><br /><sub><b>memeplex</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=memeplex" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt="AndreiD049"/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt="Yan"/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt="Jim Tittsler"/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt="Malcolm Mielle"/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt="Veesar"/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt="bentongxyz"/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt="Brian DeVries"/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</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="Clifford Fajardo "/><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="Chris Usick"/><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="Joe DeCock"/><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="Drew Tyler"/><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="Lauviah0622"/><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="Josh Dover"/><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="Phil Helm"/><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="Larry Li"/><br /><sub><b>Larry Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lingyv-li" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/infogulch"><img src="https://avatars.githubusercontent.com/u/133882?v=4?s=60" width="60px;" alt="Joe Taber"/><br /><sub><b>Joe Taber</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=infogulch" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.readingsnail.pe.kr"><img src="https://avatars.githubusercontent.com/u/1904967?v=4?s=60" width="60px;" alt="Woosuk Park"/><br /><sub><b>Woosuk Park</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=readingsnail" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.dmurph.com"><img src="https://avatars.githubusercontent.com/u/294026?v=4?s=60" width="60px;" alt="Daniel Murphy"/><br /><sub><b>Daniel Murphy</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dmurph" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dominic-DallOsto"><img src="https://avatars.githubusercontent.com/u/26859884?v=4?s=60" width="60px;" alt="Dominic D"/><br /><sub><b>Dominic D</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Dominic-DallOsto" title="Code">💻</a></td>
<td align="center"><a href="http://elgirafo.xyz"><img src="https://avatars.githubusercontent.com/u/80516439?v=4?s=60" width="60px;" alt="luca"/><br /><sub><b>luca</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elgirafo" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Lloyd-Jackman-UKPL"><img src="https://avatars.githubusercontent.com/u/55206370?v=4?s=60" width="60px;" alt="Lloyd Jackman"/><br /><sub><b>Lloyd Jackman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lloyd-Jackman-UKPL" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://sn3akiwhizper.github.io"><img src="https://avatars.githubusercontent.com/u/102705294?v=4?s=60" width="60px;" alt="sn3akiwhizper"/><br /><sub><b>sn3akiwhizper</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sn3akiwhizper" title="Documentation">📖</a></td>
<td align="center"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->

View File

@@ -0,0 +1,45 @@
# Foam Commands
Foam has various commands that you can explore by calling the command palette and typing "Foam".
In particular, some commands can be very customizible and can help with custom workflows and use cases.
## foam-vscode.create-note command
This command creates a note.
Although it works fine on its own, it can be customized to achieve various use cases.
Here are the settings available for the command:
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
- templatePath: The path of the template to use. If relative it will be resolved against the workspace root.
- text: The text to use for the note. If also a template is provided, the template has precedence
- variables: Variables to use in the text or template (e.g. `FOAM_TITLE`)
- date: The date used to resolve the FOAM_DATE_* variables. in `YYYY-MM-DD` format
- onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel': What to do in case the target file already exists
To customize a command and associate a key binding to it, open the key binding settings and add the appropriate configuration, here are some examples:
- Create a note called `test note.md` with some text. If the note already exists, ask for a new name
```
{
"key": "alt+f",
"command": "foam-vscode.create-note",
"args": {
"text": "test note ${FOAM_DATE_YEAR}",
"notePath": "test note.md",
"onFileExists": "ask"
}
}
```
- Create a note following the `weekly-note.md` template. If the note already exists, open it
```
{
"key": "alt+g",
"command": "foam-vscode.create-note",
"args": {
"templatePath": ".foam/templates/weekly-note.md",
"onFileExists": "open"
}
}
```

View File

@@ -45,11 +45,15 @@ The following properties can be used:
The above configuration would create a file `journal/daily-note-2020-07-25.mdx`, with the heading `Journal Entry, Sunday, July 25`.
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
Please see [[note-macros]]
[//begin]: # "Autogenerated link references for markdown compatibility"
[Note Templates]: note-templates.md "Note Templates"
[note-properties]: note-properties.md "Note Properties"
[note-templates#Example of date-based|Example of date-based filepath]: note-templates.md "Note Templates"
[note-macros]: ../recipes/note-macros.md "Custom Note Macros"
[//end]: # "Autogenerated link references"

View File

@@ -3,15 +3,29 @@
Foam comes with a graph visualization of your notes.
To see the graph execute the `Foam: Show Graph` command.
Your files, such as notes and documents, are shown as the nodes of the graph along with the tags defined in your notes. The edges of the graph represent either a link between two files or a file that contains a certain tag. A node in the graph will grow in size with the number of connections it has, representing stronger or more defined concepts and topics.
## Graph Navigation
With the graph you can:
With the Foam graph visualization you can:
- highlight a node by hovering on it, to quickly see how it's connected to the rest of your notes
- select one or more (by keeping `shift` pressed while selecting) nodes by clicking on them, to better understand the structure of your notes
- navigate to a note by clicking on it while pressing `ctrl` or `cmd`
- navigate to a note by clicking on it's node while pressing `ctrl` or `cmd`
- automatically center the graph on the currently edited note, to immediately see its connections
## Filter View
If you only wish to view certain types of notes or tags, or want to hide linked attachment nodes then you can apply filters to the graph.
- Open the graph view using the `Foam: Show Graph` command
- Click the button in the top right corner of the graph view that says "Open Controls"
- Expand the "Filter By Type" dropdown to view the selection of types that you can filter by
- Uncheck the checkbox for any type you want to hide
- The types displayed in this dropdown are defined by [[note-properties]] which includes Foam-standard types as well as custom types defined by you!
![Graph filtering demo](../../assets/images/graph-filter.gif)
## Custom Graph Styles
The Foam graph will use the current VS Code theme by default, but it's possible to customize it with the `foam.graph.style` setting.
@@ -24,28 +38,41 @@ A sample configuration object is provided below, you can provide as many or as l
"foam.graph.style": {
"background": "#202020",
"fontSize": 12,
"lineColor": "#277da1",
"lineWidth": 0.2,
"particleWidth": 1.0,
"highlightedForeground": "#f9c74f",
"node": {
"note": "#277da1",
"placeholder": "#545454",
"feature": "green",
}
}
```
- `note` defines the color for regular nodes
- `placeholder` defines the color for links that don't match any existing note. This is a [[placeholder]] because no file with such name exists (see [[wikilinks]] for more info).
- `feature` shows an example of how you can use note types to customize the graph. It defines the color for the notes of type `feature`
- see [[note-properties]] for details
- you can have as many types as you want
- `background` background color of the graph, adjust to increase contrast
- `fontSize` size of the title font for each node
- `lineColor` color of the edges between nodes in the graph
- `lineWidth` thickness of the edges between nodes
- `particleWidth` size of the particle animation showing link direction when highlighting a node
- `highlightedForeground` color of highlighted nodes and edges when hovering over a node
- to style individual types of nodes jump to the next section: [Style Nodes By Type](#style-nodes-by-type)
### Style nodes by type
### Style Nodes by Type
It is possible to customize the style of a node based on the `type` property in the YAML frontmatter of the corresponding document.
There are a few default node types defined by Foam that are displayed in the graph:
- `note` defines the color for regular nodes whose documents have not overriden the `type` property.
- `placeholder` defines the color for links that don't match any existing note. This is a [[placeholder]] because no file with such name exists.
- see [[wikilinks]] for more info <!--NOTE: this placeholder link should NOT have an associated file. This is to demonstrate the custom coloring-->
- `tag` defines the color for nodes representing #tags, allowing tags to be used as graph nodes similar to backlinks.
- see [[tags]] for more info
- `feature` shows an example of how you can use note types to customize the graph. It defines the color for the notes of type `feature`
- see [[note-properties]] for details
For example the following `backlinking.md` note:
```
```markdown
---
type: feature
---
@@ -58,7 +85,11 @@ And the following `settings.json`:
```json
"foam.graph.style": {
"background": "#202020",
"node": {
"note": "#277da1",
"placeholder": "#545454",
"tag": "#f9c74f",
"feature": "red",
}
}
@@ -69,6 +100,7 @@ Will result in the following graph:
![Style node by type](../../assets/images/style-node-by-type.png)
[//begin]: # "Autogenerated link references for markdown compatibility"
[wikilinks]: wikilinks.md "Wikilinks"
[note-properties]: note-properties.md "Note Properties"
[wikilinks]: wikilinks.md "Wikilinks"
[tags]: tags.md "Tags"
[//end]: # "Autogenerated link references"

View File

@@ -48,9 +48,23 @@ You can override this setting in your Foam workspace's `settings.json`:
Sometimes, you may want to ignore certain files or folders, so that Foam doesn't generate link reference definitions to them.
There are three options for excluding files from your Foam project:
1. `files.exclude` (from VSCode) will prevent the folder from showing in the file explorer.
> "Configure glob patterns for excluding files and folders. For example, the file explorer decides which files and folders to show or hide based on this setting. Refer to the Search: Exclude setting to define search-specific excludes."
2. `files.watcherExclude` (from VSCode) prevents VSCode from constantly monitoring files for changes.
> "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."
3. `foam.files.ignore` (from Foam) ignores files from being added to the Foam graph.
> "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`" (requires reloading VSCode to take effect).
For instance, if you're using a local instance of [Jekyll](https://jekyllrb.com/), you may find that it writes copies of each `.md` file into a `_site` directory, which may lead to Foam generating references to them instead of the original source notes.
You can ignore the `_site` directory by adding the following to your `.vscode/settings.json`:
You can ignore the `_site` directory by adding any of the following settings to your `.vscode/settings.json` file:
```json
"files.exclude": {
@@ -59,18 +73,16 @@ You can ignore the `_site` directory by adding the following to your `.vscode/se
"files.watcherExclude": {
"**/_site": true
},
"foam.files.ignore": [
"_site/**/*"
]
```
After changing the setting in your workspace, you can run the [[workspace-janitor]] command to convert all existing definitions.
## Future improvements
Implement `foam.exclude`. [[todo]]
See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
[//begin]: # "Autogenerated link references for markdown compatibility"
[workspace-janitor]: ../tools/workspace-janitor.md "Janitor"
[todo]: ../../dev/todo.md "Todo"
[link-reference-definition-improvements]: ../../dev/proposals/link-reference-definition-improvements.md "Link Reference Definition Improvements"
[//end]: # "Autogenerated link references"

View File

@@ -1,35 +1,56 @@
---
type: feature
keywords: hello world
keywords: hello world, bonjour
tags: [hello, bonjour]
---
# Note Properties
At the top of the file you can have a section where you define your properties.
At the top of the file you can have a section where you define your properties. This section is known as the [Front-Matter](https://learn.cloudcannon.com/jekyll/introduction-to-jekyll-front-matter/) of the document and uses [YAML formatting](https://www.codeproject.com/Articles/1214409/Learn-YAML-in-five-minutes).
> Be aware that this section needs to be at the very top of the file to be valid
> Be aware that this YAML section needs to be at the very top of the file to be valid.
For example, for this file, we have:
```text
```markdown
---
type: feature
keywords: hello world
keywords: hello world, bonjour
---
```
Those are properties.
Properties can be used to organize your notes.
This sets the `type` of this document to `feature` and sets **three** keywords for the document: `hello`, `world`, and `bonjour`. The YAML parser will treat both spaces and commas as the seperators for these YAML properties. If you want to use multi-word values for these properties, you will need to combine the words with dashes or underscores (i.e. instead of `hello world`, use `hello_world` or `hello-world`).
> You can set as many custom properties for a document as you like, but there are a few [special properties](#special-properties) defined by Foam.
## Special Properties
Some properties have special meaning for Foam:
- the `title` property will assign the name to the note that you will see in the graph, regardless of the filename or the first heading (also see how to [[write-notes-in-foam]])
- the `type` property can be used to style notes differently in the graph (also see [[graph-visualization]])
- the `tags` property can be used to add tags to a note (see [[tags-and-tag-explorer]])
| Name | Description |
| -------------------- | ------------------- |
| `title` | will assign the name to the note that you will see in the graph, regardless of the filename or the first heading (also see how to [[write-notes-in-foam]]) |
| `type` | can be used to style notes differently in the graph (also see [[graph-visualization]]). The default type for a document is `note` unless otherwise specified with this property. |
| `tags` | can be used to add tags to a note (see [[tags]]) |
For example:
```markdown
---
title: "Note Title"
type: "daily-note"
tags: daily, funny, planning
---
```
## Foam Template Properties
There also exists properties that are even more specific to Foam templates, see [[note-templates#Metadata]] for more info.
[//begin]: # "Autogenerated link references for markdown compatibility"
[write-notes-in-foam]: ../getting-started/write-notes-in-foam.md "Writing Notes"
[graph-visualization]: graph-visualization.md "Graph Visualization"
[tags]: tags.md "Tags"
[note-templates#Metadata]: note-templates.md "Note Templates"
[//end]: # "Autogenerated link references"

View File

@@ -25,15 +25,28 @@ To create a note from a template:
_Theme: Ayu Light_
## Special templates
### Default template
The `.foam/templates/new-note.md` template is special in that it is the template that will be used by the `Foam: Create New Note` command.
Customize this template to contain content that you want included every time you create a note.
Customize this template to contain content that you want included every time you create a note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
```markdown
---
type: basic-note
---
```
### Default daily note template
The `.foam/templates/daily-note.md` template is special in that it is the template that will be used when creating daily notes (e.g. by using `Foam: Open Daily Note`).
Customize this template to contain content that you want included every time you create a daily note.
Customize this template to contain content that you want included every time you create a daily note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
```markdown
---
type: daily-note
---
```
## Variables
@@ -41,12 +54,12 @@ Templates can use all the variables available in [VS Code Snippets](https://code
In addition, you can also use variables provided by Foam:
| Name | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new note. |
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
| Name | Description |
| -------------------- | ------------ |
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new |
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, `FOAM_DATE_WEEK` etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
### `FOAM_DATE_*` variables
@@ -56,6 +69,8 @@ For example, `FOAM_DATE_YEAR` has the same behaviour as VS Code's `CURRENT_YEAR`
By default, prefer using the `FOAM_DATE_` versions. The datetime used to compute the values will be the same for both `FOAM_DATE_` and VS Code's variables, with the exception of the creation notes using the daily note template.
For more nitty-gritty details about the supported date formats, [see here](https://github.com/foambubble/foam/blob/master/packages/foam-vscode/src/services/variable-resolver.ts).
#### Relative daily notes
When referring to daily notes, you can use the relative snippets (`/+1d`, `/tomorrow`, etc.). In these cases, the new notes will be created with the daily note template, but the datetime used should be the relative datetime, not the current datetime.
@@ -81,22 +96,19 @@ When creating notes in any other scenario, the `FOAM_DATE_` values are computed
Templates can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
| Name | Description |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Name | Description |
| ------------- | ---------------------- |
| `filepath` | The filepath to use when creating the new note. If the filepath is a relative filepath, it is relative to the current workspace. |
| `name` | A human readable name to show in the template picker. |
| `description` | A human readable description to show in the template picker. |
| `name` | A human readable name to show in the template picker. |
| `description` | A human readable description to show in the template picker. |
Foam-specific variables (e.g. `$FOAM_TITLE`) can be used within template metadata. However, VS Code snippet variables are ([currently](https://github.com/foambubble/foam/pull/655)) not supported.
### `filepath` attribute
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template.
If the filepath is a relative filepath, it is relative to the current workspace.
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template. If the filepath is a relative filepath, it is relative to the current workspace.
**Note:** While you can make use of the `filepath` attribute in [daily note](daily-notes.md) templates (`.foam/templates/daily-note.md`), there is currently no way to have `filepath` vary based on the date. This will be improved in the future. For now, you can customize the location of daily notes using the [`foam.openDailyNote` settings](daily-notes.md).
#### Example of relative `filepath`
#### Example of **relative** `filepath`
For example, `filepath` can be used to customize `.foam/templates/new-note.md`, overriding the default `Foam: Create New Note` behaviour of opening the file in the same directory as the active file:
@@ -109,7 +121,7 @@ foam_template:
---
```
#### Example of absolute `filepath`
#### Example of **absolute** `filepath`
`filepath` can be an absolute filepath, so that the notes get created in the same location, regardless of which file or workspace the editor currently has open.
The format of an absolute filepath may vary depending on the filesystem used.
@@ -125,6 +137,22 @@ foam_template:
---
```
#### Example of **date-based** `filepath`
It is possible to vary the `filepath` value based on the current date using the `FOAM_DATE_*` variables. This is especially useful for the [[daily-notes]] template if you wish to organize by years, months, etc. Below is an example of a daily-note template metadata section that will create new daily notes under the `journal/YEAR/MONTH-MONTH_NAME/` filepath. For example, when a note is created on November 15, 2022, a new file will be created at `C:\Users\foam_user\foam_notes\journal\2022\11-Nov\2022-11-15-daily-note.md`. This method also respects the creation of daily notes relative to the current date (i.e. `/+1d`).
```markdown
---
type: daily-note
foam_template:
description: Daily Note for $FOAM_TITLE
filepath: "C:\\Users\\foam_user\\foam_notes\\journal\\$FOAM_DATE_YEAR\\$FOAM_DATE_MONTH-$FOAM_DATE_MONTH_NAME_SHORT\\$FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE-daily-note.md"
---
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE Daily Notes
```
> Note: this method **requires** the use of absolute file paths, and in this example is using Windows path conventions. This method will also override any filename formatting defined in `.vscode/settings.json`
### `name` and `description` attributes
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):
@@ -185,3 +213,7 @@ existing_frontmatter: "Existing Frontmatter block"
---
This is the rest of the template
```
[//begin]: # "Autogenerated link references for markdown compatibility"
[daily-notes]: daily-notes.md "Daily Notes"
[//end]: # "Autogenerated link references"

View File

@@ -1,15 +1,9 @@
# Spell Checking
Foam comes with a spell checker powered by the [Spellright extension](https://marketplace.visualstudio.com/items?itemName=ban.spellright).
There are many spell checking extensions for VS Code.
Misspelled words are highlighted, like hellow.
You can place the cursor on top of the word, and press `cmd+.` for suggestions on how to fix the problem.
The most popular spell checker for VS Code is [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker).
You can configure the extension in the settings, for example to:
Another one of our favorites is [LTeX](https://marketplace.visualstudio.com/items?itemName=valentjn.vscode-ltex&ssr=false#overview), which is a bit heavier but offers some extra functionality.
- ignore certain files
- change the language(s)
- and much more
You can use any number of alternative spell checking extensions for VS Code.
One of our favorites is [LTeX](https://marketplace.visualstudio.com/items?itemName=valentjn.vscode-ltex&ssr=false#overview), which is a bit heavier but offers some extra functionality.
Another popular one is [Spellright](https://marketplace.visualstudio.com/items?itemName=ban.spellright), but be mindful that there have been reports of incompatibility with the `vscode-markdown` extension (see https://github.com/foambubble/foam/issues/1068).

View File

@@ -1,5 +1,5 @@
---
tags: my-tag1 my-tag2
tags: my-tag1 my-tag2 my-tag3/notes
---
# Tags
@@ -11,20 +11,45 @@ You can add tags to your notes to categorize or link notes together.
There are two ways of creating a tag:
- Adding a `#tag` anywhere in the text of the note, for example: #my-tag1
- Using the `tags: tag1, tag2` yaml frontmatter [[note property|note-properties]]. Notice `my-tag1` and `my-tag2` tags which are added to this document this way.
- Using the `tags: tag1, tag2` yaml frontmatter [[note-properties|note property]]. Notice `my-tag1` and `my-tag2` tags which are added to this document this way.
Tags can also be hierarchical, so you can have `#parent/child`.
Tags can also be hierarchical, so you can have `#parent/child` such as #my-tag3/info.
## Using *Tag Explorer*
It's possible to navigate tags via the Tag Explorer panel.
In the future it will be possible to explore tags via the graph as well.
It's possible to navigate tags via the Tag Explorer panel. Expand the Tag Explorer view in the left side bar which will list all the tags found in current Foam environment. Then, each level of tags can be expanded until the options to search by tag and a list of all files containing a particular tag are shown.
Tags can also be visualized in the Foam Graph Explorer. See [[graph-visualization]] for more info including how to change the color of nodes representing tags.
## Styling tags
Inline tags can be styled using custom CSS with the selector `.foam-tag`.
It is possible to customize the way that tags look in the Markdown Preview panel that renders your Foam notes. This requires some knowledge of the CSS language, which is used to customize the styles of web technologies such as VSCode. A cursory introduction to CSS can be [found here](https://www.freecodecamp.org/news/get-started-with-css-in-5-minutes-e0804813fc3e/).
1. Create a CSS file within your Foam project, for example in `.foam/css/custom-tag-style.css` or [.vscode/custom-tag-style.css](../../.vscode/custom-tag-style.css)
2. Add CSS code that targets the `.foam-tag` class
3. Add a rule for each [CSS property](https://www.w3schools.com/cssref/index.php) you would like applied to your tags.
4. Open the `.vscode/settings.json` file (or the Settings browser with `ctrl+,`)
5. Add the path to your new stylesheet to the `markdown.styles` setting.
> Note: the file path for the stylesheet will be relative to the currently open folder in the workspace when changing this setting for the current workspace. If changing this setting for the user, then the file path will be relative to your global [VSCode settings](https://code.visualstudio.com/docs/getstarted/settings).
The end result will be a CSS file that looks similiar to the content below. Now you can make your tags standout in your note previews.
```css
.foam-tag{
color:#ffffff;
background-color: #000000;
}
```
![custom tag style demo](../../assets/images/custom-tag-style.png)
## Using backlinks in place of tags
Given the power of backlinks, some people prefer to use them as tags.
For example you can tag your notes about books with [[book]].
[//begin]: # "Autogenerated link references for markdown compatibility"
[note-properties|note property]: note-properties.md "Note Properties"
[graph-visualization]: graph-visualization.md "Graph Visualization"
[//end]: # "Autogenerated link references"

View File

@@ -6,11 +6,11 @@ Wikilinks are the internal links that connect the files in your knowledge base.
To create a wikilink, type `[[` and then start typing the name of another note in your repo. Once the desired note is selected press the `tab` key to autocomplete it. For example: [[graph-visualization]].
`Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on wikilink to navigate to that note (`F12` also works while your cursor is on the wikilink). If the file doesn't exist it will be created in your workspace based on your default [[note-template]] settings.
`Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on wikilink to navigate to that note (`F12` also works while your cursor is on the wikilink). If the file doesn't exist it will be created in your workspace based on your default [[note-templates]] settings.
## Placeholders
You can also create a [[placeholder]].
You can also create a [[placeholder]]. <!--NOTE: this placeholder link should NOT have an associated file. This is to demonstrate the concept-->
A placeholder is a wikilink that doesn't have a target file and a link to a placeholder is styled differently so you can easily tell them apart.
They can still be helpful to highlight connections.
@@ -34,8 +34,8 @@ The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.f
[//begin]: # "Autogenerated link references for markdown compatibility"
[graph-visualization]: graph-visualization.md "Graph Visualization"
[note-templates]: note-templates.md "Note Templates"
[link-reference-definitions]: link-reference-definitions.md "Link Reference Definitions"
[foam-file-format]: ../../dev/foam-file-format.md "Foam File Format"
[note-templates]: note-templates.md "Note Templates"
[link-reference-definition-improvements]: ../../dev/proposals/link-reference-definition-improvements.md "Link Reference Definition Improvements"
[//end]: # "Autogenerated link references"

View File

@@ -13,7 +13,12 @@
- 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)
Any extension you install in Visual Studio Code is enabled by default. Given 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)
## I want to publish the graph view to GitHub pages or Vercel
If you want a different front-end look to your published foam and a way to see your graph view, we'd recommend checking out these templates:
- [foam-gatsby](https://github.com/mathieudutour/foam-gatsby-template) by [Mathieu Dutour](https://github.com/mathieudutour)
- [foam-gatsby-kb](https://github.com/hikerpig/foam-template-gatsby-kb) by [hikerpig](https://github.com/hikerpig)
[//begin]: # "Autogenerated link references for markdown compatibility"
[recommended-extensions]: getting-started/recommended-extensions.md "Recommended Extensions"

View File

@@ -2,12 +2,13 @@
- 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 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.
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create Note` and enter a **Title Case Name** to create `Title Case Name.md`
- Add a keyboard binding to make creating new notes easier. See [[commands]] for more info on this.
- The [[note-templates]] used by this command can be customized.
- You shouldn't worry too much about categorizing your notes. You can always [[search-for-notes]], and explore them using the [[graph-visualization]].
[//begin]: # "Autogenerated link references for markdown compatibility"
[commands]: ../features/commands.md "Foam Commands"
[note-templates]: ../features/note-templates.md "Note Templates"
[search-for-notes]: ../recipes/search-for-notes.md "Search for Notes"
[graph-visualization]: ../features/graph-visualization.md "Graph Visualization"

View File

@@ -36,8 +36,12 @@ If you want to learn more about VS Code, check out their [website](https://code.
You can see a few panels on the left, including:
- `Outline`: this panel shows you the structure of the file based on the headings
- `Tag Explorer`: This shows you the tags in your workspace, see [[tags-and-tag-explorer]] for more information on tags
- `Tag Explorer`: This shows you the tags in your workspace, see [[tags]] for more information on tags
## Settings
To view or change the settings in VS Code, press `cmd+,`
To view or change the settings in VS Code, press `cmd+,` on Mac and `ctrl+,` on Windows/Linux.
[//begin]: # "Autogenerated link references for markdown compatibility"
[tags]: ../features/tags.md "Tags"
[//end]: # "Autogenerated link references"

View File

@@ -7,7 +7,6 @@ This list is subject to change.
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (alpha)
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
- [Spell Right](https://marketplace.visualstudio.com/items?itemName=ban.spellright)
## Extensions For Additional Features
@@ -17,6 +16,7 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)
- [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
- [Excalidraw whiteboard and sketching tool integration](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
- [VSCode PDF Viewing](https://marketplace.visualstudio.com/items?itemName=tomoki1207.pdf)
- [Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) (to quickly switch between projects)
- [Markdown Extended](https://marketplace.visualstudio.com/items?itemName=jebbs.markdown-extended) (with `kbd` option disabled, `kbd` turns wikilinks into non-clickable buttons)

View File

@@ -218,7 +218,7 @@ Commit the file and push it to gitlab.
### Troubleshooting
- *Could not locate Gemfile* - You didn't follow the steps above to [#Add a Gemlock file]
- *Could not locate Gemfile* - You didn't follow the steps above to [Add a Gemlock file](#add-a-gemlock-file)
- *Conversion error: Jekyll::Converters::Scss encountered an error while converting* You need to reference a theme.
- *Pages are running in CI/CD, but I only ever see `test`, and never deploy* - Perhaps you've renamed the main branch (from master) - check the settings in `.gitlab-ci.yml` and ensure the deploy command is running to the branch you expect it to.
- *I deployed, but my .msd files don't seem to be being converted into .html files* - You need a gem that GitHub installs by default - check `gem "jekyll-optional-front-matter"` appears in the `Gemfile`

View File

@@ -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 using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc).
- Sync your GitHub repo automatically using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc) [[automatic-git-syncing]].
## Publish
@@ -82,8 +82,8 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
## Collaborate
- Give your team push access to your GitHub repo [[todo]]
- Real-time collaboration via VS Code Live Share [[todo]]
- Give your team push access to your [GitHub repo](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-access-to-your-personal-repositories/inviting-collaborators-to-a-personal-repository)
- Real-time collaboration via VS Code Live Share [[real-time-collaboration]]
- Accept patches via GitHub PRs [[todo]]
## Workflow
@@ -128,6 +128,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[good-first-task]: ../../dev/good-first-task.md "Good First Task"
[including-notes]: ../features/including-notes.md "Including notes in a note"
[write-your-notes-in-github-gist]: write-your-notes-in-github-gist.md "Write your notes in GitHub Gist"
[automatic-git-syncing]: automatic-git-syncing.md "Automatically Sync with Git"
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "GitHub Pages"
[publish-to-gitlab-pages]: ../publishing/publish-to-gitlab-pages.md "GitLab Pages"
[publish-to-azure-devops-wiki]: ../publishing/publish-to-azure-devops-wiki.md "Publish to Azure DevOps Wiki"
@@ -137,6 +138,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[publish-to-github]: ../publishing/publish-to-github.md "Publish to GitHub"
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
[real-time-collaboration]: real-time-collaboration.md "Real-time Collaboration"
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
[capture-notes-with-shortcuts-and-github-actions]: capture-notes-with-shortcuts-and-github-actions.md "Capture Notes With Shortcuts and GitHub Actions"
[//end]: # "Autogenerated link references"

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.19.2"
"version": "0.20.4"
}

View File

@@ -9,9 +9,10 @@
"packages/*"
],
"scripts": {
"vscode:package-extension": "yarn workspace foam-vscode package-extension",
"vscode:install-extension": "yarn workspace foam-vscode install-extension",
"vscode:publish-extension": "yarn workspace foam-vscode publish-extension",
"version-extension": "lerna version",
"package-extension": "yarn workspace foam-vscode package-extension",
"install-extension": "yarn workspace foam-vscode install-extension",
"publish-extension": "yarn workspace foam-vscode publish-extension",
"reset": "yarn && yarn clean && yarn build",
"clean": "lerna run clean",
"build": "lerna run build",

View File

@@ -1,7 +1,12 @@
.vscode/**
.vscode-test/**
out/test/**
out/**/*.test.*
out/**/*.spec.*
test-data/**
src/**
jest.config.js
.test-workspace
.gitignore
vsc-extension-quickstart.md
**/tsconfig.json

View File

@@ -4,13 +4,82 @@ 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.2] - 2022-07-11
## [0.20.4] - 2023-01-04
Fixes and Improvements:
- Added support for emoji tags (#1125 - thanks @badsketch)
## [0.20.3] - 2022-12-19
Fixes and Improvements:
- Show number of entries in title for orphan, placeholder, tag treeviews
## [0.20.2] - 2022-10-26
Fixes and Improvements:
- Creating new note uses default template when none is provided (#1094)
Internal:
- Changed matcher implementation to remove dependency on micromatch/glob
- Removed unnecessary dependencies and assets from extension
## [0.20.1] - 2022-10-13
Fixes and Improvements:
- Improved support for daily notes in multi root workspace (#1073)
- Create note from placeholder using template (#1061 - thanks @Dominic-DallOsto)
- Improved support for globs in multi root workspace (#1083)
## [0.20.0] - 2022-09-30
New Features:
- Added `foam-vscode.create-note` command, which can be very customized for several use cases (#1076)
Fixes and Improvements:
- Removed `+` as a trigger char for date snippets
- Improved attachment support (#915)
- Improved error handling when starting Foam without an open workspace (#908)
- Added support for opening non-text files via wikilink (#915)
- Dataviz: now clicking is enough to open a link from the graph
- Dataviz: clicking on images/attachments will open them
## [0.19.5] - 2022-09-01
Fixes and Improvements:
- Added `FOAM_DATE_WEEK` variable (#1053 - Thanks @dmurph)
- Fixed extension inclusion when generating references for attachments
- Link completion label can be note title as well as path (#1059)
- Images and attachments are not shown by default in graph view (#1056)
## [0.19.4] - 2022-08-07
Fixes and Improvements:
- Fixed note embed in preview (#1052)
## [0.19.3] - 2022-08-04
Fixes and Improvements:
- Image embeds fixed in preview (#1036)
## [0.19.2] - 2022-08-04
Fixes and Improvements:
- Added support for angle markdown links (#1044)
- Filter out invalid file name chars when creating note (#1042)
Internal:
- Reorganized docs (#1031, thanks @infogulch)
- Fixed documentation links (#1046)
- Preview code refactoring
@@ -18,39 +87,46 @@ Internal:
## [0.19.1] - 2022-07-11
Internal:
- Introduced cache for markdown parser (#1030)
- Various code refactorings
## [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)
@@ -58,6 +134,7 @@ Fixes and Improvements:
## [0.18.1] - 2022-04-13
Fixes and Improvements:
- Fixed parsing error for direct links with square brackets in them (#977)
- Improved markdown direct link resolution (#972)
- Improved templates support for custom paths (#970)
@@ -65,14 +142,17 @@ Fixes and Improvements:
## [0.18.0] - 2022-04-11
Features:
- Link synchronization on file rename
Internal:
- Changed graph computation on workspace change to simplify code
## [0.17.8] - 2022-04-01
Fixes and Improvements:
- Do not add ignored files to Foam upon change (#480)
- Restore full use of editor.action.openLink (#693)
- Minor performance improvements
@@ -80,49 +160,58 @@ Fixes and Improvements:
## [0.17.7] - 2022-03-29
Fixes and Improvements:
- Include links with sections in backlinks (#895)
- Improved navigation when document editor is already open
## [0.17.6] - 2022-03-03
Fixes and Improvements:
- Don't fail on error when scannig workspace (#943 - thanks @develmusa)
## [0.17.5] - 2022-02-22
Fixes and Improvements:
- Added FOAM_SLUG template variable (#865 - Thanks @techCarpenter)
## [0.17.4] - 2022-02-13
Fixes and Improvements:
- Improvements to Foam variables in templates (#882 - thanks @movermeyer)
- Foam variables can now be used just any other VS Code variables, including in combination with placeholders and transformers
## [0.17.3] - 2022-01-14
Fixes and Improvements:
- Fixed autocompletion with tags (#885 - thanks @memeplex)
- Improved "Open Daily Note" to be usabled in tasks (#897 - thanks @MCluck90)
## [0.17.2] - 2021-12-22
Fixes and Improvements:
- Improved support for wikilinks in titles (#878)
- Use syntax injection for wikilinks (#876 - thanks @memeplex)
- Fix when applying text edits in last line
- Fix when applying text edits in last line
Internal:
- DX: Clean up of testing setup (#881 - thanks @memeplex)
## [0.17.1] - 2021-12-16
Fixes and Improvements:
- Decorate markdown files only (#857)
- Fix template placeholders issue (#859)
- Improved replacement range for link completion
Internal:
- Major URI/path handling refactoring (#858 - thanks @memeplex)
## [0.17.0] - 2021-12-08
@@ -178,7 +267,6 @@ Fixes and Improvements:
- Link Reference Generation is now OFF by default
- Fixed preview navigation (#830)
## [0.15.5] - 2021-11-15
Fixes and Improvements:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 KiB

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.19.2",
"version": "0.20.4",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -29,6 +29,7 @@
"onCommand:foam-vscode.copy-without-brackets",
"onCommand:foam-vscode.show-graph",
"onCommand:foam-vscode.create-new-template",
"onCommand:foam-vscode.create-note",
"onCommand:foam-vscode.create-note-from-template",
"onCommand:foam-vscode.create-note-from-default-template"
],
@@ -128,6 +129,10 @@
}
],
"commandPalette": [
{
"command": "foam-vscode.create-note-from-default-template",
"when": "false"
},
{
"command": "foam-vscode.update-graph",
"when": "false"
@@ -159,6 +164,10 @@
]
},
"commands": [
{
"command": "foam-vscode.create-note",
"title": "Foam: Create New Note"
},
{
"command": "foam-vscode.clear-cache",
"title": "Foam: Clear Cache"
@@ -243,6 +252,34 @@
"configuration": {
"title": "Foam",
"properties": {
"foam.completion.label": {
"type": "string",
"default": "path",
"description": "Describes what note property to use as a label for completion items",
"enum": [
"path",
"title",
"identifier"
],
"enumDescriptions": [
"Use the path of the note",
"Use the title of the note",
"Use the identifier of the note"
]
},
"foam.completion.useAlias": {
"type": "string",
"default": "never",
"description": "Specifies in which cases to use an alias when creating a wikilink",
"enum": [
"never",
"whenPathDiffersFromTitle"
],
"enumDescriptions": [
"Never use aliases in completion items",
"Use alias if resource path is different from title"
]
},
"foam.files.ignore": {
"type": [
"array"
@@ -255,6 +292,11 @@
],
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`"
},
"foam.files.attachmentExtensions": {
"type": "string",
"default": "pdf mp3 webm wav m4a mp4 avi mov rtf txt doc docx pages xls xlsx numbers ppt pptm pptx",
"description": "Space separated list of file extensions that will be considered attachments"
},
"foam.logging.level": {
"type": "string",
"default": "info",
@@ -443,10 +485,12 @@
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^25.3.0",
"glob": "^7.1.6",
"husky": "^4.2.5",
"jest": "^26.2.2",
"jest-extended": "^0.11.5",
"markdown-it": "^12.0.4",
"micromatch": "^4.0.2",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"tsdx": "^0.13.2",
@@ -458,19 +502,14 @@
"dependencies": {
"dateformat": "^3.0.3",
"detect-newline": "^3.1.0",
"fast-array-diff": "^1.0.1",
"github-slugger": "^1.4.0",
"glob": "^7.1.6",
"gray-matter": "^4.0.2",
"lodash": "^4.17.21",
"lru-cache": "^7.12.0",
"markdown-it-regex": "^0.2.0",
"micromatch": "^4.0.2",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",
"remark-wiki-link": "^0.0.4",
"remove-markdown": "^0.3.0",
"replace-ext": "^2.0.0",
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2",

View File

@@ -4,10 +4,10 @@ import { MarkdownResourceProvider } from '../services/markdown-provider';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import { FoamWorkspace } from '../model/workspace';
import { FileDataStore, Matcher } from '../services/datastore';
import { Logger } from '../utils/log';
import detectNewline from 'detect-newline';
import { createMarkdownParser } from '../services/markdown-parser';
import { FileDataStore } from '../../test/test-datastore';
Logger.setLevel('error');
@@ -20,11 +20,13 @@ describe('generateHeadings', () => {
};
beforeAll(async () => {
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
const dataStore = new FileDataStore(readFileFromFs);
const dataStore = new FileDataStore(
readFileFromFs,
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
);
const parser = createMarkdownParser();
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
});
it.skip('should add heading to a file that does not have them', async () => {

View File

@@ -4,12 +4,12 @@ import { MarkdownResourceProvider } from '../services/markdown-provider';
import { Resource } from '../model/note';
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';
import { EOL } from 'os';
import { createMarkdownParser } from '../services/markdown-parser';
import { FileDataStore } from '../../test/test-datastore';
Logger.setLevel('error');
@@ -23,14 +23,16 @@ describe('generateLinkReferences', () => {
};
beforeAll(async () => {
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
/** 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 dataStore = new FileDataStore(
readFile,
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
);
const parser = createMarkdownParser();
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
});
it('initialised test graph correctly', () => {

View File

@@ -1,5 +1,5 @@
import { IDisposable } from '../common/lifecycle';
import { IDataStore, IMatcher } from '../services/datastore';
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
import { FoamWorkspace } from './workspace';
import { FoamGraph } from './graph';
import { ResourceParser } from './note';
@@ -22,14 +22,18 @@ export interface Foam extends IDisposable {
export const bootstrap = async (
matcher: IMatcher,
watcher: IWatcher | undefined,
dataStore: IDataStore,
parser: ResourceParser,
initialProviders: ResourceProvider[]
) => {
const workspace = new FoamWorkspace();
const tsStart = Date.now();
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
const workspace = await FoamWorkspace.fromProviders(
initialProviders,
dataStore
);
const tsWsDone = Date.now();
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
@@ -41,13 +45,28 @@ export const bootstrap = async (
const tsTagsEnd = Date.now();
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
watcher?.onDidChange(async uri => {
if (matcher.isMatch(uri)) {
await workspace.fetchAndSet(uri);
}
});
watcher?.onDidCreate(async uri => {
await matcher.refresh();
if (matcher.isMatch(uri)) {
await workspace.fetchAndSet(uri);
}
});
watcher?.onDidDelete(uri => {
workspace.delete(uri);
});
const foam: Foam = {
workspace,
graph,
tags,
services: {
dataStore,
parser,
dataStore,
matcher,
},
dispose: () => {

View File

@@ -4,7 +4,6 @@ import { URI } from './uri';
import { FoamWorkspace } from './workspace';
export interface ResourceProvider extends IDisposable {
init: (workspace: FoamWorkspace) => Promise<void>;
supports: (uri: URI) => boolean;
readAsMarkdown: (uri: URI) => Promise<string | null>;
fetch: (uri: URI) => Promise<Resource | null>;

View File

@@ -1,5 +1,5 @@
import { Logger } from '../utils/log';
import { URI } from './uri';
import { asAbsoluteUri, URI } from './uri';
Logger.setLevel('error');
@@ -81,3 +81,47 @@ describe('Foam URI', () => {
).toEqual(URI.file('../a/note.md'));
});
});
describe('asAbsoluteUri', () => {
it('should throw if no workspace folder is found', () => {
expect(() => asAbsoluteUri(URI.file('relative/path'), [])).toThrow();
});
it('should return the given URI if already absolute', () => {
const uri = URI.file('/absolute/path');
expect(asAbsoluteUri(uri, [URI.file('/base')])).toEqual(uri);
});
describe('with relative URI', () => {
it('should return a URI relative if the given URI is relative and there is only one workspace folder', () => {
const uri = URI.file('relative/path');
const workspaceFolder = URI.file('/workspace/folder');
expect(asAbsoluteUri(uri, [workspaceFolder])).toEqual(
workspaceFolder.joinPath(uri.path)
);
});
it('should match the first folder with the same name as the first part of the URI', () => {
const uri = URI.file('folder2/file');
const workspaceFolder1 = URI.file('/absolute/path/folder1');
const workspaceFolder2 = URI.file('/absolute/path/folder2');
expect(asAbsoluteUri(uri, [workspaceFolder1, workspaceFolder2])).toEqual(
workspaceFolder2.joinPath('file')
);
});
});
it('should use the first folder if no matching folder is found', () => {
const uri = URI.file('folder3/file');
const workspaceFolder1 = URI.file('/absolute/path/folder1');
const workspaceFolder2 = URI.file('/absolute/path/folder2');
expect(asAbsoluteUri(uri, [workspaceFolder1, workspaceFolder2])).toEqual(
workspaceFolder1.joinPath(uri.path)
);
});
it('should use the first matching folder', () => {
const uri = URI.file('folder/file');
const workspaceFolder1 = URI.file('/absolute/path1');
const workspaceFolder2 = URI.file('/absolute/path2/folder');
const workspaceFolder3 = URI.file('/absolute/path3/folder');
expect(
asAbsoluteUri(uri, [workspaceFolder1, workspaceFolder2, workspaceFolder3])
).toEqual(workspaceFolder2.joinPath('file'));
});
});

View File

@@ -367,3 +367,24 @@ function encodeURIComponentMinimal(path: string): string {
}
return res !== undefined ? res : path;
}
/**
* Turns a relative URI into an absolute URI given a collection of base folders.
* In case of multiple matches it returns the first one.
*
* @see {@link pathUtils.asAbsolutePaths|path.asAbsolutePath}
*
* @param uri the uri to evaluate
* @param baseFolders the base folders to use
* @returns an absolute uri
*
* TODO this probably needs to be moved to the workspace service
*/
export function asAbsoluteUri(uri: URI, baseFolders: URI[]): URI {
return URI.file(
pathUtils.asAbsolutePaths(
uri.path,
baseFolders.map(f => f.path)
)[0]
);
}

View File

@@ -5,6 +5,7 @@ import { isSome } from '../utils';
import { Emitter } from '../common/event';
import { ResourceProvider } from './provider';
import { IDisposable } from '../common/lifecycle';
import { IDataStore } from '../services/datastore';
export class FoamWorkspace implements IDisposable {
private onDidAddEmitter = new Emitter<Resource>();
@@ -23,7 +24,6 @@ export class FoamWorkspace implements IDisposable {
registerProvider(provider: ResourceProvider) {
this.providers.push(provider);
return provider.init(this);
}
set(resource: Resource) {
@@ -159,6 +159,20 @@ export class FoamWorkspace implements IDisposable {
return Promise.resolve(null);
}
/**
* Takes a resource URI, and adds it to the workspace as a resource.
* If the URI is not supported by any provider or is not found, it will not
* add anything to the workspace, and return null.
*
* @param uri the URI where the resource is located
* @returns A promise to the Resource, or null if none was found
*/
public async fetchAndSet(uri: URI): Promise<Resource | null> {
const resource = await this.fetch(uri);
resource && this.set(resource);
return resource;
}
public readAsMarkdown(uri: URI): Promise<string | null> {
for (const provider of this.providers) {
if (provider.supports(uri)) {
@@ -220,12 +234,13 @@ export class FoamWorkspace implements IDisposable {
}
static async fromProviders(
providers: ResourceProvider[]
providers: ResourceProvider[],
dataStore: IDataStore
): Promise<FoamWorkspace> {
const workspace = new FoamWorkspace();
for (const provider of providers) {
await workspace.registerProvider(provider);
}
await Promise.all(providers.map(p => workspace.registerProvider(p)));
const files = await dataStore.list();
await Promise.all(files.map(f => workspace.fetchAndSet(f)));
return workspace;
}
}

View File

@@ -1,13 +1,19 @@
import { Resource, ResourceLink } from '../model/note';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { FoamWorkspace } from '../model/workspace';
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
import { getFoamVsCodeConfig } from '../../services/config';
const imageExtensions = ['.png', '.jpg', '.gif'];
const attachmentExtensions = ['.pdf', ...imageExtensions];
const attachmentExtConfig = getFoamVsCodeConfig(
'files.attachmentExtensions',
''
)
.split(' ')
.map(ext => '.' + ext.trim());
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
const attachmentExtensions = [...attachmentExtConfig, ...imageExtensions];
const asResource = (uri: URI): Resource => {
const type = imageExtensions.includes(uri.getExtension())
@@ -29,48 +35,10 @@ const asResource = (uri: URI): Resource => {
export class AttachmentResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
constructor(
private readonly matcher: IMatcher,
private readonly dataStore: IDataStore,
private readonly watcher?: IWatcher
) {}
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.debug('Found: ' + uri.toString());
workspace.set(asResource(uri));
}
if (this.watcher != null) {
this.disposables = [
this.watcher.onDidChange(async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
workspace.set(asResource(uri));
}
}),
this.watcher.onDidCreate(async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
workspace.set(asResource(uri));
}
}),
this.watcher.onDidDelete(uri => {
this.supports(uri) && workspace.delete(uri);
}),
];
}
}
supports(uri: URI) {
return attachmentExtensions.includes(uri.getExtension());
return attachmentExtensions.includes(
uri.getExtension().toLocaleLowerCase()
);
}
async readAsMarkdown(uri: URI): Promise<string | null> {

View File

@@ -1,7 +1,7 @@
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
import { Matcher, toMatcherPathFormat } from '../../test/test-datastore';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { URI } from '../model/uri';
import { Logger } from '../utils/log';
import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
Logger.setLevel('error');
@@ -62,11 +62,11 @@ describe('Matcher', () => {
});
it('happy path', () => {
const matcher = new Matcher([URI.file('/')], ['**/*'], ['**/*.pdf']);
expect(matcher.isMatch(URI.file('/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/file.pdf'))).toBeFalsy();
expect(matcher.isMatch(URI.file('/dir/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/dir/file.pdf'))).toBeFalsy();
const matcher = new Matcher([URI.file('/root/')], ['**/*'], ['**/*.pdf']);
expect(matcher.isMatch(URI.file('/root/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/root/file.pdf'))).toBeFalsy();
expect(matcher.isMatch(URI.file('/root/dir/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/root/dir/file.pdf'))).toBeFalsy();
});
it('ignores files in the exclude list', () => {
@@ -83,11 +83,3 @@ describe('Matcher', () => {
expect(matcher.isMatch(files[3])).toEqual(false);
});
});
describe('Datastore', () => {
it('uses the matcher to get the file list', async () => {
const matcher = new Matcher([testFolder], ['**/*.md'], []);
const ds = new FileDataStore(readFileFromFs);
expect((await ds.list(matcher.include[0])).length).toEqual(4);
});
});

View File

@@ -1,12 +1,30 @@
import micromatch from 'micromatch';
import { URI } from '../model/uri';
import { Logger } from '../utils/log';
import { glob } from 'glob';
import { promisify } from 'util';
import { isWindows } from '../common/platform';
import { Event } from '../common/event';
const findAllFiles = promisify(glob);
/**
* Represents a source of files and content
*/
export interface IDataStore {
/**
* List the files matching the given glob from the
* store
*/
list: () => Promise<URI[]>;
/**
* Read the content of the file from the store
*
* Returns `null` in case of errors while reading
*/
read: (uri: URI) => Promise<string | null>;
}
export interface IWatcher {
onDidChange: Event<URI>;
onDidCreate: Event<URI>;
onDidDelete: Event<URI>;
}
export interface IMatcher {
/**
@@ -24,6 +42,14 @@ export interface IMatcher {
*/
isMatch(uri: URI): boolean;
/**
* Refreshes the list of files that this matcher matches
* To be used when new files are added to the workspace,
* it can be a more or less expensive operation depending on the
* implementation of the matcher
*/
refresh(): Promise<void>;
/**
* The include globs
*/
@@ -35,100 +61,14 @@ export interface IMatcher {
exclude: string[];
}
/**
* The matcher requires the path to be in unix format, so if we are in windows
* we convert the fs path on the way in and out
*/
export const toMatcherPathFormat = isWindows
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
: (uri: URI) => uri.toFsPath();
export const toFsPath = isWindows
? (path: string): string => path.replace(/\//g, '\\')
: (path: string): string => path;
export class Matcher implements IMatcher {
public readonly folders: string[];
public readonly include: string[] = [];
public readonly exclude: string[] = [];
export class GenericDataStore implements IDataStore {
constructor(
baseFolders: URI[],
include: string[] = ['**/*'],
exclude: string[] = []
) {
this.folders = baseFolders.map(toMatcherPathFormat);
Logger.info('Workspace folders: ', this.folders);
private readonly listFiles: () => Promise<URI[]>,
private readFile: (uri: URI) => Promise<string>
) {}
this.folders.forEach(folder => {
const withFolder = folderPlusGlob(folder);
this.include.push(
...include.map(glob => {
return withFolder(glob);
})
);
this.exclude.push(...exclude.map(withFolder));
});
Logger.info('Glob patterns', {
includeGlobs: this.include,
ignoreGlobs: this.exclude,
});
}
match(files: URI[]) {
const matches = micromatch(
files.map(f => f.toFsPath()),
this.include,
{
ignore: this.exclude,
nocase: true,
format: toFsPath,
}
);
return matches.map(URI.file);
}
isMatch(uri: URI) {
return this.match([uri]).length > 0;
}
}
export interface IWatcher {
onDidChange: Event<URI>;
onDidCreate: Event<URI>;
onDidDelete: Event<URI>;
}
/**
* Represents a source of files and content
*/
export interface IDataStore {
/**
* List the files matching the given glob from the
* store
*/
list: (glob: string, ignoreGlob?: string | string[]) => Promise<URI[]>;
/**
* Read the content of the file from the store
*
* Returns `null` in case of errors while reading
*/
read: (uri: URI) => Promise<string | null>;
}
/**
* 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,
strict: false,
});
return res.map(URI.file);
async list(): Promise<URI[]> {
return this.listFiles();
}
async read(uri: URI) {
@@ -143,12 +83,74 @@ export class FileDataStore implements IDataStore {
}
}
export const folderPlusGlob = (folder: string) => (glob: string): string => {
if (folder.substr(-1) === '/') {
folder = folder.slice(0, -1);
/**
* A matcher that instead of using globs uses a list of files to
* check the matches.
* The {@link refresh} function has been added to the interface to accomodate
* this matcher, far from ideal but to be refactored later
*/
export class FileListBasedMatcher implements IMatcher {
private files: string[] = [];
include: string[];
exclude: string[];
constructor(files: URI[], private readonly listFiles: () => Promise<URI[]>) {
this.files = files.map(f => f.path);
}
if (glob.startsWith('/')) {
glob = glob.slice(1);
match(files: URI[]): URI[] {
return files.filter(f => this.files.includes(f.path));
}
return folder.length > 0 ? `${folder}/${glob}` : glob;
};
isMatch(uri: URI): boolean {
return this.files.includes(uri.path);
}
async refresh() {
this.files = (await this.listFiles()).map(f => f.path);
}
static async createFromListFn(listFiles: () => Promise<URI[]>) {
const files = await listFiles();
return new FileListBasedMatcher(files, listFiles);
}
}
/**
* A matcher that includes all URIs passed to it
*/
export class AlwaysIncludeMatcher implements IMatcher {
include: string[] = ['**/*'];
exclude: string[] = [];
match(files: URI[]): URI[] {
return files;
}
isMatch(uri: URI): boolean {
return true;
}
refresh(): Promise<void> {
return;
}
}
export class SubstringExcludeMatcher implements IMatcher {
include: string[] = ['**/*'];
exclude: string[] = [];
constructor(exclude: string) {
this.exclude = [exclude];
}
match(files: URI[]): URI[] {
return files.filter(f => this.isMatch(f));
}
isMatch(uri: URI): boolean {
return !uri.path.includes(this.exclude[0]);
}
refresh(): Promise<void> {
return;
}
}

View File

@@ -301,6 +301,21 @@ describe('Generation of markdown references', () => {
expect(references.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
});
it('should always add extensions for attachments, even when includeExtension = false', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[page-b]] and [[image.png]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir1/page-b.md'))
.set(createNoteFromMarkdown('', '/dir1/image.png'));
const references = createMarkdownReferences(workspace, noteA.uri, false);
expect(references.map(r => r.url)).toEqual(['page-b', 'image.png']);
});
it('should use relative paths', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(

View File

@@ -8,64 +8,19 @@ import { isNone, isSome } from '../utils';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { FoamWorkspace } from '../model/workspace';
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
import { MarkdownLink } from './markdown-link';
import { IDataStore } from './datastore';
export class MarkdownResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
constructor(
private readonly matcher: IMatcher,
private readonly dataStore: IDataStore,
private readonly parser: ResourceParser,
private readonly watcher?: IWatcher
private readonly parser: ResourceParser
) {}
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);
await Promise.all(
files.map(async uri => {
Logger.debug('Found: ' + uri.toString());
const content = await this.dataStore.read(uri);
if (isSome(content)) {
workspace.set(this.parser.parse(uri, content));
}
})
);
if (this.watcher != null) {
this.disposables = [
this.watcher.onDidChange(async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
}),
this.watcher.onDidCreate(async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
}),
this.watcher.onDidDelete(uri => {
this.supports(uri) && workspace.delete(uri);
}),
];
}
}
supports(uri: URI) {
return uri.isMarkdown();
}
@@ -181,7 +136,7 @@ to generate markdown reference list`
}
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
if (!includeExtension) {
if (!includeExtension && relativeUri.path.endsWith('.md')) {
relativeUri = relativeUri.changeExtension('*', '');
}

View File

@@ -1,6 +1,6 @@
import { isSome } from './core';
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
export const extractHashtags = (
text: string

View File

@@ -0,0 +1,30 @@
import { asAbsolutePaths } from './path';
describe('path utils', () => {
describe('asAbsolutePaths', () => {
it('returns the path if already absolute', () => {
const paths = asAbsolutePaths('/path/to/test', [
'/root/Users',
'/root/tmp',
]);
expect(paths).toEqual(['/path/to/test']);
});
it('returns the matching base if found', () => {
const paths = asAbsolutePaths('tmp/to/test', [
'/root/Users',
'/root/tmp',
]);
expect(paths).toEqual(['/root/tmp/to/test']);
});
it('returns all bases if no match is found', () => {
const paths = asAbsolutePaths('path/to/test', [
'/root/Users',
'/root/tmp',
]);
expect(paths).toEqual([
'/root/Users/path/to/test',
'/root/tmp/path/to/test',
]);
});
});
});

View File

@@ -1,5 +1,6 @@
import { CharCode } from '../common/charCode';
import { posix } from 'path';
import { isNone } from './core';
/**
* Converts filesystem path to POSIX path. Supported inputs are:
@@ -174,3 +175,46 @@ function parseUNCShare(uncPath: string): [string, string] {
return [uncPath.substring(2, idx), uncPath.substring(idx) || '\\'];
}
}
/**
* Turns a relative path into an absolute path given a collection of base folders.
* - if no base folder is provided, it will throw
* - if the given path is already absolute, it will return it
* - if the given path is relative it will return absolute paths for the ones matching the
* first part of the path
* - if no matching base folder is found, it will return an absolute path per base folder
* @param path the path to evaluate
* @param baseFolders the base folders to use
* @returns an array of absolute path, guaranteed to have at least 1 element
*/
export function asAbsolutePaths(path: string, baseFolders: string[]): string[] {
if (isNone(baseFolders) || baseFolders.length === 0) {
throw new Error('Cannot compute absolute URI without a base');
}
if (isAbsolute(path)) {
return [path];
}
let tokens = path.split('/');
const firstDir = tokens[0];
const res = [];
if (baseFolders.length > 1) {
for (const folder of baseFolders) {
const lastDir = folder.split('/').pop();
if (lastDir === firstDir) {
tokens = tokens.slice(1);
res.push([folder, ...tokens].join('/'));
continue;
}
}
}
if (res.length === 0) {
for (const folder of baseFolders) {
const match = folder.endsWith('/')
? folder.substring(0, folder.length - 1)
: folder;
res.push([match, ...tokens].join('/'));
}
}
return res;
}

View File

@@ -7,11 +7,13 @@ describe('hashtag extraction', () => {
it('returns empty list if no tags are present', () => {
expect(extractHashtags('hello world')).toEqual([]);
});
it('works with simple strings', () => {
expect(
extractHashtags('hello #world on #this planet').map(t => t.label)
).toEqual(['world', 'this']);
});
it('detects the offset of the tag', () => {
expect(extractHashtags('#hello')).toEqual([{ label: 'hello', offset: 0 }]);
expect(extractHashtags(' #hello')).toEqual([{ label: 'hello', offset: 1 }]);
@@ -19,21 +21,25 @@ describe('hashtag extraction', () => {
{ label: 'hello', offset: 3 },
]);
});
it('works with tags at beginning or end of text', () => {
expect(
extractHashtags('#hello world on this #planet').map(t => t.label)
).toEqual(['hello', 'planet']);
});
it('supports _ and -', () => {
expect(
extractHashtags('#hello-world on #this_planet').map(t => t.label)
).toEqual(['hello-world', 'this_planet']);
});
it('supports nested tags', () => {
expect(
extractHashtags('#parent/child on #planet').map(t => t.label)
).toEqual(['parent/child', 'planet']);
});
it('ignores tags that only have numbers in text', () => {
expect(
extractHashtags('this #123 tag should be ignore, but not #123four').map(
@@ -41,7 +47,8 @@ describe('hashtag extraction', () => {
)
).toEqual(['123four']);
});
it('supports unicode letters like Chinese charaters', () => {
it('supports unicode letters like Chinese characters', () => {
expect(
extractHashtags(`
this #tag_with_unicode_letters_汉字, pure Chinese tag like #纯中文标签 and
@@ -55,6 +62,24 @@ describe('hashtag extraction', () => {
]);
});
it('supports emoji tags', () => {
expect(
extractHashtags(`this is a pure emoji #⭐, #⭐⭐, #👍👍🏽👍🏿 some mixed emoji #π🥧, #✅todo
#urgent❗ or #❗❗urgent, and some nested emoji #📥/🟥 or #📥/🟢
`).map(t => t.label)
).toEqual([
'⭐',
'⭐⭐',
'👍👍🏽👍🏿',
'π🥧',
'✅todo',
'urgent❗',
'❗❗urgent',
'📥/🟥',
'📥/🟢',
]);
});
it('ignores hashes in plain text urls and links', () => {
expect(
extractHashtags(`

View File

@@ -2,9 +2,10 @@ import { workspace } from 'vscode';
import dateFormat from 'dateformat';
import { focusNote } from './utils';
import { URI } from './core/model/uri';
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
import { toVsCodeUri } from './utils/vsc-utils';
import { NoteFactory } from './services/templates';
import { getFoamVsCodeConfig } from './services/config';
import { asAbsoluteWorkspaceUri } from './services/editor';
/**
* Open the daily note file.
@@ -42,17 +43,9 @@ export async function openDailyNoteFor(date?: Date) {
*/
export function getDailyNotePath(date: Date): URI {
const folder = getFoamVsCodeConfig<string>('openDailyNote.directory') ?? '.';
const dailyNoteDirectory = URI.file(folder);
const dailyNoteDirectory = asAbsoluteWorkspaceUri(URI.file(folder));
const dailyNoteFilename = getDailyNoteFileName(date);
if (dailyNoteDirectory.isAbsolute()) {
return dailyNoteDirectory.joinPath(dailyNoteFilename);
} else {
return fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
dailyNoteDirectory.path,
dailyNoteFilename
);
}
return dailyNoteDirectory.joinPath(dailyNoteFilename);
}
/**

View File

@@ -1,18 +1,16 @@
import { workspace, ExtensionContext, window, commands } 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, toVsCodeUri } from './utils/vsc-utils';
import { AttachmentResourceProvider } from './core/services/attachment-provider';
import { VsCodeWatcher } from './services/watcher';
import { createMarkdownParser } from './core/services/markdown-parser';
import VsCodeBasedParserCache from './services/cache';
import { createMatcherAndDataStore } from './services/editor';
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -22,34 +20,36 @@ export async function activate(context: ExtensionContext) {
try {
Logger.info('Starting Foam');
if (workspace.workspaceFolders === undefined) {
Logger.info('No workspace open. Foam will not start');
return;
}
// Prepare Foam
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 excludes = getIgnoredFilesSetting().map(g => g.toString());
const {
matcher,
dataStore,
excludePatterns,
} = await createMatcherAndDataStore(excludes);
Logger.info('Loading from directories:');
for (const folder of workspace.workspaceFolders) {
Logger.info('- ' + folder.uri.fsPath);
Logger.info(' Include: **/*');
Logger.info(' Exclude: ' + excludePatterns.get(folder.name).join(','));
}
const watcher = new VsCodeWatcher(
workspace.createFileSystemWatcher('**/*')
);
const parserCache = new VsCodeBasedParserCache(context);
const parser = createMarkdownParser([], parserCache);
const markdownProvider = new MarkdownResourceProvider(
matcher,
dataStore,
parser,
watcher
);
const attachmentProvider = new AttachmentResourceProvider(
matcher,
dataStore,
watcher
);
const markdownProvider = new MarkdownResourceProvider(dataStore, parser);
const attachmentProvider = new AttachmentResourceProvider();
const foamPromise = bootstrap(matcher, dataStore, parser, [
const foamPromise = bootstrap(matcher, watcher, dataStore, parser, [
markdownProvider,
attachmentProvider,
]);
@@ -59,6 +59,7 @@ export async function activate(context: ExtensionContext) {
const foam = await foamPromise;
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
context.subscriptions.push(
foam,
watcher,

View File

@@ -1,18 +1,26 @@
import { commands, ExtensionContext } from 'vscode';
import { commands, window, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
import { DEFAULT_TEMPLATE_URI, NoteFactory } from '../../services/templates';
import { getDefaultTemplateUri, NoteFactory } from '../../services/templates';
import { Resolver } from '../../services/variable-resolver';
/**
* Create a new note from the default template.
*
* @deprecated use 'foam-vscode.create-note' instead
*/
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.create-note-from-default-template',
() => {
window.showWarningMessage(
"This command is deprecated, use 'Foam: Create Note' (foam-vscode.create-note) instead"
);
const resolver = new Resolver(new Map(), new Date());
return NoteFactory.createFromTemplate(
DEFAULT_TEMPLATE_URI,
getDefaultTemplateUri(),
resolver,
undefined,
`---

View File

@@ -1,11 +1,6 @@
import { commands, ExtensionContext, QuickPickItem, window } from 'vscode';
import { commands, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
import {
getTemplateMetadata,
getTemplates,
NoteFactory,
TEMPLATES_DIR,
} from '../../services/templates';
import { askUserForTemplate, NoteFactory } from '../../services/templates';
import { Resolver } from '../../services/variable-resolver';
const feature: FoamFeature = {
@@ -14,102 +9,17 @@ const feature: FoamFeature = {
commands.registerCommand(
'foam-vscode.create-note-from-template',
async () => {
const selectedTemplate = await askUserForTemplate();
if (selectedTemplate === undefined) {
return;
const templateUri = await askUserForTemplate();
if (templateUri) {
const resolver = new Resolver(new Map(), new Date());
await NoteFactory.createFromTemplate(templateUri, resolver);
}
const templateFilename =
(selectedTemplate as QuickPickItem).description ||
(selectedTemplate as QuickPickItem).label;
const templateUri = TEMPLATES_DIR.joinPath(templateFilename);
const resolver = new Resolver(new Map(), new Date());
await NoteFactory.createFromTemplate(templateUri, resolver);
}
)
);
},
};
async function offerToCreateTemplate(): Promise<void> {
const response = await window.showQuickPick(['Yes', 'No'], {
placeHolder:
'No templates available. Would you like to create one instead?',
});
if (response === 'Yes') {
commands.executeCommand('foam-vscode.create-new-template');
return;
}
}
function sortTemplatesMetadata(
t1: Map<string, string>,
t2: Map<string, string>
) {
// Sort by name's existence, then name, then path
if (t1.get('name') === undefined && t2.get('name') !== undefined) {
return 1;
}
if (t1.get('name') !== undefined && t2.get('name') === undefined) {
return -1;
}
const pathSortOrder = t1
.get('templatePath')
.localeCompare(t2.get('templatePath'));
if (t1.get('name') === undefined && t2.get('name') === undefined) {
return pathSortOrder;
}
const nameSortOrder = t1.get('name').localeCompare(t2.get('name'));
return nameSortOrder || pathSortOrder;
}
async function askUserForTemplate() {
const templates = await getTemplates();
if (templates.length === 0) {
return offerToCreateTemplate();
}
const templatesMetadata = (
await Promise.all(
templates.map(async templateUri => {
const metadata = await getTemplateMetadata(templateUri);
metadata.set('templatePath', templateUri.getBasename());
return metadata;
})
)
).sort(sortTemplatesMetadata);
const items: QuickPickItem[] = await Promise.all(
templatesMetadata.map(metadata => {
const label = metadata.get('name') || metadata.get('templatePath');
const description = metadata.get('name')
? metadata.get('templatePath')
: null;
const detail = metadata.get('description');
const item = {
label: label,
description: description,
detail: detail,
};
Object.keys(item).forEach(key => {
if (!item[key]) {
delete item[key];
}
});
return item;
})
);
return await window.showQuickPick(items, {
placeHolder: 'Select a template to use.',
});
}
export default feature;

View File

@@ -0,0 +1,129 @@
import { commands, window } from 'vscode';
import { URI } from '../../core/model/uri';
import { asAbsoluteWorkspaceUri, readFile } from '../../services/editor';
import {
closeEditors,
createFile,
deleteFile,
expectSameUri,
getUriInWorkspace,
} from '../../test/test-utils-vscode';
describe('create-note command', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('uses sensible defaults to work even without params', async () => {
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve('Test note')));
await commands.executeCommand('foam-vscode.create-note');
expect(spy).toBeCalled();
const target = asAbsoluteWorkspaceUri(URI.file('Test note.md'));
expectSameUri(target, window.activeTextEditor?.document.uri);
await deleteFile(target);
});
it('gives precedence to the template over the text', async () => {
const templateA = await createFile('Template A', [
'.foam',
'templates',
'template-for-create-note.md',
]);
const target = getUriInWorkspace();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.path,
templatePath: templateA.uri.path,
text: 'hello',
});
expect(window.activeTextEditor.document.getText()).toEqual('Template A');
expectSameUri(window.activeTextEditor.document.uri, target);
await deleteFile(target);
await deleteFile(templateA.uri);
});
it('focuses on the newly created note', async () => {
const target = getUriInWorkspace();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.path,
text: 'hello',
});
expect(window.activeTextEditor.document.getText()).toEqual('hello');
expectSameUri(window.activeTextEditor.document.uri, target);
await deleteFile(target);
});
it('supports variables', async () => {
const target = getUriInWorkspace();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.path,
text: 'hello ${FOAM_TITLE}', // eslint-disable-line no-template-curly-in-string
variables: { FOAM_TITLE: 'world' },
});
expect(window.activeTextEditor.document.getText()).toEqual('hello world');
expectSameUri(window.activeTextEditor.document.uri, target);
await deleteFile(target);
});
it('supports date variables', async () => {
const target = getUriInWorkspace();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.path,
text: 'hello ${FOAM_DATE_YEAR}', // eslint-disable-line no-template-curly-in-string
date: '2021-10-01',
});
expect(window.activeTextEditor.document.getText()).toEqual('hello 2021');
expectSameUri(window.activeTextEditor.document.uri, target);
await deleteFile(target);
});
it('supports various options to deal with existing notes', async () => {
const target = await createFile('hello');
const content = await readFile(target.uri);
expect(content).toEqual('hello');
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.uri.path,
text: 'test overwrite',
onFileExists: 'overwrite',
});
expect(window.activeTextEditor.document.getText()).toEqual(
'test overwrite'
);
expectSameUri(window.activeTextEditor.document.uri, target.uri);
await closeEditors();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.uri.path,
text: 'test open',
onFileExists: 'open',
});
expect(window.activeTextEditor.document.getText()).toEqual(
'test overwrite'
);
expectSameUri(window.activeTextEditor.document.uri, target.uri);
await closeEditors();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.uri.path,
text: 'test cancel',
onFileExists: 'cancel',
});
expect(window.activeTextEditor).toBeUndefined();
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
await closeEditors();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.uri.path,
text: 'test ask',
onFileExists: 'ask',
});
expect(spy).toBeCalled();
await deleteFile(target);
});
});

View File

@@ -0,0 +1,112 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { URI } from '../../core/model/uri';
import {
askUserForTemplate,
getDefaultTemplateUri,
getPathFromTitle,
NoteFactory,
} from '../../services/templates';
import { Foam } from '../../core/model/foam';
import { Resolver } from '../../services/variable-resolver';
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
import { isSome } from '../../core/utils';
interface CreateNoteArgs {
/**
* The path of the note to create.
* If relative it will be resolved against the workspace root.
*/
notePath?: string;
/**
* The path of the template to use.
*/
templatePath?: string;
/**
* Whether to ask the user to select a template for the new note. If so, overwrites templatePath.
*/
askForTemplate?: boolean;
/**
* The text to use for the note.
* If a template is provided, the template has precedence
*/
text?: string;
/**
* Variables to use in the text or template
*/
variables?: Map<string, string>;
/**
* The date used to resolve the FOAM_DATE_* variables. in YYYY-MM-DD format
*/
date?: string;
/**
* What to do in case the target file already exists
*/
onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel';
}
const DEFAULT_NEW_NOTE_TEXT = `# \${FOAM_TITLE}
\${FOAM_SELECTED_TEXT}`;
async function createNote(args: CreateNoteArgs) {
args = args ?? {};
const date = isSome(args.date) ? new Date(Date.parse(args.date)) : new Date();
const resolver = new Resolver(
new Map(Object.entries(args.variables ?? {})),
date
);
const text = args.text ?? DEFAULT_NEW_NOTE_TEXT;
const noteUri =
args.notePath && asAbsoluteWorkspaceUri(URI.file(args.notePath));
let templateUri: URI;
if (args.askForTemplate) {
const selectedTemplate = await askUserForTemplate();
if (selectedTemplate) {
templateUri = selectedTemplate;
} else {
return;
}
} else {
templateUri = args.templatePath
? asAbsoluteWorkspaceUri(URI.file(args.templatePath))
: getDefaultTemplateUri();
}
if (await fileExists(templateUri)) {
return NoteFactory.createFromTemplate(
templateUri,
resolver,
noteUri,
text,
args.onFileExists
);
} else {
return NoteFactory.createNote(
noteUri ?? (await getPathFromTitle(resolver)),
text,
resolver,
args.onFileExists
);
}
}
export const CREATE_NOTE_COMMAND = {
command: 'foam-vscode.create-note',
title: 'Foam: Create Note',
asURI: (args: CreateNoteArgs) =>
vscode.Uri.parse(`command:${CREATE_NOTE_COMMAND.command}`).with({
query: encodeURIComponent(JSON.stringify(args)),
}),
};
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, createNote)
);
},
};
export default feature;

View File

@@ -10,3 +10,4 @@ export { default as openRandomNoteCommand } from './open-random-note';
export { default as openResource } from './open-resource';
export { default as updateGraphCommand } from './update-graph';
export { default as updateWikilinksCommand } from './update-wikilinks';
export { default as createNote } from './create-note';

View File

@@ -28,13 +28,12 @@ const feature: FoamFeature = {
uri.path === vscode.window.activeTextEditor?.document.uri.path
? vscode.window.activeTextEditor?.document.uri
: toVsCodeUri(uri.asPlain());
// if the doc is already open, reuse the same colunm
const targetEditor = vscode.window.visibleTextEditors.find(
ed => targetUri.path === ed.document.uri.path
);
const column = targetEditor?.viewColumn;
return vscode.window.showTextDocument(targetUri, {
viewColumn: column,
});
return vscode.commands.executeCommand('vscode.open', targetUri);
}
case 'placeholder': {
const title = uri.getName();

View File

@@ -206,8 +206,7 @@ const feature: FoamFeature = {
languages.registerCompletionItemProvider(
'markdown',
datesCompletionProvider,
'/',
'+'
'/'
)
);
},

View File

@@ -3,30 +3,26 @@ 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 { FileDataStore, Matcher } from '../core/services/datastore';
import {
cleanWorkspace,
closeEditors,
createFile,
showInEditor,
} from '../test/test-utils-vscode';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { toVsCodeUri } from '../utils/vsc-utils';
import { HoverProvider } from './hover-provider';
import { readFileFromFs } from '../test/test-utils';
import { FileDataStore } from '../test/test-datastore';
// 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.
const createWorkspace = () => {
const matcher = new Matcher(
vscode.workspace.workspaceFolders.map(f => fromVsCodeUri(f.uri))
const dataStore = new FileDataStore(
readFileFromFs,
vscode.workspace.workspaceFolders[0].uri.fsPath
);
const dataStore = new FileDataStore(readFileFromFs);
const parser = createMarkdownParser();
const resourceProvider = new MarkdownResourceProvider(
matcher,
dataStore,
parser
);
const resourceProvider = new MarkdownResourceProvider(dataStore, parser);
const workspace = new FoamWorkspace();
workspace.registerProvider(resourceProvider);
return workspace;
@@ -179,7 +175,7 @@ describe('Hover provider', () => {
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(getValue(result.contents[0])).toEqual(
`This is some content from file B`
);
@@ -205,7 +201,7 @@ describe('Hover provider', () => {
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(getValue(result.contents[0])).toEqual(
`This is some content from file B`
);
@@ -235,7 +231,7 @@ The content of file B`);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(getValue(result.contents[0])).toEqual(`The content of file B`);
ws.dispose();
graph.dispose();
@@ -255,9 +251,12 @@ The content of file B`);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(result.contents[0]).toEqual(null);
expect(result.contents[1]).toEqual(null);
expect(getValue(result.contents[2])).toMatch(
"[Create note from template for 'wikilink'](command:foam-vscode.create-note?"
);
ws.dispose();
graph.dispose();
});
@@ -281,11 +280,12 @@ The content of file B`);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(getValue(result.contents[0])).toEqual(`This is some content`);
expect(getValue(result.contents[1])).toMatch(
/^Also referenced in 1 note:/
);
expect(result.contents[2]).toEqual(null);
ws.dispose();
graph.dispose();
});
@@ -309,7 +309,7 @@ The content of file B`);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(getValue(result.contents[1])).toMatch(
/^Also referenced in 1 note:/
);
@@ -333,12 +333,14 @@ The content of file B`);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
const result = await provider.provideHover(doc, pos, noCancelToken);
expect(result.contents).toHaveLength(2);
expect(result.contents).toHaveLength(3);
expect(result.contents[0]).toEqual(null);
expect(getValue(result.contents[1])).toMatch(
/^Also referenced in 2 notes:/
);
expect(getValue(result.contents[2])).toMatch(
"[Create note from template for 'placeholder'](command:foam-vscode.create-note?"
);
ws.dispose();
graph.dispose();
});

View File

@@ -13,6 +13,7 @@ import { FoamWorkspace } from '../core/model/workspace';
import { Range } from '../core/model/range';
import { FoamGraph } from '../core/model/graph';
import { OPEN_COMMAND } from './commands/open-resource';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
export const CONFIG_KEY = 'links.hover.enable';
@@ -108,8 +109,36 @@ export class HoverProvider implements vscode.HoverProvider {
: this.workspace.get(targetUri).title;
}
// If placeholder, offer to create a new note from template (compared to default link provider - not from template)
const basedir =
vscode.workspace.workspaceFolders.length > 0
? vscode.workspace.workspaceFolders[0].uri
: vscode.window.activeTextEditor?.document.uri
? vscode.window.activeTextEditor!.document.uri
: undefined;
if (basedir === undefined) {
return;
}
const target = fromVsCodeUri(basedir)
.resolve(targetUri, true)
.changeExtension('', '.md');
const args = {
text: target.getName(),
notePath: target.path,
askForTemplate: true,
};
const command = CREATE_NOTE_COMMAND.asURI(args);
const newNoteFromTemplate = new vscode.MarkdownString(
`[Create note from template for '${targetUri.getName()}'](${command})`
);
newNoteFromTemplate.isTrusted = true;
const hover: vscode.Hover = {
contents: [mdContent, sources.length > 0 ? references : null],
contents: [
mdContent,
sources.length > 0 ? references : null,
targetUri.isPlaceholder() ? newNoteFromTemplate : null,
],
range: toVsCodeRange(targetLink.range),
};
return hover;

View File

@@ -1,34 +1,26 @@
import { FoamFeature } from '../types';
import * as commands from './commands';
import dataviz from './dataviz';
import * as panels from './panels';
import dateSnippets from './date-snippets';
import tagsExplorer from './tags-tree-view';
import orphans from './orphans';
import placeholders from './placeholders';
import backlinks from './backlinks';
import hoverProvider from './hover-provider';
import previewNavigation from './preview';
import preview from './preview';
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 refactor from './refactor';
import { FoamFeature } from '../types';
export const features: FoamFeature[] = [
...Object.values(commands),
...Object.values(panels),
refactor,
navigationProviders,
wikilinkDiagnostics,
tagsExplorer,
dataviz,
dateSnippets,
orphans,
placeholders,
backlinks,
hoverProvider,
linkDecorations,
previewNavigation,
preview,
completionProvider,
tagCompletionProvider,
completionCursorMove,

View File

@@ -7,10 +7,11 @@ import {
closeEditors,
createFile,
showInEditor,
withModifiedFoamConfiguration,
} from '../test/test-utils-vscode';
import { fromVsCodeUri } from '../utils/vsc-utils';
import {
CompletionProvider,
WikilinkCompletionProvider,
SectionCompletionProvider,
} from './link-completion';
@@ -63,7 +64,7 @@ describe('Link Completion', () => {
it('should not return any link for empty documents', async () => {
const { uri } = await createFile('');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const provider = new WikilinkCompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
@@ -76,7 +77,7 @@ describe('Link Completion', () => {
it('should not return link outside the wikilink brackets', async () => {
const { uri } = await createFile('[[file]] then');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const provider = new WikilinkCompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
@@ -90,7 +91,7 @@ describe('Link Completion', () => {
for (const text of ['[[', '[[file]] [[', '[[file]] #tag [[']) {
const { uri } = await createFile(text);
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const provider = new WikilinkCompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
@@ -110,6 +111,103 @@ describe('Link Completion', () => {
}
});
it('should support label setting', async () => {
const { uri: noteUri, content } = await createFile(`# My Note Title`);
const workspace = createTestWorkspace();
workspace.set(parser.parse(noteUri, content));
const provider = new WikilinkCompletionProvider(
workspace,
FoamGraph.fromWorkspace(workspace)
);
const { uri } = await createFile('[[');
const { doc } = await showInEditor(uri);
await withModifiedFoamConfiguration(
'completion.label',
'title',
async () => {
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 3)
);
expect(links.items.map(i => i.label)).toEqual(['My Note Title']);
}
);
await withModifiedFoamConfiguration(
'completion.label',
'path',
async () => {
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 3)
);
expect(links.items.map(i => i.label)).toEqual([noteUri.getBasename()]);
}
);
await withModifiedFoamConfiguration(
'completion.label',
'identifier',
async () => {
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 3)
);
expect(links.items.map(i => i.label)).toEqual([
workspace.getIdentifier(noteUri),
]);
}
);
});
it('should support alias setting', async () => {
const { uri: noteUri, content } = await createFile(`# My Note Title`);
const workspace = createTestWorkspace();
workspace.set(parser.parse(noteUri, content));
const provider = new WikilinkCompletionProvider(
workspace,
FoamGraph.fromWorkspace(workspace)
);
const { uri } = await createFile('[[');
const { doc } = await showInEditor(uri);
await withModifiedFoamConfiguration(
'completion.useAlias',
'never',
async () => {
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 3)
);
expect(links.items.map(i => i.insertText)).toEqual([
workspace.getIdentifier(noteUri),
]);
}
);
await withModifiedFoamConfiguration(
'completion.useAlias',
'whenPathDiffersFromTitle',
async () => {
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 3)
);
expect(links.items.map(i => i.insertText)).toEqual([
`${workspace.getIdentifier(noteUri)}|My Note Title`,
]);
}
);
});
it('should return sections for other notes', async () => {
for (const text of [
'[[file-name#',
@@ -171,7 +269,7 @@ alias: alias-a
ws.set(parser.parse(uri, content));
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const provider = new WikilinkCompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,

View File

@@ -1,8 +1,10 @@
import * as vscode from 'vscode';
import { Foam } from '../core/model/foam';
import { FoamGraph } from '../core/model/graph';
import { Resource } from '../core/model/note';
import { URI } from '../core/model/uri';
import { FoamWorkspace } from '../core/model/workspace';
import { getFoamVsCodeConfig } from '../services/config';
import { FoamFeature } from '../types';
import { getNoteTooltip, mdDocSelector } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
@@ -28,7 +30,7 @@ const feature: FoamFeature = {
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
mdDocSelector,
new CompletionProvider(foam.workspace, foam.graph),
new WikilinkCompletionProvider(foam.workspace, foam.graph),
'['
),
vscode.languages.registerCompletionItemProvider(
@@ -159,7 +161,7 @@ export class SectionCompletionProvider
}
}
export class CompletionProvider
export class WikilinkCompletionProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
@@ -186,20 +188,45 @@ export class CompletionProvider
position.line,
position.character
);
const labelStyle = getCompletionLabelSetting();
const aliasSetting = getCompletionAliasSetting();
const resources = this.ws.list().map(resource => {
const label = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
const resourceIsDocument =
['attachment', 'image'].indexOf(resource.type) === -1;
const identifier = this.ws.getIdentifier(resource.uri);
const label = !resourceIsDocument
? identifier
: labelStyle === 'path'
? vscode.workspace.asRelativePath(toVsCodeUri(resource.uri))
: labelStyle === 'title'
? resource.title
: identifier;
const item = new ResourceCompletionItem(
label,
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.detail = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
item.sortText = resourceIsDocument
? `0-${item.label}`
: `1-${item.label}`;
const useAlias =
resourceIsDocument &&
aliasSetting !== 'never' &&
wikilinkRequiresAlias(resource);
item.insertText = useAlias
? `${identifier}|${resource.title}`
: identifier;
item.commitCharacters = useAlias ? [] : linkCommitCharacters;
item.range = replacementRange;
item.command = COMPLETION_CURSOR_MOVE;
item.commitCharacters = linkCommitCharacters;
return item;
});
const aliases = this.ws.list().flatMap(resource =>
@@ -265,4 +292,23 @@ class ResourceCompletionItem extends vscode.CompletionItem {
}
}
function getCompletionLabelSetting() {
const labelStyle: 'path' | 'title' | 'identifier' = getFoamVsCodeConfig(
'completion.label'
);
return labelStyle;
}
function getCompletionAliasSetting() {
const aliasStyle: 'never' | 'whenPathDiffersFromTitle' = getFoamVsCodeConfig(
'completion.useAlias'
);
return aliasStyle;
}
const normalize = (text: string) => text.toLocaleLowerCase().trim();
function wikilinkRequiresAlias(resource: Resource) {
return normalize(resource.uri.getName()) !== normalize(resource.title);
}
export default feature;

View File

@@ -54,7 +54,7 @@ describe('Document navigation', () => {
expect(links.length).toEqual(0);
});
it('should create links for wikilinks', async () => {
it('should not create links for wikilinks, as this is managed by the definition provider', async () => {
const fileA = await createFile('# File A', ['file-a.md']);
const fileB = await createFile(`this is a link to [[${fileA.name}]].`);
const ws = createTestWorkspace()
@@ -66,9 +66,7 @@ describe('Document navigation', () => {
const provider = new NavigationProvider(ws, graph, parser);
const links = provider.provideDocumentLinks(doc);
expect(links.length).toEqual(1);
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileA.uri));
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 26));
expect(links.length).toEqual(0);
});
it('should create links for placeholders', async () => {

View File

@@ -159,22 +159,23 @@ export class NavigationProvider
})
);
return targets.map(o => {
const command = OPEN_COMMAND.asURI(o.target);
const documentLink = new vscode.DocumentLink(
new vscode.Range(
o.link.range.start.line,
o.link.range.start.character + 2,
o.link.range.end.line,
o.link.range.end.character - 2
),
command
);
documentLink.tooltip = o.target.isPlaceholder()
? `Create note for '${o.target.path}'`
: `Go to ${o.target.toFsPath()}`;
return documentLink;
});
return targets
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
.map(o => {
const command = OPEN_COMMAND.asURI(o.target);
const documentLink = new vscode.DocumentLink(
new vscode.Range(
o.link.range.start.line,
o.link.range.start.character + 2,
o.link.range.end.line,
o.link.range.end.character - 2
),
command
);
documentLink.tooltip = `Create note for '${o.target.path}'`;
return documentLink;
});
}
}

View File

@@ -1,32 +0,0 @@
import { FoamGraph } from '../core/model/graph';
import { createTestNote, createTestWorkspace } from '../test/test-utils';
import { isOrphan } from './orphans';
const orphanA = createTestNote({
uri: '/path/orphan-a.md',
title: 'Orphan A',
});
const nonOrphan1 = createTestNote({
uri: '/path/non-orphan-1.md',
});
const nonOrphan2 = createTestNote({
uri: '/path/non-orphan-2.md',
links: [{ slug: 'non-orphan-1' }],
});
const workspace = createTestWorkspace()
.set(orphanA)
.set(nonOrphan1)
.set(nonOrphan2);
const graph = FoamGraph.fromWorkspace(workspace);
describe('isOrphan', () => {
it('should return true when a note with no connections is provided', () => {
expect(isOrphan(orphanA.uri, graph)).toBeTruthy();
});
it('should return false when a note with connections is provided', () => {
expect(isOrphan(nonOrphan1.uri, graph)).toBeFalsy();
});
});

View File

@@ -1,51 +0,0 @@
import * as vscode from 'vscode';
import { Foam } from '../core/model/foam';
import { FoamGraph } from '../core/model/graph';
import { URI } from '../core/model/uri';
import { getOrphansConfig } from '../settings';
import { FoamFeature } from '../types';
import {
GroupedResourcesTreeDataProvider,
ResourceTreeItem,
UriTreeItem,
} from '../utils/grouped-resources-tree-data-provider';
import { fromVsCodeUri } from '../utils/vsc-utils';
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
fromVsCodeUri(dir.uri)
);
const provider = new GroupedResourcesTreeDataProvider(
'orphans',
'orphan',
getOrphansConfig(),
workspacesURIs,
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
uri => {
if (uri.isPlaceholder()) {
return new UriTreeItem(uri);
}
const resource = foam.workspace.find(uri);
return new ResourceTreeItem(resource, foam.workspace);
}
);
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
...provider.commands,
foam.graph.onDidUpdate(() => provider.refresh())
);
},
};
export const isOrphan = (uri: URI, graph: FoamGraph) =>
graph.getConnections(uri).length === 0;
export default feature;

View File

@@ -1,17 +1,17 @@
import { workspace, window } from 'vscode';
import { createTestNote, createTestWorkspace } from '../test/test-utils';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
import {
cleanWorkspace,
closeEditors,
createNote,
getUriInWorkspace,
} from '../test/test-utils-vscode';
} from '../../test/test-utils-vscode';
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
import { OPEN_COMMAND } from './commands/open-resource';
import { toVsCodeUri } from '../utils/vsc-utils';
import { FoamGraph } from '../core/model/graph';
import { URI } from '../core/model/uri';
import { ResourceTreeItem } from '../../utils/grouped-resources-tree-data-provider';
import { OPEN_COMMAND } from '../commands/open-resource';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { FoamGraph } from '../../core/model/graph';
import { URI } from '../../core/model/uri';
describe('Backlinks panel', () => {
beforeAll(async () => {

View File

@@ -1,16 +1,16 @@
import * as vscode from 'vscode';
import { groupBy } from 'lodash';
import { URI } from '../core/model/uri';
import { URI } from '../../core/model/uri';
import { getNoteTooltip, isNone } from '../utils';
import { FoamFeature } from '../types';
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
import { Foam } from '../core/model/foam';
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, toVsCodeUri } from '../utils/vsc-utils';
import { getNoteTooltip, isNone } from '../../utils';
import { FoamFeature } from '../../types';
import { ResourceTreeItem } from '../../utils/grouped-resources-tree-data-provider';
import { Foam } from '../../core/model/foam';
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, toVsCodeUri } from '../../utils/vsc-utils';
const feature: FoamFeature = {
activate: async (

View File

@@ -1,11 +1,11 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../types';
import { FoamFeature } from '../../types';
import { TextDecoder } from 'util';
import { getGraphStyle, getTitleMaxLength } from '../settings';
import { isSome } from '../utils';
import { Foam } from '../core/model/foam';
import { Logger } from '../core/utils/log';
import { fromVsCodeUri } from '../utils/vsc-utils';
import { getGraphStyle, getTitleMaxLength } from '../../settings';
import { isSome } from '../../utils';
import { Foam } from '../../core/model/foam';
import { Logger } from '../../core/utils/log';
import { fromVsCodeUri } from '../../utils/vsc-utils';
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
@@ -141,10 +141,11 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
const selectedNote = foam.workspace.get(fromVsCodeUri(noteUri));
if (isSome(selectedNote)) {
const doc = await vscode.workspace.openTextDocument(
selectedNote.uri.path // vscode doesn't recognize the URI directly
vscode.commands.executeCommand(
'vscode.open',
noteUri,
vscode.ViewColumn.One
);
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
}
break;
}

View File

@@ -0,0 +1,5 @@
export { default as backlinks } from './backlinks';
export { default as dataviz } from './dataviz';
export { default as orphans } from './orphans';
export { default as placeholders } from './placeholders';
export { default as tags } from './tags-explorer';

View File

@@ -0,0 +1,61 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { createMatcherAndDataStore } from '../../services/editor';
import { getOrphansConfig } from '../../settings';
import { FoamFeature } from '../../types';
import {
GroupedResourcesTreeDataProvider,
ResourceTreeItem,
UriTreeItem,
} from '../../utils/grouped-resources-tree-data-provider';
const EXCLUDE_TYPES = ['image', 'attachment'];
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const { matcher } = await createMatcherAndDataStore(
getOrphansConfig().exclude
);
const provider = new GroupedResourcesTreeDataProvider(
'orphans',
'orphan',
() =>
foam.graph
.getAllNodes()
.filter(
uri =>
!EXCLUDE_TYPES.includes(foam.workspace.find(uri)?.type) &&
foam.graph.getConnections(uri).length === 0
),
uri => {
return uri.isPlaceholder()
? new UriTreeItem(uri)
: new ResourceTreeItem(foam.workspace.find(uri), foam.workspace);
},
matcher
);
provider.setGroupBy(getOrphansConfig().groupBy);
const treeView = vscode.window.createTreeView('foam-vscode.orphans', {
treeDataProvider: provider,
showCollapseAll: true,
});
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${provider.numElements})`;
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
...provider.commands,
foam.graph.onDidUpdate(() => {
treeView.title = baseTitle + ` (${provider.numElements})`;
provider.refresh();
})
);
},
};
export default feature;

View File

@@ -0,0 +1,49 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { createMatcherAndDataStore } from '../../services/editor';
import { getPlaceholdersConfig } from '../../settings';
import { FoamFeature } from '../../types';
import {
GroupedResourcesTreeDataProvider,
UriTreeItem,
} from '../../utils/grouped-resources-tree-data-provider';
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const { matcher } = await createMatcherAndDataStore(
getPlaceholdersConfig().exclude
);
const provider = new GroupedResourcesTreeDataProvider(
'placeholders',
'placeholder',
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
uri => {
return new UriTreeItem(uri);
},
matcher
);
provider.setGroupBy(getPlaceholdersConfig().groupBy);
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
treeDataProvider: provider,
showCollapseAll: true,
});
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${provider.numElements})`;
context.subscriptions.push(
treeView,
...provider.commands,
foam.graph.onDidUpdate(() => {
treeView.title = baseTitle + ` (${provider.numElements})`;
provider.refresh();
})
);
},
};
export default feature;

View File

@@ -1,19 +1,24 @@
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';
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
import { FileDataStore, Matcher } from '../core/services/datastore';
import { createMarkdownParser } from '../core/services/markdown-parser';
import {
createTestNote,
readFileFromFs,
TEST_DATA_DIR,
} from '../../test/test-utils';
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
import { TagItem, TagReference, TagsProvider } from './tags-explorer';
import { bootstrap, Foam } from '../../core/model/foam';
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
import { createMarkdownParser } from '../../core/services/markdown-parser';
import { URI } from '../../core/model/uri';
import { FileDataStore, Matcher } from '../../test/test-datastore';
describe('Tags tree panel', () => {
let _foam: Foam;
let provider: TagsProvider;
const dataStore = new FileDataStore(readFileFromFs);
const matcher = new Matcher([]);
const dataStore = new FileDataStore(readFileFromFs, TEST_DATA_DIR.toFsPath());
const matcher = new Matcher([URI.file(TEST_DATA_DIR.toFsPath())]);
const parser = createMarkdownParser();
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
beforeAll(async () => {
await cleanWorkspace();
@@ -25,7 +30,9 @@ describe('Tags tree panel', () => {
});
beforeEach(async () => {
_foam = await bootstrap(matcher, dataStore, parser, [mdProvider]);
_foam = await bootstrap(matcher, undefined, dataStore, parser, [
mdProvider,
]);
provider = new TagsProvider(_foam, _foam.workspace);
await closeEditors();
});

View File

@@ -1,11 +1,11 @@
import { URI } from '../core/model/uri';
import { URI } from '../../core/model/uri';
import * as vscode from 'vscode';
import { FoamFeature } from '../types';
import { getNoteTooltip, isSome } from '../utils';
import { toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
import { Foam } from '../core/model/foam';
import { FoamWorkspace } from '../core/model/workspace';
import { Resource, Tag } from '../core/model/note';
import { FoamFeature } from '../../types';
import { getNoteTooltip, isSome } from '../../utils';
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import { Resource, Tag } from '../../core/model/note';
const TAG_SEPARATOR = '/';
const feature: FoamFeature = {
@@ -15,11 +15,18 @@ const feature: FoamFeature = {
) => {
const foam = await foamPromise;
const provider = new TagsProvider(foam, foam.workspace);
const treeView = vscode.window.createTreeView('foam-vscode.tags-explorer', {
treeDataProvider: provider,
showCollapseAll: true,
});
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
'foam-vscode.tags-explorer',
provider
)
treeView,
foam.tags.onDidUpdate(() => {
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
})
);
foam.tags.onDidUpdate(() => provider.refresh());
},
@@ -29,9 +36,12 @@ export default feature;
export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
// prettier-ignore
private _onDidChangeTreeData: vscode.EventEmitter<TagTreeItem | undefined | void> = new vscode.EventEmitter<TagTreeItem | undefined | void>();
private _onDidChangeTreeData: vscode.EventEmitter<
TagTreeItem | undefined | void
> = new vscode.EventEmitter<TagTreeItem | undefined | void>();
// prettier-ignore
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> = this._onDidChangeTreeData.event;
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> =
this._onDidChangeTreeData.event;
private tags: {
tag: string;

View File

@@ -1,42 +0,0 @@
import * as vscode from 'vscode';
import { Foam } from '../core/model/foam';
import { getPlaceholdersConfig } from '../settings';
import { FoamFeature } from '../types';
import {
GroupedResourcesTreeDataProvider,
UriTreeItem,
} from '../utils/grouped-resources-tree-data-provider';
import { fromVsCodeUri } from '../utils/vsc-utils';
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
fromVsCodeUri(dir.uri)
);
const provider = new GroupedResourcesTreeDataProvider(
'placeholders',
'placeholder',
getPlaceholdersConfig(),
workspacesURIs,
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
uri => {
return new UriTreeItem(uri);
}
);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
'foam-vscode.placeholders',
provider
),
...provider.commands,
foam.graph.onDidUpdate(() => provider.refresh())
);
},
};
export default feature;

View File

@@ -1,10 +1,10 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { Foam } from '../../core/model/foam';
import markdownItFoamTags from './tag-highlight';
import markdownItWikilinkNavigation from './wikilink-navigation';
import markdownItRemoveLinkReferences from './remove-wikilink-references';
import markdownItWikilinkEmbed from './wikilink-embed';
import { default as markdownItFoamTags } from './tag-highlight';
import { default as markdownItWikilinkNavigation } from './wikilink-navigation';
import { default as markdownItRemoveLinkReferences } from './remove-wikilink-references';
import { default as markdownItWikilinkEmbed } from './wikilink-embed';
const feature: FoamFeature = {
activate: async (
@@ -16,9 +16,9 @@ const feature: FoamFeature = {
return {
extendMarkdownIt: (md: markdownit) => {
return [
markdownItWikilinkEmbed,
markdownItFoamTags,
markdownItWikilinkNavigation,
markdownItWikilinkEmbed,
markdownItRemoveLinkReferences,
].reduce((acc, extension) => extension(acc, foam.workspace), md);
},

View File

@@ -1,6 +1,6 @@
import MarkdownIt from 'markdown-it';
import { FoamWorkspace } from '../../core/model/workspace';
import markdownItFoamTags from './tag-highlight';
import { default as markdownItFoamTags } from './tag-highlight';
describe('Stylable tag generation in preview', () => {
const md = markdownItFoamTags(MarkdownIt(), new FoamWorkspace());

View File

@@ -6,7 +6,8 @@ import {
deleteFile,
withModifiedFoamConfiguration,
} from '../../test/test-utils-vscode';
import markdownItWikilinkEmbed, {
import {
default as markdownItWikilinkEmbed,
CONFIG_EMBED_NOTE_IN_CONTAINER,
} from './wikilink-embed';

View File

@@ -1,9 +1,7 @@
import markdownItRegex from 'markdown-it-regex';
import * as vscode from 'vscode';
import { isSome } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';
import { Logger } from '../../core/utils/log';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { Resource } from '../../core/model/note';
import { getFoamVsCodeConfig } from '../../services/config';
// eslint-disable-next-line no-restricted-imports
@@ -17,7 +15,7 @@ export const markdownItWikilinkEmbed = (
workspace: FoamWorkspace
) => {
return md.use(markdownItRegex, {
name: 'include-notes',
name: 'embed-wikilinks',
regex: /!\[\[([^[\]]+?)\]\]/,
replace: (wikilink: string) => {
try {
@@ -58,9 +56,7 @@ Embed for attachments is not supported
break;
case 'image':
content = `<div class="embed-container-image">${md.render(
`![](${vscode.workspace.asRelativePath(
toVsCodeUri(includedNote.uri)
)})`
`![](${md.normalizeLink(includedNote.uri.path)})`
)}</div>`;
break;
}

View File

@@ -1,12 +1,9 @@
import MarkdownIt from 'markdown-it';
import { createMarkdownParser } from '../../core/services/markdown-parser';
import { FoamWorkspace } from '../../core/model/workspace';
import { createTestNote } from '../../test/test-utils';
import { getUriInWorkspace } from '../../test/test-utils-vscode';
import markdownItWikilinkNavigation from './wikilink-navigation';
import markdownItRemoveLinkReferences from './remove-wikilink-references';
const parser = createMarkdownParser();
import { default as markdownItWikilinkNavigation } from './wikilink-navigation';
import { default as markdownItRemoveLinkReferences } from './remove-wikilink-references';
describe('Link generation in preview', () => {
const noteA = createTestNote({

View File

@@ -1,18 +1,12 @@
import markdownItRegex from 'markdown-it-regex';
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { isNone, isSome } from '../../utils';
import { Foam } from '../../core/model/foam';
import { isNone } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';
import { Logger } from '../../core/utils/log';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { Resource } from '../../core/model/note';
import { MarkdownLink } from '../../core/services/markdown-link';
import { Range } from '../../core/model/range';
import { isEmpty } from 'lodash';
import { getFoamVsCodeConfig } from '../../services/config';
// eslint-disable-next-line no-restricted-imports
import { readFileSync } from 'fs';
export const markdownItWikilinkNavigation = (
md: markdownit,
@@ -20,7 +14,7 @@ export const markdownItWikilinkNavigation = (
) => {
return md.use(markdownItRegex, {
name: 'connect-wikilinks',
regex: /\[\[([^[\]]+?)\]\]/,
regex: /(?=[^!])\[\[([^[\]]+?)\]\]/,
replace: (wikilink: string) => {
try {
const { target, alias } = MarkdownLink.analyzeLink({

View File

@@ -5,7 +5,12 @@ import {
createFile,
showInEditor,
} from '../test/test-utils-vscode';
import { getCurrentEditorDirectory, replaceSelection } from './editor';
import {
asAbsoluteWorkspaceUri,
getCurrentEditorDirectory,
replaceSelection,
} from './editor';
import { URI } from '../core/model/uri';
describe('Editor utils', () => {
beforeAll(closeEditors);
@@ -38,4 +43,14 @@ describe('Editor utils', () => {
expect(doc.doc.getText()).toEqual('This was the file A');
});
});
describe('asAbsoluteWorkspaceUri', () => {
it('should work with the VS Code workspace folders if none are passed', () => {
const uri = URI.file('relative/path');
const workspaceFolder = workspace.workspaceFolders[0];
expect(asAbsoluteWorkspaceUri(uri)).toEqual(
fromVsCodeUri(workspaceFolder.uri).joinPath(uri.path)
);
});
});
});

View File

@@ -1,9 +1,13 @@
import { URI } from '../core/model/uri';
import { isEmpty } from 'lodash';
import { asAbsoluteUri, URI } from '../core/model/uri';
import { TextEncoder } from 'util';
import {
FileType,
RelativePattern,
Selection,
SnippetString,
TextDocument,
Uri,
ViewColumn,
window,
workspace,
@@ -12,6 +16,13 @@ import {
import { focusNote } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { isSome } from '../core/utils';
import {
AlwaysIncludeMatcher,
FileListBasedMatcher,
GenericDataStore,
IDataStore,
IMatcher,
} from '../core/services/datastore';
interface SelectionInfo {
document: TextDocument;
@@ -85,3 +96,92 @@ export function getCurrentEditorDirectory(): URI {
throw new Error('A file must be open in editor, or workspace folder needed');
}
export 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;
}
}
export async function readFile(uri: URI): Promise<string | undefined> {
if (await fileExists(uri)) {
return workspace.fs
.readFile(toVsCodeUri(uri))
.then(bytes => bytes.toString());
}
return undefined;
}
export const deleteFile = (uri: URI) => {
return workspace.fs.delete(toVsCodeUri(uri), { recursive: true });
};
/**
* Turns a relative URI into an absolute URI for the given workspace.
* @param uri the uri to evaluate
* @returns an absolute uri
*/
export function asAbsoluteWorkspaceUri(uri: URI): URI {
if (workspace.workspaceFolders === undefined) {
throw new Error('An open folder or workspace is required');
}
const folders = workspace.workspaceFolders.map(folder =>
fromVsCodeUri(folder.uri)
);
const res = asAbsoluteUri(uri, folders);
return res;
}
export const createMatcherAndDataStore = async (
excludes: string[]
): Promise<{
matcher: IMatcher;
dataStore: IDataStore;
excludePatterns: Map<string, string[]>;
}> => {
const excludePatterns = new Map<string, string[]>();
workspace.workspaceFolders.forEach(f => excludePatterns.set(f.name, []));
for (const exclude of excludes) {
const tokens = exclude.split('/');
const matchesFolder = workspace.workspaceFolders.find(
f => f.name === tokens[0]
);
if (matchesFolder) {
excludePatterns.get(tokens[0]).push(tokens.slice(1).join('/'));
} else {
for (const [, value] of excludePatterns.entries()) {
value.push(exclude);
}
}
}
const listFiles = async () => {
let files: Uri[] = [];
for (const folder of workspace.workspaceFolders) {
const uris = await workspace.findFiles(
new RelativePattern(folder.uri.path, '**/*'),
new RelativePattern(
folder.uri.path,
`{${excludePatterns.get(folder.name).join(',')}}`
)
);
files = [...files, ...uris];
}
return files.map(fromVsCodeUri);
};
const readFile = async (uri: URI) =>
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
const dataStore = new GenericDataStore(listFiles, readFile);
const matcher = isEmpty(excludes)
? new AlwaysIncludeMatcher()
: await FileListBasedMatcher.createFromListFn(listFiles);
return { matcher, dataStore, excludePatterns };
};

View File

@@ -1,7 +1,6 @@
import { Selection, ViewColumn, window, workspace } from 'vscode';
import { isWindows } from '../core/common/platform';
import { Selection, ViewColumn, window } from 'vscode';
import { fromVsCodeUri } from '../utils/vsc-utils';
import { determineNewNoteFilepath, NoteFactory } from '../services/templates';
import { NoteFactory } from '../services/templates';
import {
closeEditors,
createFile,
@@ -10,6 +9,7 @@ import {
showInEditor,
} from '../test/test-utils-vscode';
import { Resolver } from './variable-resolver';
import { fileExists } from './editor';
describe('Create note from template', () => {
beforeEach(async () => {
@@ -113,27 +113,6 @@ foam_template: # foam template metadata
});
describe('Creation with active text selection', () => {
it('should populate FOAM_SELECTED_TEXT with the current selection', async () => {
const templateA = await createFile('Template A', [
'.foam',
'templates',
'template-a.md',
]);
const file = await createFile('Content of first file');
const { editor } = await showInEditor(file.uri);
editor.selection = new Selection(0, 11, 1, 0);
const target = getUriInWorkspace();
const resolver = new Resolver(new Map(), new Date());
await NoteFactory.createFromTemplate(templateA.uri, resolver, target);
expect(await resolver.resolveFromName('FOAM_SELECTED_TEXT')).toEqual(
'first file'
);
await deleteFile(templateA);
await deleteFile(target);
await deleteFile(file);
});
it('should open created note in a new column if there was a selection', async () => {
const templateA = await createFile('Template A', [
'.foam',
@@ -183,91 +162,69 @@ foam_template: # foam template metadata
expect(window.visibleTextEditors[0].document.getText()).toEqual(
`This is my first file: [[${target.getName()}]]`
);
await deleteFile(template.uri);
});
});
});
describe('determineNewNoteFilepath', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
describe('NoteFactory.createNote', () => {
beforeEach(async () => {
await closeEditors();
});
it('should use the template path if absolute', async () => {
const winAbsolutePath = 'C:\\absolute_path\\journal\\My Note Title.md';
const linuxAbsolutePath = '/absolute_path/journal/My Note Title.md';
const winResult = await determineNewNoteFilepath(
winAbsolutePath,
undefined,
it('should create a new note', async () => {
const target = getUriInWorkspace();
await NoteFactory.createNote(
target,
'Hello World',
new Resolver(new Map(), new Date())
);
expect(winResult.toFsPath()).toMatch(winAbsolutePath);
const linuxResult = await determineNewNoteFilepath(
linuxAbsolutePath,
undefined,
new Resolver(new Map(), new Date())
);
expect(linuxResult.toFsPath()).toMatch(linuxAbsolutePath);
expect(await fileExists(target)).toBeTruthy();
expect(window.activeTextEditor.document.getText()).toEqual('Hello World');
await deleteFile(target);
});
it('should compute the relative template filepath from the current directory', async () => {
const relativePath = isWindows
? 'journal\\My Note Title.md'
: 'journal/My Note Title.md';
const resultFilepath = await determineNewNoteFilepath(
relativePath,
it('should support not replacing the selection with a link to the newly created note', async () => {
const file = await createFile('This is my first file: World');
const { editor } = await showInEditor(file.uri);
editor.selection = new Selection(0, 23, 0, 28);
const target = getUriInWorkspace();
await NoteFactory.createNote(
target,
'Hello ${FOAM_SELECTED_TEXT} ${FOAM_SELECTED_TEXT}', // eslint-disable-line no-template-curly-in-string
new Resolver(new Map(), new Date()),
undefined,
new Resolver(new Map(), new Date())
false
);
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(relativePath);
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
expect(window.activeTextEditor.document.getText()).toEqual(
'Hello World World'
);
expect(window.visibleTextEditors[0].document.getText()).toEqual(
`This is my first file: World`
);
await deleteFile(file.uri);
await deleteFile(target);
});
it('should use the note title if nothing else is available', async () => {
const noteTitle = 'My new note';
const resultFilepath = await determineNewNoteFilepath(
it('should support replacing the selection with a link to the newly created note', async () => {
const file = await createFile('This is my first file: World');
const { editor } = await showInEditor(file.uri);
editor.selection = new Selection(0, 23, 0, 28);
const target = getUriInWorkspace();
await NoteFactory.createNote(
target,
'Hello ${FOAM_SELECTED_TEXT} ${FOAM_SELECTED_TEXT}', // eslint-disable-line no-template-curly-in-string
new Resolver(new Map(), new Date()),
undefined,
undefined,
new Resolver(new Map().set('FOAM_TITLE', noteTitle), new Date())
true
);
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(`${noteTitle}.md`);
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
});
it('should ask the user for a note title if nothing else is available', async () => {
const noteTitle = 'My new note';
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve(noteTitle)));
const resultFilepath = await determineNewNoteFilepath(
undefined,
undefined,
new Resolver(new Map(), new Date())
expect(window.activeTextEditor.document.getText()).toEqual(
'Hello World World'
);
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(`${noteTitle}.md`);
expect(spy).toHaveBeenCalled();
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
});
it('should filter invalid chars from the title #1042', async () => {
const noteTitle = 'My new note/';
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve(noteTitle)));
const resultFilepath = await determineNewNoteFilepath(
undefined,
undefined,
new Resolver(new Map(), new Date())
expect(window.visibleTextEditors[0].document.getText()).toEqual(
`This is my first file: [[${target.getName()}]]`
);
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(`My new note.md`);
expect(spy).toHaveBeenCalled();
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
await deleteFile(file.uri);
await deleteFile(target);
});
});

View File

@@ -1,35 +1,51 @@
import { URI } from '../core/model/uri';
import { TextEncoder } from 'util';
import { FileType, SnippetString, ViewColumn, window, workspace } from 'vscode';
import {
SnippetString,
ViewColumn,
QuickPickItem,
commands,
window,
workspace,
} from 'vscode';
import { focusNote } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
import { UserCancelledOperation } from './errors';
import {
asAbsoluteWorkspaceUri,
createDocAndFocus,
deleteFile,
fileExists,
findSelectionContent,
getCurrentEditorDirectory,
readFile,
replaceSelection,
} from './editor';
import { Resolver } from './variable-resolver';
import dateFormat from 'dateformat';
import { isSome } from '../core/utils';
/**
* The templates directory
*/
export const TEMPLATES_DIR = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath('.foam', 'templates');
export const getTemplatesDir = () =>
fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
'.foam',
'templates'
);
/**
* The URI of the default template
*/
export const DEFAULT_TEMPLATE_URI = TEMPLATES_DIR.joinPath('new-note.md');
export const getDefaultTemplateUri = () =>
getTemplatesDir().joinPath('new-note.md');
/**
* The URI of the template for daily notes
*/
export const DAILY_NOTE_TEMPLATE_URI = TEMPLATES_DIR.joinPath('daily-note.md');
export const getDailyNoteTemplateUri = () =>
getTemplatesDir().joinPath('daily-note.md');
const WIKILINK_DEFAULT_TEMPLATE_TEXT = `# $\{1:$FOAM_TITLE}\n\n$0`;
@@ -55,9 +71,7 @@ For a full list of features see [the VS Code snippets page](https://code.visuals
export async function getTemplateMetadata(
templateUri: URI
): Promise<Map<string, string>> {
const contents = await workspace.fs
.readFile(toVsCodeUri(templateUri))
.then(bytes => bytes.toString());
const contents = (await readFile(templateUri)) ?? '';
const [templateMetadata] = extractFoamTemplateFrontmatterMetadata(contents);
return templateMetadata;
}
@@ -74,11 +88,7 @@ export async function getTemplateInfo(
templateFallbackText = '',
resolver: Resolver
) {
const templateText = (await fileExists(templateUri))
? await workspace.fs
.readFile(toVsCodeUri(templateUri))
.then(bytes => bytes.toString())
: templateFallbackText;
const templateText = (await readFile(templateUri)) ?? templateFallbackText;
const templateWithResolvedVariables = await resolver.resolveText(
templateText
@@ -95,7 +105,173 @@ export async function getTemplateInfo(
};
}
export type OnFileExistStrategy =
| 'open'
| 'overwrite'
| 'cancel'
| 'ask'
| ((filePath: URI) => Promise<URI | undefined>);
export async function askUserForTemplate() {
const templates = await getTemplates();
if (templates.length === 0) {
return offerToCreateTemplate();
}
const templatesMetadata = (
await Promise.all(
templates.map(async templateUri => {
const metadata = await getTemplateMetadata(templateUri);
metadata.set('templatePath', templateUri.getBasename());
return metadata;
})
)
).sort(sortTemplatesMetadata);
const items: QuickPickItem[] = await Promise.all(
templatesMetadata.map(metadata => {
const label = metadata.get('name') || metadata.get('templatePath');
const description = metadata.get('name')
? metadata.get('templatePath')
: null;
const detail = metadata.get('description');
const item = {
label: label,
description: description,
detail: detail,
};
Object.keys(item).forEach(key => {
if (!item[key]) {
delete item[key];
}
});
return item;
})
);
const selectedTemplate = await window.showQuickPick(items, {
placeHolder: 'Select a template to use.',
});
if (selectedTemplate === undefined) {
return undefined;
}
const templateFilename =
(selectedTemplate as QuickPickItem).description ||
(selectedTemplate as QuickPickItem).label;
const templateUri = getTemplatesDir().joinPath(templateFilename);
return templateUri;
}
async function offerToCreateTemplate(): Promise<void> {
const response = await window.showQuickPick(['Yes', 'No'], {
placeHolder:
'No templates available. Would you like to create one instead?',
});
if (response === 'Yes') {
commands.executeCommand('foam-vscode.create-new-template');
return;
}
}
function sortTemplatesMetadata(
t1: Map<string, string>,
t2: Map<string, string>
) {
// Sort by name's existence, then name, then path
if (t1.get('name') === undefined && t2.get('name') !== undefined) {
return 1;
}
if (t1.get('name') !== undefined && t2.get('name') === undefined) {
return -1;
}
const pathSortOrder = t1
.get('templatePath')
.localeCompare(t2.get('templatePath'));
if (t1.get('name') === undefined && t2.get('name') === undefined) {
return pathSortOrder;
}
const nameSortOrder = t1.get('name').localeCompare(t2.get('name'));
return nameSortOrder || pathSortOrder;
}
export const NoteFactory = {
createNote: async (
newFilePath: URI,
text: string,
resolver: Resolver,
onFileExists?: OnFileExistStrategy,
replaceSelectionWithLink = true
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
try {
const onFileExistsFn = async (existingFile: URI) => {
if (typeof onFileExists === 'function') {
return onFileExists(existingFile);
}
switch (onFileExists) {
case 'open':
await commands.executeCommand(
'vscode.open',
toVsCodeUri(existingFile)
);
return;
case 'overwrite':
await deleteFile(existingFile);
return existingFile;
case 'cancel':
return undefined;
case 'ask':
default: {
const newProposedPath = await askUserForFilepathConfirmation(
existingFile
);
return newProposedPath && URI.file(newProposedPath);
}
}
};
while (await fileExists(newFilePath)) {
const proposedNewFilepath = await onFileExistsFn(newFilePath);
if (proposedNewFilepath === undefined) {
return { didCreateFile: false, uri: newFilePath };
}
newFilePath = proposedNewFilepath;
}
const expandedText = await resolver.resolveText(text);
const selectedContent = findSelectionContent();
await createDocAndFocus(
new SnippetString(expandedText),
newFilePath,
selectedContent ? ViewColumn.Beside : ViewColumn.Active
);
if (replaceSelectionWithLink && selectedContent !== undefined) {
const newNoteTitle = newFilePath.getName();
await replaceSelection(
selectedContent.document,
selectedContent.selection,
`[[${newNoteTitle}]]`
);
}
return { didCreateFile: true, uri: newFilePath };
} catch (err) {
if (err instanceof UserCancelledOperation) {
return;
}
throw err;
}
},
/**
* Creates a new note using a template.
* @param templateUri the URI of the template to use.
@@ -108,60 +284,29 @@ export const NoteFactory = {
resolver: Resolver,
filepathFallbackURI?: URI,
templateFallbackText = '',
onFileExists?: (filePath: URI) => Promise<string | undefined>
onFileExists?: OnFileExistStrategy
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
try {
onFileExists = onFileExists
? onFileExists
: (existingFile: URI) => {
const filename = existingFile.getBasename();
return askUserForFilepathConfirmation(existingFile, filename);
};
const template = await getTemplateInfo(
templateUri,
templateFallbackText,
resolver
);
const selectedContent = findSelectionContent();
if (selectedContent?.content) {
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content);
}
const templateSnippet = new SnippetString(template.text);
let newFilePath = await determineNewNoteFilepath(
template.metadata.get('filepath'),
filepathFallbackURI,
resolver
const newFilePath = asAbsoluteWorkspaceUri(
template.metadata.has('filepath')
? URI.file(template.metadata.get('filepath'))
: isSome(filepathFallbackURI)
? filepathFallbackURI
: await getPathFromTitle(resolver)
);
while (await fileExists(newFilePath)) {
const proposedNewFilepath = await onFileExists(newFilePath);
if (proposedNewFilepath === undefined) {
return { didCreateFile: false, uri: newFilePath };
}
newFilePath = URI.file(proposedNewFilepath);
}
await createDocAndFocus(
templateSnippet,
return NoteFactory.createNote(
newFilePath,
selectedContent ? ViewColumn.Beside : ViewColumn.Active
template.text,
resolver,
onFileExists
);
if (selectedContent !== undefined) {
const newNoteTitle = newFilePath.getName();
await replaceSelection(
selectedContent.document,
selectedContent.selection,
`[[${newNoteTitle}]]`
);
}
return { didCreateFile: true, uri: newFilePath };
} catch (err) {
if (err instanceof UserCancelledOperation) {
return;
@@ -185,7 +330,7 @@ export const NoteFactory = {
targetDate
);
return NoteFactory.createFromTemplate(
DAILY_NOTE_TEMPLATE_URI,
getDailyNoteTemplateUri(),
resolver,
filepathFallbackURI,
templateFallbackText,
@@ -197,17 +342,24 @@ export const NoteFactory = {
* Creates a new note when following a placeholder wikilink using the default template.
* @param wikilinkPlaceholder the placeholder value from the wikilink. (eg. `[[Hello Joe]]` -> `Hello Joe`)
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
* @param templateURI URI of the template to use. If undefined, use the default template.
*/
createForPlaceholderWikilink: (
createForPlaceholderWikilink: async (
wikilinkPlaceholder: string,
filepathFallbackURI: URI
filepathFallbackURI: URI,
templateURI?: URI
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
const resolver = new Resolver(
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
new Date()
);
if (templateURI === undefined) {
templateURI = getDefaultTemplateUri();
}
return NoteFactory.createFromTemplate(
DEFAULT_TEMPLATE_URI,
templateURI,
resolver,
filepathFallbackURI,
WIKILINK_DEFAULT_TEMPLATE_TEXT
@@ -217,7 +369,7 @@ export const NoteFactory = {
export const createTemplate = async (): Promise<void> => {
const defaultFilename = 'new-template.md';
const defaultTemplate = TEMPLATES_DIR.joinPath(defaultFilename);
const defaultTemplate = getTemplatesDir().joinPath(defaultFilename);
const fsPath = defaultTemplate.toFsPath();
const filename = await window.showInputBox({
prompt: `Enter the filename for the new template`,
@@ -243,14 +395,18 @@ export const createTemplate = async (): Promise<void> => {
};
async function askUserForFilepathConfirmation(
defaultFilepath: URI,
defaultFilename: string
) {
defaultFilepath: URI
): Promise<string | undefined> {
const fsPath = defaultFilepath.toFsPath();
return await window.showInputBox({
const defaultFilename = defaultFilepath.getBasename();
const defaultExtension = defaultFilepath.getExtension();
return window.showInputBox({
prompt: `Enter the filename for the new note`,
value: fsPath,
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
valueSelection: [
fsPath.length - defaultFilename.length,
fsPath.length - defaultExtension.length,
],
validateInput: async value =>
value.trim().length === 0
? 'Please enter a value'
@@ -271,25 +427,14 @@ async function askUserForFilepathConfirmation(
*/
const UNALLOWED_CHARS = '/\\#%&{}<>?*$!\'":@+`|=';
export async function determineNewNoteFilepath(
templateFilepathAttribute: string | undefined,
fallbackURI: URI | undefined,
resolver: Resolver
): Promise<URI> {
if (templateFilepathAttribute) {
let defaultFilepath = URI.file(templateFilepathAttribute);
if (!defaultFilepath.isAbsolute()) {
defaultFilepath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(templateFilepathAttribute);
}
return defaultFilepath;
}
if (fallbackURI) {
return fallbackURI;
}
/**
* Uses the title to generate a file path.
* It sanitizes the title to remove special characters and spaces.
*
* @param resolver the resolver to use
* @returns the string path of the new note
*/
export const getPathFromTitle = async (resolver: Resolver) => {
let defaultName = await resolver.resolveFromName('FOAM_TITLE');
UNALLOWED_CHARS.split('').forEach(char => {
defaultName = defaultName.split(char).join('');
@@ -299,13 +444,4 @@ export async function determineNewNoteFilepath(
`${defaultName}.md`
);
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;
}
}
};

View File

@@ -1,9 +1,14 @@
import { window } from 'vscode';
import { Selection, window } from 'vscode';
import { Resolver } from './variable-resolver';
import { Variable } from '../core/common/snippetParser';
import {
createFile,
deleteFile,
showInEditor,
} from '../test/test-utils-vscode';
describe('substituteFoamVariables', () => {
test('Does nothing if no Foam-specific variables are used', async () => {
describe('variable-resolver, text substitution', () => {
it('should do nothing if no Foam-specific variables are used', async () => {
const input = `
# \${AnotherVariable} <-- Unrelated to Foam
# \${AnotherVariable:default_value} <-- Unrelated to Foam
@@ -31,7 +36,7 @@ describe('substituteFoamVariables', () => {
expect(await resolver.resolveText(input)).toEqual(input);
});
test('Correctly substitutes variables that are substrings of one another', async () => {
it('should correctly substitute variables that are substrings of one another', async () => {
// FOAM_TITLE is a substring of FOAM_TITLE_NON_EXISTENT_VARIABLE
// If we're not careful with how we substitute the values
// we can end up putting the FOAM_TITLE in place FOAM_TITLE_NON_EXISTENT_VARIABLE should be.
@@ -56,8 +61,8 @@ describe('substituteFoamVariables', () => {
});
});
describe('resolveFoamVariables', () => {
test('Does nothing for unknown Foam-specific variables', async () => {
describe('variable-resolver, variable resolution', () => {
it('should do nothing for unknown Foam-specific variables', async () => {
const variables = [new Variable('FOAM_FOO')];
const expected = new Map<string, string>();
@@ -67,7 +72,7 @@ describe('resolveFoamVariables', () => {
expect(await resolver.resolveAll(variables)).toEqual(expected);
});
test('Resolves FOAM_TITLE', async () => {
it('should resolve FOAM_TITLE', async () => {
const foamTitle = 'My note title';
const variables = [new Variable('FOAM_TITLE'), new Variable('FOAM_SLUG')];
@@ -84,7 +89,7 @@ describe('resolveFoamVariables', () => {
expect(await resolver.resolveAll(variables)).toEqual(expected);
});
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
it('should resolve FOAM_TITLE without asking the user when it is provided', async () => {
const foamTitle = 'My note title';
const variables = [new Variable('FOAM_TITLE')];
@@ -97,7 +102,7 @@ describe('resolveFoamVariables', () => {
expect(await resolver.resolveAll(variables)).toEqual(expected);
});
test('Resolves FOAM_DATE_* properties with current day by default', async () => {
it('should resolve FOAM_DATE_* properties with current day by default', async () => {
const variables = [
new Variable('FOAM_DATE_YEAR'),
new Variable('FOAM_DATE_YEAR_SHORT'),
@@ -134,7 +139,7 @@ describe('resolveFoamVariables', () => {
);
});
test('Resolves FOAM_DATE_* properties with given date', async () => {
it('should resolve FOAM_DATE_* properties with given date', async () => {
const targetDate = new Date(2021, 9, 12, 1, 2, 3);
const variables = [
new Variable('FOAM_DATE_YEAR'),
@@ -149,6 +154,7 @@ describe('resolveFoamVariables', () => {
new Variable('FOAM_DATE_MINUTE'),
new Variable('FOAM_DATE_SECOND'),
new Variable('FOAM_DATE_SECONDS_UNIX'),
new Variable('FOAM_DATE_WEEK'),
];
const expected = new Map<string, string>();
@@ -161,6 +167,7 @@ describe('resolveFoamVariables', () => {
expected.set('FOAM_DATE_DAY_NAME', 'Tuesday');
expected.set('FOAM_DATE_DAY_NAME_SHORT', 'Tue');
expected.set('FOAM_DATE_HOUR', '01');
expected.set('FOAM_DATE_WEEK', '41');
expected.set('FOAM_DATE_MINUTE', '02');
expected.set('FOAM_DATE_SECOND', '03');
expected.set(
@@ -173,10 +180,35 @@ describe('resolveFoamVariables', () => {
expect(await resolver.resolveAll(variables)).toEqual(expected);
});
describe('FOAM_DATE_WEEK', () => {
it('should start counting weeks from 1', async () => {
// week number starts from 1, not 0
// the first "partial week" of the year is really the last of the previous
const resolver = new Resolver(
new Map<string, string>(),
new Date(2021, 0, 1, 1, 2, 3)
);
expect(await resolver.resolve(new Variable('FOAM_DATE_WEEK'))).toEqual(
'53'
);
});
it('should pad week number to 2 digits', async () => {
// week number is 2-digit
const resolver = new Resolver(
new Map<string, string>(),
new Date(2021, 0, 7, 1, 2, 3)
);
expect(await resolver.resolve(new Variable('FOAM_DATE_WEEK'))).toEqual(
'01'
);
});
});
});
describe('resolveFoamTemplateVariables', () => {
test('Does nothing for template without Foam-specific variables', async () => {
describe('variable-resolver, resolveText', () => {
it('should do nothing for template without Foam-specific variables', async () => {
const input = `
# \${AnotherVariable} <-- Unrelated to Foam
# \${AnotherVariable:default_value} <-- Unrelated to Foam
@@ -191,7 +223,7 @@ describe('resolveFoamTemplateVariables', () => {
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Does nothing for unknown Foam-specific variables', async () => {
it('should do nothing for unknown Foam-specific variables', async () => {
const input = `
# $FOAM_FOO
# \${FOAM_FOO}
@@ -204,7 +236,18 @@ describe('resolveFoamTemplateVariables', () => {
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in a newline', async () => {
it('should resolve FOAM_SELECTED_TEXT with the editor selection', async () => {
const file = await createFile('Content of note file');
const { editor } = await showInEditor(file.uri);
editor.selection = new Selection(0, 11, 1, 0);
const resolver = new Resolver(new Map(), new Date());
expect(await resolver.resolveFromName('FOAM_SELECTED_TEXT')).toEqual(
'note file'
);
await deleteFile(file);
});
it('should append FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in a newline', async () => {
const foamTitle = 'My note title';
jest
@@ -221,7 +264,7 @@ describe('resolveFoamTemplateVariables', () => {
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in multiple newlines', async () => {
it('should append FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in multiple newlines', async () => {
const foamTitle = 'My note title';
jest
@@ -238,7 +281,7 @@ describe('resolveFoamTemplateVariables', () => {
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Appends FOAM_SELECTED_TEXT without a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template does not end in a newline', async () => {
it('should append FOAM_SELECTED_TEXT without a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template does not end in a newline', async () => {
const foamTitle = 'My note title';
jest
@@ -255,7 +298,7 @@ describe('resolveFoamTemplateVariables', () => {
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Does not append FOAM_SELECTED_TEXT to a template if there is no selected text and is not referenced', async () => {
it('should not append FOAM_SELECTED_TEXT to a template if there is no selected text and is not referenced', async () => {
const foamTitle = 'My note title';
jest

View File

@@ -18,6 +18,7 @@ const knownFoamVariables = new Set([
'FOAM_DATE_MONTH_NAME',
'FOAM_DATE_MONTH_NAME_SHORT',
'FOAM_DATE_DATE',
'FOAM_DATE_WEEK',
'FOAM_DATE_DAY_NAME',
'FOAM_DATE_DAY_NAME_SHORT',
'FOAM_DATE_HOUR',
@@ -165,6 +166,25 @@ export class Resolver implements VariableResolver {
String(this.foamDate.getDate().valueOf()).padStart(2, '0')
);
break;
case 'FOAM_DATE_WEEK': {
// https://en.wikipedia.org/wiki/ISO_8601#Week_dates
const date = new Date(this.foamDate);
// Find Thursday of this week starting on Monday
date.setDate(date.getDate() + 4 - (date.getDay() || 7));
const thursday = date.getTime();
// Find January 1st
date.setMonth(0); // January
date.setDate(1); // 1st
const janFirst = date.getTime();
// Round the amount of days to compensate for daylight saving time
const days = Math.round((thursday - janFirst) / 86400000); // 1 day = 86400000 ms
const weekDay = Math.floor(days / 7) + 1;
value = Promise.resolve(String(weekDay.valueOf()).padStart(2, '0'));
break;
}
case 'FOAM_DATE_DAY_NAME':
value = Promise.resolve(
this.foamDate.toLocaleString('default', { weekday: 'long' })

View File

@@ -0,0 +1,85 @@
import { URI } from '../core/model/uri';
import { Logger } from '../core/utils/log';
import { Matcher, toMatcherPathFormat } from './test-datastore';
import { TEST_DATA_DIR } from './test-utils';
Logger.setLevel('error');
const testFolder = TEST_DATA_DIR.joinPath('test-datastore');
describe('Matcher', () => {
it('generates globs with the base dir provided', () => {
const matcher = new Matcher([testFolder], ['*'], []);
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
expect(matcher.include).toEqual([
toMatcherPathFormat(testFolder.joinPath('*')),
]);
});
it('defaults to including everything and excluding nothing', () => {
const matcher = new Matcher([testFolder]);
expect(matcher.exclude).toEqual([]);
expect(matcher.include).toEqual([
toMatcherPathFormat(testFolder.joinPath('**', '*')),
]);
});
it('supports multiple includes', () => {
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
expect(matcher.exclude).toEqual([]);
expect(matcher.include).toEqual([
toMatcherPathFormat(testFolder.joinPath('g1')),
toMatcherPathFormat(testFolder.joinPath('g2')),
]);
});
it('has a match method to filter strings', () => {
const matcher = new Matcher([testFolder], ['*.md'], []);
const files = [
testFolder.joinPath('file1.md'),
testFolder.joinPath('file2.md'),
testFolder.joinPath('file3.mdx'),
testFolder.joinPath('sub', 'file4.md'),
];
expect(matcher.match(files)).toEqual([
testFolder.joinPath('file1.md'),
testFolder.joinPath('file2.md'),
]);
});
it('has a isMatch method to see whether a file is matched or not', () => {
const matcher = new Matcher([testFolder], ['*.md'], []);
const files = [
testFolder.joinPath('file1.md'),
testFolder.joinPath('file2.md'),
testFolder.joinPath('file3.mdx'),
testFolder.joinPath('sub', 'file4.md'),
];
expect(matcher.isMatch(files[0])).toEqual(true);
expect(matcher.isMatch(files[1])).toEqual(true);
expect(matcher.isMatch(files[2])).toEqual(false);
expect(matcher.isMatch(files[3])).toEqual(false);
});
it('happy path', () => {
const matcher = new Matcher([URI.file('/root/')], ['**/*'], ['**/*.pdf']);
expect(matcher.isMatch(URI.file('/root/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/root/file.pdf'))).toBeFalsy();
expect(matcher.isMatch(URI.file('/root/dir/file.md'))).toBeTruthy();
expect(matcher.isMatch(URI.file('/root/dir/file.pdf'))).toBeFalsy();
});
it('ignores files in the exclude list', () => {
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
const files = [
testFolder.joinPath('file1.md'),
testFolder.joinPath('file2.md'),
testFolder.joinPath('file3.mdx'),
testFolder.joinPath('sub', 'file4.md'),
];
expect(matcher.isMatch(files[0])).toEqual(false);
expect(matcher.isMatch(files[1])).toEqual(true);
expect(matcher.isMatch(files[2])).toEqual(false);
expect(matcher.isMatch(files[3])).toEqual(false);
});
});

View File

@@ -0,0 +1,96 @@
import micromatch from 'micromatch';
import { promisify } from 'util';
import { glob } from 'glob';
import { Logger } from '../core/utils/log';
import { IDataStore, IMatcher } from '../core/services/datastore';
import { URI } from '../core/model/uri';
import { isWindows } from '../core/common/platform';
import { asAbsolutePaths } from '../core/utils/path';
const findAllFiles = promisify(glob);
/**
* File system based data store
*/
export class FileDataStore implements IDataStore {
constructor(
private readFile: (uri: URI) => Promise<string>,
private readonly basedir: string
) {}
async list(): Promise<URI[]> {
const res = await findAllFiles([this.basedir, '**/*'].join('/'));
return res.map(URI.file);
}
async read(uri: URI) {
try {
return await this.readFile(uri);
} catch (e) {
Logger.error(
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
);
return null;
}
}
}
/**
* The matcher requires the path to be in unix format, so if we are in windows
* we convert the fs path on the way in and out
*/
export const toMatcherPathFormat = isWindows
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
: (uri: URI) => uri.toFsPath();
export const toFsPath = isWindows
? (path: string): string => path.replace(/\//g, '\\')
: (path: string): string => path;
export class Matcher implements IMatcher {
public readonly folders: string[];
public readonly include: string[] = [];
public readonly exclude: string[] = [];
constructor(
baseFolders: URI[],
includeGlobs: string[] = ['**/*'],
excludeGlobs: string[] = []
) {
this.folders = baseFolders.map(toMatcherPathFormat);
Logger.info('Workspace folders: ', this.folders);
this.include = includeGlobs.flatMap(glob =>
asAbsolutePaths(glob, this.folders)
);
this.exclude = excludeGlobs.flatMap(glob =>
asAbsolutePaths(glob, this.folders)
);
Logger.info('Glob patterns', {
includeGlobs: this.include,
ignoreGlobs: this.exclude,
});
}
match(files: URI[]) {
const matches = micromatch(
files.map(f => f.toFsPath()),
this.include,
{
ignore: this.exclude,
nocase: true,
format: toFsPath,
}
);
return matches.map(URI.file);
}
isMatch(uri: URI) {
return this.match([uri]).length > 0;
}
refresh(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -14,7 +14,7 @@ Logger.setLevel('error');
export const cleanWorkspace = async () => {
const files = await vscode.workspace.findFiles('**', '{.vscode,.keep}');
await Promise.all(files.map(f => vscode.workspace.fs.delete(f)));
await Promise.all(files.map(f => deleteFile(fromVsCodeUri(f))));
};
export const showInEditor = async (uri: URI) => {
@@ -28,9 +28,15 @@ export const closeEditors = async () => {
await wait(100);
};
export const deleteFile = (file: URI | { uri: URI }) => {
export const deleteFile = async (file: URI | { uri: URI }) => {
const uri = 'uri' in file ? file.uri : file;
return vscode.workspace.fs.delete(toVsCodeUri(uri), { recursive: true });
try {
await vscode.workspace.fs.delete(toVsCodeUri(uri), {
recursive: true,
});
} catch (e) {
// ignore
}
};
/**
@@ -118,3 +124,21 @@ export const withModifiedConfiguration = async (key, value, fn: () => void) => {
*/
export const withModifiedFoamConfiguration = (key, value, fn: () => void) =>
withModifiedConfiguration(`foam.${key}`, value, fn);
/**
* Utility function to check if two URIs are the same.
* It has the goal of supporting Uri and URI, and dealing with
* inconsistencies in the way they are represented (especially the
* drive letter in Windows)
*
* @param actual the actual value
* @param expected the expected value
*/
export const expectSameUri = (
actual: vscode.Uri | URI,
expected: vscode.Uri | URI
) => {
expect(actual.path.toLocaleLowerCase()).toEqual(
expected.path.toLocaleLowerCase()
);
};

View File

@@ -6,7 +6,6 @@ import { Logger } from '../core/utils/log';
import { Range } from '../core/model/range';
import { URI } from '../core/model/uri';
import { FoamWorkspace } from '../core/model/workspace';
import { Matcher } from '../core/services/datastore';
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
import { NoteLinkDefinition, Resource } from '../core/model/note';
import { createMarkdownParser } from '../core/services/markdown-parser';
@@ -32,13 +31,11 @@ export const strToUri = URI.file;
export const createTestWorkspace = () => {
const workspace = new FoamWorkspace();
const matcher = new Matcher([URI.file('/')], ['**/*']);
const parser = createMarkdownParser();
const provider = new MarkdownResourceProvider(
matcher,
{
read: _ => Promise.resolve(''),
list: _ => Promise.resolve([]),
list: () => Promise.resolve([]),
},
parser
);

View File

@@ -8,11 +8,9 @@ import {
workspace,
Selection,
MarkdownString,
version,
ViewColumn,
} from 'vscode';
import matter from 'gray-matter';
import removeMarkdown from 'remove-markdown';
import { toVsCodeUri } from './utils/vsc-utils';
import { Logger } from './core/utils/log';
import { URI } from './core/model/uri';
@@ -179,14 +177,8 @@ export function getContainsTooltip(titles: string[]): string {
* @param note A Foam Note
*/
export function getNoteTooltip(content: string): string {
const STABLE_MARKDOWN_STRING_API_VERSION = '1.52.1';
const strippedContent = stripFrontMatter(stripImages(content));
if (version >= STABLE_MARKDOWN_STRING_API_VERSION) {
return formatMarkdownTooltip(strippedContent) as any;
}
return formatSimpleTooltip(strippedContent);
return formatMarkdownTooltip(strippedContent) as any;
}
export function formatMarkdownTooltip(content: string): MarkdownString {
@@ -200,16 +192,6 @@ export function formatMarkdownTooltip(content: string): MarkdownString {
return md;
}
export function formatSimpleTooltip(content: string): string {
const CHARACTERS_LIMIT = 200;
const flatContent = removeMarkdown(content)
.replace(/\r?\n|\r/g, ' ')
.replace(/\s+/g, ' ');
const extract = flatContent.substr(0, CHARACTERS_LIMIT);
const ellipsis = flatContent.length > CHARACTERS_LIMIT ? '...' : '';
return `${extract}${ellipsis}`;
}
export function getExcerpt(
markdown: string,
maxLines: number

View File

@@ -1,16 +1,19 @@
import { FoamWorkspace } from '../core/model/workspace';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import {
GroupedResoucesConfigGroupBy,
GroupedResourcesConfig,
} from '../settings';
import { createTestNote, strToUri } from '../test/test-utils';
AlwaysIncludeMatcher,
SubstringExcludeMatcher,
} from '../core/services/datastore';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import { GroupedResoucesConfigGroupBy } from '../settings';
import { createTestNote } from '../test/test-utils';
import {
DirectoryTreeItem,
GroupedResourcesTreeDataProvider,
UriTreeItem,
} from './grouped-resources-tree-data-provider';
const testMatcher = new SubstringExcludeMatcher('path-exclude');
describe('GroupedResourcesTreeDataProvider', () => {
const matchingNote1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
const matchingNote2 = createTestNote({
@@ -32,25 +35,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
.set(excludedPathNote)
.set(notMatchingNote);
// Mock config
const config: GroupedResourcesConfig = {
exclude: ['path-exclude/**/*'],
groupBy: GroupedResoucesConfigGroupBy.Folder,
};
it('should return the grouped resources as a folder tree', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
@@ -72,15 +69,16 @@ describe('GroupedResourcesTreeDataProvider', () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const directory = new DirectoryTreeItem(
'/path',
[new UriTreeItem(matchingNote1.uri)],
@@ -98,22 +96,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
});
it('should return the flattened resources', async () => {
const mockConfig = {
...config,
groupBy: GroupedResoucesConfigGroupBy.Off,
};
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
mockConfig,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Off);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
@@ -132,19 +127,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
});
it('should return the grouped resources without exclusion', async () => {
const mockConfig = { ...config, exclude: [] };
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
mockConfig,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
new AlwaysIncludeMatcher()
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
expect.anything(),
@@ -163,15 +158,15 @@ describe('GroupedResourcesTreeDataProvider', () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
description,
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
{

View File

@@ -1,16 +1,13 @@
import * as path from 'path';
import * as vscode from 'vscode';
import micromatch from 'micromatch';
import {
GroupedResourcesConfig,
GroupedResoucesConfigGroupBy,
} from '../settings';
import { GroupedResoucesConfigGroupBy } from '../settings';
import { getContainsTooltip, getNoteTooltip, isSome } from '../utils';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import { toVsCodeUri } from './vsc-utils';
import { URI } from '../core/model/uri';
import { Resource } from '../core/model/note';
import { FoamWorkspace } from '../core/model/workspace';
import { IMatcher } from '../core/services/datastore';
/**
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
@@ -82,13 +79,10 @@ export class GroupedResourcesTreeDataProvider
constructor(
private providerId: string,
private resourceName: string,
config: GroupedResourcesConfig,
workspaceUris: URI[],
private computeResources: () => Array<URI>,
private createTreeItem: (item: URI) => GroupedResourceTreeItem
private createTreeItem: (item: URI) => GroupedResourceTreeItem,
private matcher: IMatcher
) {
this.groupBy = config.groupBy;
this.exclude = this.getGlobs(workspaceUris, config.exclude);
this.setContext();
this.doComputeResources();
}
@@ -106,6 +100,10 @@ export class GroupedResourcesTreeDataProvider
];
}
public get numElements() {
return this.flatUris.length;
}
setGroupBy(groupBy: GroupedResoucesConfigGroupBy): void {
this.groupBy = groupBy;
this.setContext();
@@ -163,30 +161,10 @@ export class GroupedResourcesTreeDataProvider
private doComputeResources(): void {
this.flatUris = this.computeResources()
.filter(uri => !this.isMatch(uri))
.filter(uri => this.matcher.isMatch(uri))
.filter(isSome);
}
private isMatch(uri: URI) {
return micromatch.isMatch(uri.toFsPath(), this.exclude);
}
private getGlobs(fsURI: URI[], globs: string[]): string[] {
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
const exclude: string[] = [];
for (const fsPath of fsURI) {
let folder = fsPath.path.replace(/\\/g, '/');
if (folder.substr(-1) === '/') {
folder = folder.slice(0, -1);
}
exclude.push(...globs.map(g => `${folder}/${g}`));
}
return exclude;
}
private getUrisByDirectory(): UrisByDirectory {
const resourcesByDirectory: UrisByDirectory = {};
for (const uri of this.flatUris) {

View File

@@ -85,6 +85,8 @@ let model = {
style: defaultStyle,
showNodesOfType: {
placeholder: true,
image: false,
attachment: false,
note: true,
tag: true,
},
@@ -235,12 +237,10 @@ function initDataviz(channel) {
Actions.highlightNode(node?.id);
})
.onNodeClick((node, event) => {
if (event.getModifierState('Control') || event.getModifierState('Meta')) {
channel.postMessage({
type: 'webviewDidSelectNode',
payload: node.id,
});
}
channel.postMessage({
type: 'webviewDidSelectNode',
payload: node.id,
});
Actions.selectNode(node.id, event.getModifierState('Shift'));
})
.onBackgroundClick(event => {

View File

@@ -1,41 +0,0 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesnt yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
* Press `F5` to run the tests in a new window with your extension loaded.
* See the output of the test result in the debug console.
* Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).

277
readme.md
View File

@@ -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 -->
[![All Contributors](https://img.shields.io/badge/all_contributors-99-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-108-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Discord Chat](https://img.shields.io/discord/729975036148056075?color=748AD9&label=discord%20chat&style=flat-square)](https://foambubble.github.io/join-discord/g)
@@ -91,7 +91,7 @@ See for each occurrence the context in which it lives, as well as a preview of t
### Tag Explorer Panel
Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/user/user/features/tags).
Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/user/features/tags).
Foam also supports hierarchical tags.
![Tag Explorer Panel](./assets/screenshots/feature-tags-panel.gif)
@@ -186,7 +186,7 @@ You can also browse the [docs folder](https://github.com/foambubble/foam/tree/ma
Foam is licensed under the [MIT license](LICENSE).
[//begin]: # "Autogenerated link references for markdown compatibility"
[Backlinking]: docs/features/backlinking.md "Backlinking"
[Backlinking]: docs/user/features/backlinking.md "Backlinking"
[//end]: # "Autogenerated link references"
## Contributors ✨
@@ -197,135 +197,148 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt=""/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt=""/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
<td align="center"><a href="https://nygaard.site"><img src="https://avatars.githubusercontent.com/u/4606342?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nikhil Nygaard</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=njnygaard" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Pearcekieser"><img src="https://avatars.githubusercontent.com/u/5055971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Pearcekieser</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Pearcekieser" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/theowenyoung"><img src="https://avatars.githubusercontent.com/u/62473795?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Owen Young</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=theowenyoung" title="Documentation">📖</a> <a href="#content-theowenyoung" title="Content">🖋</a></td>
<td align="center"><a href="http://www.prashu.com"><img src="https://avatars.githubusercontent.com/u/476729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Prashanth Subrahmanyam</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ksprashu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/JonasSprenger"><img src="https://avatars.githubusercontent.com/u/25108895?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonas SPRENGER</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=JonasSprenger" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Laptop765"><img src="https://avatars.githubusercontent.com/u/1468359?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Laptop765" title="Documentation">📖</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/memeplex"><img src="https://avatars.githubusercontent.com/u/2845433?v=4?s=60" width="60px;" alt=""/><br /><sub><b>memeplex</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=memeplex" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</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>
<tbody>
<tr>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt="Jani Eväkallio"/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt="Joe Previte"/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt="Riccardo"/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt="Janne Ojanaho"/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt="Paul Shen"/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt="coffenbacher"/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt="Mathieu Dutour"/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt="Michael Hansen"/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt="David Nadlinger"/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt="Fernando"/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt="Juan Gonzalez"/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt="Louie Christie"/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt="Sandro"/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt="Simon Knott"/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt="Steven"/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt="Tim"/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt="Saurav Khdoolia"/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt="Ankit Tiwari"/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt="Ayush Baweja"/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt="TaiChi-IO"/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt="Juan F Gonzalez "/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt="Sanket Dasgupta"/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt="Nicholas Stafie"/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt="Francis Hamel"/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt="digiguru"/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt="CHIRAG SINGHAL"/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt="Jonathan Carter"/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt="Julian Elve"/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt="Thomas Koppelaar"/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt="Akshay"/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt="John Lindquist"/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt="Ashwin Ramaswami"/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt="Claudio Canales"/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt="vitaly-pevgonen"/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt="Dmitry Shemetov"/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt="hooncp"/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt="Martin Laws"/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt="Sean K Smith"/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt="Kevin Neely"/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt="Arief Rahmansyah"/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt="Vishesh Handa"/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt="Hitesh Kumar"/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt="Spencer Woo"/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt="ingalless"/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt="José Duarte"/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt="Yenly"/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt="hikerpig"/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt="Sigfried Gold"/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt="Tristan Sokol"/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt="Danil Rodin"/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt="Scott Williams"/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt="jackiexiao"/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt="John B Nelson"/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt="Asif Mehedi"/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt="Tan Li"/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt="Shauna Gordon"/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt="Mike Cluck"/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt="Brandon Pugh"/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt="Max Davitt"/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt="Brian Anglin"/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt="elswork"/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt="léon h"/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
<td align="center"><a href="https://nygaard.site"><img src="https://avatars.githubusercontent.com/u/4606342?v=4?s=60" width="60px;" alt="Nikhil Nygaard"/><br /><sub><b>Nikhil Nygaard</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=njnygaard" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt="Mark Dixon"/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt="Joel James"/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt="Hashiguchi Ryo"/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt="Michael Overmeyer"/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt="Derrick Qin"/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt="Omar López"/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt="Robin King"/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt="Dheepak "/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt="Daniel VG"/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt="Barabas"/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt="Engincan VESKE"/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt="Paul de Raaij"/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt="Scott Bronson"/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt="Rafael Riedel"/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Pearcekieser"><img src="https://avatars.githubusercontent.com/u/5055971?v=4?s=60" width="60px;" alt="Pearcekieser"/><br /><sub><b>Pearcekieser</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Pearcekieser" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/theowenyoung"><img src="https://avatars.githubusercontent.com/u/62473795?v=4?s=60" width="60px;" alt="Owen Young"/><br /><sub><b>Owen Young</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=theowenyoung" title="Documentation">📖</a> <a href="#content-theowenyoung" title="Content">🖋</a></td>
<td align="center"><a href="http://www.prashu.com"><img src="https://avatars.githubusercontent.com/u/476729?v=4?s=60" width="60px;" alt="Prashanth Subrahmanyam"/><br /><sub><b>Prashanth Subrahmanyam</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ksprashu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/JonasSprenger"><img src="https://avatars.githubusercontent.com/u/25108895?v=4?s=60" width="60px;" alt="Jonas SPRENGER"/><br /><sub><b>Jonas SPRENGER</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=JonasSprenger" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Laptop765"><img src="https://avatars.githubusercontent.com/u/1468359?v=4?s=60" width="60px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Laptop765" title="Documentation">📖</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=60" width="60px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/memeplex"><img src="https://avatars.githubusercontent.com/u/2845433?v=4?s=60" width="60px;" alt="memeplex"/><br /><sub><b>memeplex</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=memeplex" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt="AndreiD049"/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt="Yan"/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt="Jim Tittsler"/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt="Malcolm Mielle"/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt="Veesar"/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt="bentongxyz"/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt="Brian DeVries"/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</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="Clifford Fajardo "/><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="Chris Usick"/><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="Joe DeCock"/><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="Drew Tyler"/><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="Lauviah0622"/><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="Josh Dover"/><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="Phil Helm"/><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="Larry Li"/><br /><sub><b>Larry Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lingyv-li" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/infogulch"><img src="https://avatars.githubusercontent.com/u/133882?v=4?s=60" width="60px;" alt="Joe Taber"/><br /><sub><b>Joe Taber</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=infogulch" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.readingsnail.pe.kr"><img src="https://avatars.githubusercontent.com/u/1904967?v=4?s=60" width="60px;" alt="Woosuk Park"/><br /><sub><b>Woosuk Park</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=readingsnail" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.dmurph.com"><img src="https://avatars.githubusercontent.com/u/294026?v=4?s=60" width="60px;" alt="Daniel Murphy"/><br /><sub><b>Daniel Murphy</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dmurph" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dominic-DallOsto"><img src="https://avatars.githubusercontent.com/u/26859884?v=4?s=60" width="60px;" alt="Dominic D"/><br /><sub><b>Dominic D</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Dominic-DallOsto" title="Code">💻</a></td>
<td align="center"><a href="http://elgirafo.xyz"><img src="https://avatars.githubusercontent.com/u/80516439?v=4?s=60" width="60px;" alt="luca"/><br /><sub><b>luca</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elgirafo" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Lloyd-Jackman-UKPL"><img src="https://avatars.githubusercontent.com/u/55206370?v=4?s=60" width="60px;" alt="Lloyd Jackman"/><br /><sub><b>Lloyd Jackman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lloyd-Jackman-UKPL" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://sn3akiwhizper.github.io"><img src="https://avatars.githubusercontent.com/u/102705294?v=4?s=60" width="60px;" alt="sn3akiwhizper"/><br /><sub><b>sn3akiwhizper</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sn3akiwhizper" title="Documentation">📖</a></td>
<td align="center"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->

View File

@@ -2222,9 +2222,9 @@
"@babel/types" "^7.3.0"
"@types/braces@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb"
integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b"
integrity sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==
"@types/dateformat@^3.0.1":
version "3.0.1"
@@ -2340,9 +2340,9 @@
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/micromatch@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
integrity sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.2.tgz#ce29c8b166a73bf980a5727b1e4a4d099965151d"
integrity sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==
dependencies:
"@types/braces" "*"
@@ -5087,11 +5087,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fast-array-diff@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fast-array-diff/-/fast-array-diff-1.0.1.tgz#463bacfeddaa3f5d56b79f6847fe322f15581c92"
integrity sha512-pU83E/Y7+c/hRrDlmIiPYHy0Ugt+QypqzHKZI5qFOWMJAspWdmOyIeN/1FbdnGPlROp6FeGLLfhMO075DBqb4A==
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -9548,11 +9543,6 @@ remark-wiki-link@^0.0.4:
"@babel/runtime" "^7.4.4"
unist-util-map "^1.0.3"
remove-markdown@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98"
integrity sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -9575,11 +9565,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
replace-ext@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06"
integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"