Compare commits

...

142 Commits

Author SHA1 Message Date
Riccardo Ferretti
b25152d115 v0.25.11 2024-03-18 13:17:27 +01:00
Riccardo Ferretti
1545079c62 Prepare for release 2024-03-18 13:17:04 +01:00
Riccardo Ferretti
4835164902 Fire onDidUpdate even only after full graph recomputed 2024-03-18 13:16:26 +01:00
Riccardo Ferretti
06efdc2865 v0.25.10 2024-03-18 10:09:31 +01:00
Riccardo Ferretti
b68fd7e138 Prepare for release 2024-03-18 10:09:04 +01:00
Riccardo Ferretti
d8baa2fd36 Fixed graph computation issue 2024-03-18 10:06:18 +01:00
Riccardo Ferretti
7f587095e8 v0.25.9 2024-03-17 20:51:24 +01:00
Riccardo Ferretti
77ad245319 Prepare next release 2024-03-17 20:51:16 +01:00
Riccardo
b892c783da Rename placeholder on note creation so it can update it if necessary (#1344)
* Introduced Location
* Passing a reference to the source link to the create-note command

Also
* Added withTiming fn for performance logging
* Added extra test to check incoming wikilink with sections
* Tweaked creation of vscode URI to also support raw objects
2024-03-17 20:49:11 +01:00
Andrew Thiesen
e4f6259104 Update recipes.md (#1341)
REM foamy-js. https://github.com/yenly/foamy-nextjs has been Archived and is no longer being maintained.
2024-03-10 15:18:37 +01:00
Riccardo Ferretti
aa197239fc v0.25.8 2024-02-21 09:53:40 +01:00
Riccardo Ferretti
8f3c23dd60 Prepare for release 2024-02-21 09:47:23 +01:00
Richard C Yeh
9a027c08ba Bump dateformat from 3.0.3 to 4.5.1. Closes #1324. (#1326) 2024-02-19 10:15:34 +01:00
dependabot[bot]
959d0f1ea1 Bump axios from 1.3.2 to 1.6.1 (#1306)
Bumps [axios](https://github.com/axios/axios) from 1.3.2 to 1.6.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.2...v1.6.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:48:34 +01:00
dependabot[bot]
57e32c4349 Bump @babel/traverse from 7.20.13 to 7.23.2 (#1297)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.13 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:48:10 +01:00
dependabot[bot]
f168f66368 Bump follow-redirects from 1.15.2 to 1.15.4 (#1318)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:47:46 +01:00
Riccardo Ferretti
103ff12b2d v0.25.7 2024-01-16 14:47:01 +01:00
Riccardo Ferretti
96a3afa132 Prepare for release 2024-01-16 14:46:11 +01:00
Riccardo Ferretti
d586e63104 Removed "use" from quick fix list elements 2024-01-16 14:29:54 +01:00
Miguel Angel Bruni Montero
2fba6e9008 Modifies url encoding to target only the filename and skip spaces (#1322) 2024-01-15 20:22:04 +01:00
Riccardo
cdbb965661 Update commands.md 2023-12-15 22:31:54 +01:00
Riccardo Ferretti
0c958e31f6 v0.25.6 2023-12-13 20:01:32 +01:00
Riccardo Ferretti
fd84fcfd74 Preparing next release 2023-12-13 20:01:00 +01:00
allcontributors[bot]
becf495edc add MABruni as a contributor for code (#1313)
* 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-12-13 19:59:11 +01:00
Miguel Angel Bruni Montero
4d99883c03 Fix #1298. Wikilink definitions links not working. (#1311)
* Added encoding to handle special characters when wikilink definitions are generated

* Added tests for wikilinks encoding and updated previous tests to support encoded links
2023-12-13 19:58:36 +01:00
Riccardo Ferretti
8bd679c751 v0.25.5 2023-11-30 19:19:26 +01:00
Riccardo Ferretti
8986ee286c Preparation for next release 2023-11-30 19:19:03 +01:00
Riccardo
51b1af1981 Using note title in preview panel (#1309)
See conversation in https://github.com/foambubble/foam/pull/1150
2023-11-30 19:17:50 +01:00
Daniel Carosone
4276e8043f extension descriptions now reflect .vscode dir (#1302)
* extension descriptions now reflect .vscode dir

including that image pasting is now supported natively

* Markdown AiO is still recommended

---------

Co-authored-by: Daniel Carosone <dan@geek.com.au>
2023-10-26 10:24:51 +02:00
Riccardo Ferretti
8d1e9b15ce Readding markdown-all-in-one in extension list and fixing script 2023-10-26 10:23:45 +02:00
jonathan berger
bfcfad32e8 Add more detail to Support for sections. (#1296)
* Add more detail to Support for `sections`.

* Removed extra quote

---------

Co-authored-by: Riccardo <code@riccardoferretti.com>
2023-10-25 12:53:00 +02:00
allcontributors[bot]
abe18cc961 add dcarosone as a contributor for doc (#1301)
* 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-10-25 11:43:29 +02:00
Riccardo Ferretti
9490aa2dad Fixed typo 2023-10-25 11:36:34 +02:00
Riccardo Ferretti
6c3f4588b3 Explicitly create .vscode folder in sync script 2023-10-25 11:26:07 +02:00
Riccardo Ferretti
21b8c5b827 Updated default extensions and settings for foam docs and template
Also monitoring the folder to trigger the appropriate sync job
2023-10-25 11:22:46 +02:00
Daniel Carosone
c66ed74aea replicate .vscode dir to template (#1300)
fixes #1299

Co-authored-by: Daniel Carosone <dan@geek.com.au>
2023-10-25 11:17:43 +02:00
Riccardo Ferretti
3876811fb6 v0.25.4 2023-10-19 17:41:13 +02:00
Riccardo Ferretti
d9299ee9d4 Prepare for next release 2023-10-19 17:40:29 +02:00
Joe Taber
23e21a62f3 Copy assets to foam-template repo (#1238) 2023-10-12 17:22:38 +02:00
Riccardo
e7c8d5a4eb Added support for linking to sections in current file (#1289)
And improved support for links to sections that need to be slugified
2023-10-12 17:01:13 +02:00
Daniel Wang
3ef1b69b2e Fix embedded notes not generating proper reference links (#1286) 2023-09-25 10:09:13 +02:00
Riccardo Ferretti
5859b2a9c6 v0.25.3 2023-09-07 13:07:26 +02:00
Riccardo Ferretti
54086fdd7e Preparation for next release 2023-09-07 13:07:06 +02:00
Daniel Wang
a308dfd109 Handle a case where the embedded note does not exist (#1283) 2023-09-07 09:03:25 +02:00
Daniel Wang
e327115673 Feat #879: Note Embedding Syntax (#1281)
* Removed deprecated preview.embedNoteInContainer

* Integrate explicit type modifiers to wikilink syntax

* Add to documentation
2023-09-06 00:08:56 +02:00
Riccardo Ferretti
c019767476 Fix #1242 - attachments are not considered in the context of orphan notes
A note that only has outbound links to attachments is now still considered an orphan.
2023-09-03 11:46:31 +02:00
Riccardo Ferretti
5e8b817a82 v0.25.2 2023-09-02 19:02:14 +02:00
Riccardo Ferretti
e0acc0ba8c Updated changelog 2023-09-02 19:01:49 +02:00
Riccardo
df4efc5138 Expand all button in tree views (#1276)
* Added expand all util function

* Added expand-all command for notes explorer

* Added expand-all command for placeholder tree view

* Added expand-all in tags explorer

* Added id field to tree items to make `reveal` work for all items
2023-08-30 16:19:14 +02:00
allcontributors[bot]
1e2b3b1bc3 add thara as a contributor for doc (#1280)
* 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-08-30 16:17:45 +02:00
Daniel Wang
eee364de50 Feat #879: Content embedding style (#1279)
* Add content extractor and enable it as an option for note embed type

* Fix content extractor edgecase where note title has lines above it
2023-08-30 16:17:25 +02:00
Tomochika Hara
3ed2bcd37b Fix deadlink (#1278) 2023-08-30 16:02:53 +02:00
Riccardo Ferretti
797aa9f29a v0.25.1 2023-08-23 19:31:27 +02:00
Riccardo Ferretti
30ab58485c Preparation for next release 2023-08-23 19:31:11 +02:00
Jim Graham
ba98f1990c Add last week's notes to snippet provider (#1248)
Adds snippets for

* `/-monday` -- last monday
* `/-tuesday` -- last tuesday

etc.
2023-08-23 19:26:17 +02:00
Daniel Wang
6c1d6868f7 Feat #879: Different embedding styles prep (#1273)
* Refactor note embedding to be extensible

* Prepare new setting to replace preview.embedNoteInContainer

* Fix wikilink-embed e2e tests

* Improve readability

* Set embedNoteInContainer to null in e2e tests to more closely mimic live state
2023-08-23 19:22:00 +02:00
Riccardo
e773e1ff68 Tags explorer improvements (#1275)
* Updated FolderTreeProvider to allow for values at any level of the tree

* Added utility method "walk" and passing node to createValueTreeItem

* Moved tag explorer to use FolderTreeProvider

* Added path in node info

* Added commands for showing tags in workspace or current file only

* Added support for grouping or flattening tags hierarchy

* Moved e2e test cleanup in the suite so that it also works when running the tests from VS Code debug task

* Added flag for registering commands in tags-explorer (needed for testing :( )
2023-08-22 12:36:32 +02:00
Riccardo
86e2bb1ba0 Improved test cleanup and config management (#1274)
* clean up test workspace before running tests

* improved function that runs code with modified config to properly restore former value
2023-08-21 14:30:21 +02:00
allcontributors[bot]
fc2fb6a0ab add nicholas-l as a contributor for code (#1272)
* 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-08-17 10:44:30 +02:00
Nicholas Latham
956d0119be Update Jest to v29 (#1271)
* Update Jest to v29

* Add stream to workspace test

Without `--stream` in the workspace package, the command ends
successfully without correctly running the tests.
2023-08-17 10:43:51 +02:00
allcontributors[bot]
8ddb6a2d12 add tcheneau as a contributor for doc (#1268)
* 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-08-05 11:49:59 +02:00
Tony Cheneau
fb9447630f Update note-templates.md (#1267)
Fix typo (backticks instead of simple quotes)
2023-08-04 16:07:17 +02:00
Riccardo
06a5988a52 Added support for path parameter in filter (#1250) 2023-07-01 14:08:24 +02:00
Riccardo Ferretti
fac2247382 v0.25.0 2023-06-30 14:12:50 +02:00
Riccardo Ferretti
b089b997bb Preparation for next release 2023-06-30 14:12:30 +02:00
Riccardo
a504054504 Support for multiple file extensions and default extension (#1235)
* Added multiple extension support for markdown provider

* Added default extension support

* Injecting extensions params to FoamWorkspace and MarkdownProvider (to avoid dependencies on non-core code)

* Inject extensions to attachment provider
2023-06-30 14:08:59 +02:00
Riccardo Ferretti
a00d18cfbb Fixed connections panel documentation (fixes @1244) 2023-06-19 13:08:16 +02:00
Riccardo Ferretti
5ca7c3eb52 Fixed #1236 - show full name in "Create note with template" hover 2023-05-23 22:33:08 +02:00
Riccardo
571b6a3528 Add FOAM_TITLE_SAFE variable (#1232)
* Introduced FOAM_TITLE_SAFE, which is the FOAM_TITLE variable with all the invalid path characters replaced by '-'

* (out of scope) In notes explorer show item description only when name is different from note title
2023-05-22 17:14:21 +02:00
Riccardo Ferretti
b6c9eac86c Notes explorer ignores untitled documents 2023-05-22 15:56:44 +02:00
dependabot[bot]
557330413c Bump yaml from 1.10.2 to 2.2.2 (#1210)
Bumps [yaml](https://github.com/eemeli/yaml) from 1.10.2 to 2.2.2.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v2.2.2)

---
updated-dependencies:
- dependency-name: yaml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-21 18:48:07 +02:00
Riccardo
20894a1166 Connections panel tweaks (#1233)
* Renamed commands and labels in connections panel

* renamed backlinks.ts -> connections.ts

* (out of scope) In notes explorer show item description only when name is different from note title
2023-05-21 18:37:53 +02:00
Riccardo Ferretti
d0b3d5ff11 v0.24.0 2023-05-19 11:07:30 +02:00
Riccardo Ferretti
34fb62bb0b Preparation for next release 2023-05-19 11:07:02 +02:00
Riccardo Ferretti
f297139e0c getBlock supports sections 2023-05-19 11:04:32 +02:00
Riccardo
09e13f77b0 Connections panel (#1230)
* Turning backlinks panel to connections panel

* Added support for filter commands

* Fixed broken imports that were driving tests nuts

* Do not register connections.* commands during test
2023-05-19 09:52:54 +02:00
allcontributors[bot]
56d8c4c7a0 add hezhizhen as a contributor for tool (#1229)
* 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-05-14 18:18:21 +02:00
Zhizhen He
626a323193 Add GH Action workflow to check spell (#1221) 2023-05-14 18:16:08 +02:00
Riccardo
25d9b5e459 Chore: improve janitor code (#1228)
* Refactor: improved generate link references code

* Cleaned up update-wikilink module

* update-wikilink command uses janitor code

* Moved NoteLinkDefinition code in own class, and fixed duplication error

* Renamed command

* Testing on linux only...while I figure out the issue with the other systems
2023-05-11 17:56:21 +02:00
Riccardo
c2241f16de Refactoring of Backlinks, Placeholder and Orphan panels (#1226)
- Placeholder and Orphan panels using the Folder hierarchy

- Backlinks using the same pattern as the other tree views
2023-05-07 22:06:22 +02:00
Riccardo Ferretti
5dee7cb2c0 Fixed link in documentation (#1225) 2023-05-07 21:40:14 +02:00
Riccardo
154ded382b Lint and cleanup (#1224)
* Simplified feature activation API

* Moved tree view util modules and added comments to classes

* Removed deprecated command foam-vscode.create-note-from-default-template
2023-05-06 15:37:00 +02:00
Riccardo Ferretti
5de69ff3c3 v0.23.0 2023-05-06 15:33:46 +02:00
Riccardo Ferretti
8aefcfd515 Preparation for 0.23.0 release 2023-05-06 15:33:22 +02:00
Riccardo
e0e08a2a0f Notes explorer (#1223)
* Added notes explorer

* Fixed line reference in range tree items

Thanks to Wikilens extensions for the high level inspiration (and the choice for the backlink tree item icon, as I find it just perfect)
2023-05-06 14:49:19 +02:00
Riccardo
93c5d2f80c Improvements in tree views (#1220) 2023-05-02 10:36:50 +02:00
Jim Graham
1c294d84c5 Enable tag completion in front matter (#1191)
* Addresses #1184

Currently tag completion only works in the front matter if you type a `#`
character. Adding the suggested tag will then mark the tag as a comment

```markdown
---
tags: #foo #bar
---
```

Update the tag completion provider to recognize if we are in the
front-matter, by using adding two functions to utils.ts. Because the
tag completion intellisense must be summoned with either the `#`
character or the keybinding (typically `ctrl+space`), allow
for 2 outcomes

1. if the tag is prefixed in the front matter with a `#`, remove it when
   substituting the tag.
2. If `ctrl-space` is used, recognize we are on the `tags: ` line and
   allow for non-`#` prefaced words.

The tag provider only works on the `tags: ` within the `tags: ` key of
the frontmatter. For example

```markdown
---
title: A title
tags:
 - foo
 - bar
 - |
```
(where `|` is the cursor) will provide suggestions for tags.

Outside the `tags:` element, suggestions will not be provided.
```markdown
---
title: A title
tags:
 - foo
 - bar
dates:
 - 2023-01-1
 - |
```

* Refactor into functions for front matter & content

Refactor the main provider method into two
sub-functions, one for front matter, one for
regular content. Add helper functions to generate
the `CompletionItems` and to find the start & end
indices of the last match to `#{tag}`.
2023-05-02 06:11:06 +02:00
Riccardo Ferretti
f1b15eceed v0.22.2 2023-04-20 18:25:37 -07:00
Riccardo Ferretti
96f410a453 Prepare for next release 2023-04-20 18:25:06 -07:00
Riccardo Ferretti
95a14d5dd6 Create blocks markdown parser only once 2023-04-20 18:25:00 -07:00
Riccardo
10905fd703 Various improvements in tree view panels (#1201)
* Show note block in panels on hover preview
* Show tag references within tag explorer
* Improved structure of view related commands
* Refactored grouped resources tree data provider and added support for placeholders filter
  - Consolidated the naming of the accessory commands
  - Consolidated the management of the state/context related to grouping
  - Removed group-by config, simply restore the last used setting
  - Added filter to only show the placeholders related to the open file
* Refreshing placeholders when changing editor and filtering by active document
2023-04-21 03:20:22 +02:00
Riccardo Ferretti
f4eaf5c5ff v0.22.1 2023-04-15 13:39:00 -07:00
Riccardo Ferretti
b4830eaf30 Prepare for next release 2023-04-15 13:38:28 -07:00
Jim Graham
0cda6aed50 Allow for # alone to trigger tag completion (#1192)
* Allow for `#` alone to trigger tag completion

In #1183, I reused [HASHTAG_REGEX](83a90177b9/packages/foam-vscode/src/core/utils/hashtags.ts (L2-L3))
to validate the tag line when the `CompletionProvider` was triggered.

I wanted to prevent this:

```markdown
 # This is a Markdown header
```

but using the `HASHTAG_REGEX` had the side effect of requiring an
_additional_ character to trigger the completion provider.

```markdown

1. #p <-- triggers completion
2. #  <-- does not trigger
3. #_ (space) <-- does not trigger
```
both 1. and 2. should have triggered.

To fix, I use a slightly different regex that uses a negative lookahead
to ensure that the `#` is not followed by a space. I also added spec
cases to cover this situation.

* Update regex for more robust detection of tags

Update the regex used for more robust detection of tags. Replace the
negative lookahead assertion `\s` with `[ \t]` (allow for `\n`), and
add `#` to the class so that `##` is ignored.

Attempted to add the negation `^[0-9p{L}p{Emoji}p{N}-/]` to the
negative look ahead. This was to exclude items like `#$`, `#&` that
can't be tags. However my regex-fu was insufficient.

Instead, if the regex match is to a single `#`, ensure it is the
character to the left of the cursor. Example

  `this is text #%|`

where the `|` represents the cursor. The `TAG_REGEX`
will match the `#` at index 13. However since the cursor is at 15, the
Completion provider will not run.

Update the tests to cover these situations and add them all to a sub-
`describe` block labeled by the bug issue number #1189

* Use regex groups to determine match position

For the case like `here is #my-tag and now # |`, where `|` is the cursor
position after a trailing space, the match on `#my-tag` would allow tag
completion at the cursor position.

Ensure that the last regexp match group covers up to the the cursor
position. This also handles the case of `#$` because the match will only
be `#`.
2023-04-15 22:34:55 +02:00
Riccardo Ferretti
89c9bb5a7f v0.22.0 2023-04-15 10:47:20 -07:00
Riccardo Ferretti
941e870a65 Prepare for 0.22.0 2023-04-15 10:47:06 -07:00
Riccardo
c6655c33ff Fixed #1193 and added tests (#1197) 2023-04-15 19:31:48 +02:00
Riccardo
c94fb18f8a Resource tree items improvements (#1196)
* Consolidated common tree view code and migrated placeholder panel
* Migrated backlink panel to new pattern
* Tweaked code and fixed tests
2023-04-15 19:21:24 +02:00
Riccardo
cbd55bac74 Fix #1134 - added support for deep tag hierarchy (#1194) 2023-04-15 02:22:12 +02:00
Riccardo Ferretti
83a90177b9 v0.21.4 2023-04-14 10:39:05 -07:00
Riccardo Ferretti
37aec28af6 Prepare for next release 2023-04-14 10:38:46 -07:00
Riccardo Ferretti
447f7fc068 Fix for #1188 and #1190 - escape backslash in YAML property of generated daily note template 2023-04-14 10:37:37 -07:00
Riccardo Ferretti
ad1243665a Removed unnecessary log message 2023-04-13 17:23:38 -07:00
Riccardo Ferretti
f07de73bc4 v0.21.3 2023-04-12 17:05:49 -07:00
Riccardo Ferretti
c431ccfb62 Preparation for next release 2023-04-12 17:05:25 -07:00
Riccardo Ferretti
f31ef897cc Fix #1188 - Fixed path relative to workspace root 2023-04-12 17:04:23 -07:00
Riccardo Ferretti
7a5f45c0ce v0.21.2 2023-04-11 13:51:30 -06:00
Riccardo Ferretti
df32d9e708 Preparation for next release 2023-04-11 13:51:13 -06:00
Riccardo Ferretti
b3d4691bfa Fix #1170 newNotePath not always working
This commit fixes an issue in the note creation flow where a template is used. Before the fix, when a template is present, Foam would force an absolute path resolution instead of delegating that step to the note creation. After the fix this step is removed and the resolution is left to the note creation code
2023-04-11 13:45:14 -06:00
allcontributors[bot]
f5260f7d3f add jimgraham as a contributor for code (#1185)
* 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-04-09 03:36:21 +02:00
Jim Graham
9b4b7ec84d Use the HASHTAG_REGEX to identify tags (#1183)
Instead of a generic `TAG_REGEX`, use the more specific `HASHTAG_REGEX` to identify a tag when launching tab completion.

Also add a new test case with the issue number in the test case description
2023-04-09 03:35:48 +02:00
Riccardo Ferretti
52b7f86a9f Updated TOC in FAQ docs 2023-03-22 10:56:11 -06:00
Joe Taber
2db7060124 Add GH Action workflow to sync user docs to template repo on change (#1180)
Also strips wikilinks during docs sync
fixes #1177
2023-03-22 17:47:22 +01:00
Riccardo
a4f04b3b6b Fixed temporary template generated (#1175) 2023-02-26 23:44:09 +01:00
Riccardo
b5a8a5d7c7 Chore - improved Text Edit API (#1174) 2023-02-26 23:37:50 +01:00
Riccardo
f5a29e431c Fix 1168 - relative path in embed (#1173)
* added isEmbed flag to ResourceLink
* Added tests for new embed support
2023-02-26 23:02:16 +01:00
Riccardo Ferretti
5a7a1ba89f v0.21.1 2023-02-24 17:32:15 -06:00
Riccardo Ferretti
b054bafc78 Preparation for next release 2023-02-24 17:31:55 -06:00
Riccardo
8acb60253a fix #1171 - add extension to notes created from placeholders (#1172) 2023-02-24 23:45:44 +01:00
allcontributors[bot]
3c69508dcb add Skakerman as a contributor for code (#1166)
* 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-02-16 21:32:28 +01:00
Scott Akerman
a368be9b47 Fix Previews of Embedded Sections (#1162) 2023-02-16 21:31:48 +01:00
Riccardo Ferretti
b1f76bb653 v0.21.0 2023-02-16 14:20:49 -06:00
Riccardo Ferretti
d4bc16b9bd Preparation for next release 2023-02-16 14:20:35 -06:00
Riccardo
882b0b6012 Use filter in open-resource command (#1161)
* Added title param in create-note command

* Added utility functions for commands

* Use create-note command when dealing with placeholders

* Updated open-resource command to new pattern

* Pass workspace.isTrusted to createFilter

* Fixed bug when finding absolute paths in workspace without providing basedir

* open-resource command can also receive `uri` param to skip filtering step

* added tests

* added docs
2023-02-15 22:53:32 +01:00
Riccardo Ferretti
048623d910 v0.20.8 2023-02-10 17:10:38 -06:00
Riccardo Ferretti
f2fbe927ae Preparation for next release 2023-02-10 17:10:22 -06:00
Riccardo
d0ee71be1b Updated a bunch of dependencies (#1160)
* Updated typescript and vscode engine version to support workspace trust

* updates tsdx to dts, and updated a other deps too

* updated eslint configuration

* Updated node version

* Update lerna

* Updated github action configuration

* removed glob library
2023-02-11 00:08:23 +01:00
Riccardo
2a14dc0c57 Added resource filters (#1158)
* Added note filter and a few tests

* Added expression for filters and trusted workspace support

* Consolidate `include` and `exclude` into `path` parameter

* Added documentation
2023-02-10 12:17:42 +01:00
Riccardo Ferretti
745acbabd3 Fixed VS Code installs badge 2023-02-01 13:33:33 +01:00
Riccardo Ferretti
c226cddcd8 v0.20.7 2023-02-01 13:23:06 +01:00
Riccardo Ferretti
1b0d3239b5 Preparation for next release 2023-02-01 13:22:48 +01:00
Riccardo Ferretti
cddf3bc769 Removed unnecessary activation events 2023-01-31 21:50:00 +01:00
Riccardo Ferretti
bd158e9b8e Partial fix to #1143 - Inform the user that links are not updated on directory rename 2023-01-31 21:49:24 +01:00
Riccardo Ferretti
5e648ca9c6 v0.20.6 2023-01-21 14:53:16 +01:00
Riccardo Ferretti
cc53f995f0 Preparation for next release 2023-01-21 14:53:01 +01:00
Riccardo
44fea21bc7 Support for creating new note in current dir or root dir (#1142)
* Added support for creating new note in current dir or root dir
* Added tests
2023-01-14 23:30:55 +01:00
Liu YongLiang
2b70e49e95 Update contribution-guide.md with node version and fix broken link (#1136)
* Update contribution-guide.md
2023-01-13 10:23:11 +01:00
Daniel Wang
f5c4cba799 Fix #920: Links in Markdown Preview not jumping to sections (#1135)
Fixed wikilink section linking
2023-01-13 10:22:11 +01:00
Riccardo
e06f46e393 Update engines: node to 16 and vscode to 1.70.0 (#1140) 2023-01-11 20:02:49 +01:00
Liu YongLiang
4ce51673cc Update build-vs-assemble.md (#1137) 2023-01-11 19:22:21 +01:00
allcontributors[bot]
4db9409679 add tlylt as a contributor for doc (#1139)
* 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-11 19:21:57 +01:00
Liu YongLiang
0e6b9f8c83 Add markup-converter recipe (#1138) 2023-01-11 19:21:17 +01:00
160 changed files with 13154 additions and 10330 deletions

View File

@@ -995,8 +995,90 @@
"contributions": [
"code"
]
},
{
"login": "tlylt",
"name": "Liu YongLiang",
"avatar_url": "https://avatars.githubusercontent.com/u/41845017?v=4",
"profile": "http://yongliangliu.com",
"contributions": [
"doc"
]
},
{
"login": "Skakerman",
"name": "Scott Akerman",
"avatar_url": "https://avatars.githubusercontent.com/u/15224439?v=4",
"profile": "http://scottakerman.com",
"contributions": [
"code"
]
},
{
"login": "jimgraham",
"name": "Jim Graham",
"avatar_url": "https://avatars.githubusercontent.com/u/430293?v=4",
"profile": "http://www.jim-graham.net/",
"contributions": [
"code"
]
},
{
"login": "hezhizhen",
"name": "Zhizhen He",
"avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4",
"profile": "https://t.me/littlepoint",
"contributions": [
"tool"
]
},
{
"login": "tcheneau",
"name": "Tony Cheneau",
"avatar_url": "https://avatars.githubusercontent.com/u/952059?v=4",
"profile": "https://amnesiak.org/me",
"contributions": [
"doc"
]
},
{
"login": "nicholas-l",
"name": "Nicholas Latham",
"avatar_url": "https://avatars.githubusercontent.com/u/12977174?v=4",
"profile": "https://github.com/nicholas-l",
"contributions": [
"code"
]
},
{
"login": "thara",
"name": "Tomochika Hara",
"avatar_url": "https://avatars.githubusercontent.com/u/1532891?v=4",
"profile": "https://thara.dev",
"contributions": [
"doc"
]
},
{
"login": "dcarosone",
"name": "Daniel Carosone",
"avatar_url": "https://avatars.githubusercontent.com/u/11495017?v=4",
"profile": "https://github.com/dcarosone",
"contributions": [
"doc"
]
},
{
"login": "MABruni",
"name": "Miguel Angel Bruni Montero",
"avatar_url": "https://avatars.githubusercontent.com/u/100445384?v=4",
"profile": "https://github.com/MABruni",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
"skipCi": true,
"commitType": "docs"
}

View File

@@ -6,21 +6,22 @@
"sourceType": "module"
},
"env": { "node": true, "es6": true },
"plugins": ["@typescript-eslint", "import", "jest"],
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jest/recommended"
],
"rules": {
"no-redeclare": "off",
"no-unused-vars": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/no-unused-vars": "warn",
"import/no-extraneous-dependencies": [
"error",
{

View File

@@ -9,18 +9,28 @@ on:
- master
jobs:
typos-check:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v3
- name: Check spelling with custom config file
uses: crate-ci/typos@v1.14.8
with:
config: ./typos.toml
lint:
name: Lint
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v1
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '12'
node-version: '18'
- name: Restore Dependencies and VS Code test instance
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@@ -34,21 +44,22 @@ jobs:
test:
name: Build and Test
strategy:
matrix:
os: [macos-10.15, ubuntu-18.04, windows-2019]
runs-on: ${{ matrix.os }}
env:
OS: ${{ matrix.os }}
# strategy:
# matrix:
# os: [macos-12, ubuntu-22.04, windows-2022]
# runs-on: ${{ matrix.os }}
runs-on: ubuntu-22.04
# env:
# OS: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v1
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '12'
node-version: '18'
- name: Restore Dependencies and VS Code test instance
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules

49
.github/workflows/update-docs.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Update Docs
on:
push:
branches:
- master
paths:
- docs/user/**/*
- docs/.vscode/**/*
- docs/assets/**/*
workflow_dispatch:
jobs:
update-docs:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
repository: foambubble/foam-template
path: foam-template
- uses: actions/checkout@v3
with:
path: foam
- name: Copy and fixup user docs files
id: copy
run: |
rm -r foam-template/docs
rm -r foam-template/assets
rm -r foam-template/.vscode
cp -r foam/docs/user foam-template/docs
cp -r foam/docs/assets foam-template/assets
cp -r foam/docs/.vscode foam-template/.vscode
# Strip autogenerated wikileaks references because
# they are not an appropriate default user experience.
(cd foam-template/docs; sed -i '/\[\/\/begin\]/,/\[\/\/end\]/d' $(find . -type f -name \*.md))
# Set the commit message format
echo "message=Docs sync @ $(cd foam; git log --pretty='format:%h %s')" >> $GITHUB_OUTPUT
- uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.FOAM_DOCS_SYNC_TOKEN }}
path: foam-template
commit-message: ${{ steps.copy.outputs.message }}
branch: bot/foam-docs-sync
delete-branch: true
title: Sync docs from foam
body: Copy docs from main foam repo

View File

@@ -5,17 +5,11 @@
// Foam's own extension
"foam.foam-vscode",
// Prettier for auto formatting code
"esbenp.prettier-vscode",
// GitLens for seeing version history inline
"eamodio.gitlens",
// Tons of markdown goodies (lists, tables of content, so much more)
"yzhang.markdown-all-in-one",
// Graph visualizer
"tchayen.markdown-links",
// Prettier for auto formatting code
"esbenp.prettier-vscode",
// Understated grayscale theme (light and dark variants)
"philipbe.theme-gray-matter"

View File

@@ -1,4 +0,0 @@
{
"purpose": "this file exists to tell the foam-vscode plugin that it's currently in a foam workspace",
"future": "we may use this for custom configuration"
}

View File

@@ -3,6 +3,6 @@
[
{
"key": "cmd+shift+n",
"command": "vscodeMarkdownNotes.newNote"
"command": "foam-vscode.create-note"
}
]

View File

@@ -5,7 +5,6 @@
"editor.overviewRulerBorder": false,
"editor.lineHeight": 24,
"foam.edit.linkReferenceDefinitions": "withExtensions",
"vscodeMarkdownNotes.noteCompletionConvention": "noExtension",
"[markdown]": {
"editor.quickSuggestions": {
"other": true,
@@ -13,21 +12,11 @@
"strings": false
}
},
"cSpell.language": "en-GB",
"git.enableSmartCommit": true,
"git.postCommitCommand": "sync",
"spellright.language": [
"en"
],
"spellright.documentTypes": [
"markdown",
"plaintext"
],
"files.exclude": {
"_site/**": true
},
"files.insertFinalNewline": true,
"markdown.styles": [
".vscode/custom-tag-style.css"
]
"markdown.styles": [".vscode/custom-tag-style.css"]
}

View File

@@ -1 +0,0 @@
Backlinking

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

View File

@@ -3,7 +3,7 @@
The Foam prototype is built by assembling third-party extensions, which seems like a good strategy because
- It supports picking and mixing of tools and workflows
- Less code to write an maintain
- Less code to write and maintain
But there's also a bunch of roadmap items that are hard to implement this way, as the third party plugins don't do exactly what we want them to do (e.g. Markdown All In One is not compatible with [[referencing-notes-by-title]].

View File

@@ -23,7 +23,7 @@ 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 (assuming [node.js](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
If you're interested in contributing, this short guide will help you get things set up locally (assuming [node.js >= v16](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
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:
@@ -55,7 +55,7 @@ This project uses [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspac
Originally Foam had:
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
- [/packages/foam-core](https://github.com/foambubble/foam/tree/ee7a8919761f168d3931079adf21c5ad4d63db59/packages/foam-core) - Powers the core functionality in Foam across all platforms.
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VS Code plugin.
To improve DX we have moved the `foam-core` module into `packages/foam-vscode/src/core`, but from a development point of view it's useful to think of the `foam-vscode/src/core` "submodule" as something that might be extracted in the future.
@@ -93,7 +93,7 @@ After you have made your changes to your copy of the project, it is time to try
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.
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://blog.robsewell.com/blog/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!

View File

@@ -14,7 +14,7 @@ Currently it is not possible within Foam to include other notes into a note. Nex
Initial work and thought on including a note was ignited by issue [#652](https://github.com/foambubble/foam/issues/652). Requested by a user was a likewise functionality as offered in Obsidian. This was simply the ability to include a note.
Whilst researching digital gardening for my own setup, I came across an in-depth overview by [Maggie Appleton](https://maggieappleton.com/roam-garden). Showing examples of her personal Roam Research I see valuable possibilites to connect more information, if we would add additional functionalities to the possibility of including a note. This proposal displays these possible functionalities and markup.
Whilst researching digital gardening for my own setup, I came across an in-depth overview by [Maggie Appleton](https://maggieappleton.com/roam-garden). Showing examples of her personal Roam Research I see valuable possibilities to connect more information, if we would add additional functionalities to the possibility of including a note. This proposal displays these possible functionalities and markup.
## New features
@@ -29,7 +29,7 @@ The minimal functionality is the ability to fully include a note. Markup used in
### Include a section of a note
It could be interesting to only include a section of a note instead of the entire note. In order to do so thse user should be able to use the following syntax:
It could be interesting to only include a section of a note instead of the entire note. In order to do so the user should be able to use the following syntax:
`![[wikilink#section-b]]`
@@ -37,11 +37,11 @@ As a result it will include the section title + section content until the next s
### Include an attribute of a file (note property or frontmatter)
As a user I could be interested in collecting the value of any given proeprty for a note. For example, I might want to include the tags as defined in the frontmatter of note A. This should be possible via the syntax:
As a user I could be interested in collecting the value of any given property for a note. For example, I might want to include the tags as defined in the frontmatter of note A. This should be possible via the syntax:
`![[wikilink:<property>]]`
The property value should be lookedup by foam defined properties, e.g. title, **or** any property defined in the frontmatter of a note.
The property value should be looked up by foam defined properties, e.g. title, **or** any property defined in the frontmatter of a note.
So, the example of including the tags of a note should be:

View File

@@ -117,7 +117,7 @@ The potential solution:
enum LinkReferenceDefinitions {
Off, // link reference definitions are not generated
WithExtensions, // link reference definitions contain .md (or similar) file extensions
WithoutExtensions // link reference definitions do not contain file extenions
WithoutExtensions // link reference definitions do not contain file extensions
}
```

View File

@@ -20,6 +20,6 @@
- select "tags" in top left
- select the tag that was just released, click "edit" and copy release information from changelog
- publish (no need to attach artifacts)
8. Annouce on Discord
8. Announce on Discord
Steps 1 to 6 should really be replaced by a GitHub action...
Steps 1 to 6 should really be replaced by a GitHub action...

View File

@@ -108,144 +108,155 @@ If that sounds like something you're interested in, I'd love to have you along o
<table>
<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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><a href="http://yongliangliu.com"><img src="https://avatars.githubusercontent.com/u/41845017?v=4?s=60" width="60px;" alt="Liu YongLiang"/><br /><sub><b>Liu YongLiang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tlylt" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://scottakerman.com"><img src="https://avatars.githubusercontent.com/u/15224439?v=4?s=60" width="60px;" alt="Scott Akerman"/><br /><sub><b>Scott Akerman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skakerman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=60" width="60px;" alt="Zhizhen He"/><br /><sub><b>Zhizhen He</b></sub></a><br /><a href="#tool-hezhizhen" title="Tools">🔧</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://amnesiak.org/me"><img src="https://avatars.githubusercontent.com/u/952059?v=4?s=60" width="60px;" alt="Tony Cheneau"/><br /><sub><b>Tony Cheneau</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tcheneau" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-l"><img src="https://avatars.githubusercontent.com/u/12977174?v=4?s=60" width="60px;" alt="Nicholas Latham"/><br /><sub><b>Nicholas Latham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nicholas-l" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -1,10 +1,11 @@
# Backlinking
When using [[wikilinks]], you can find all notes that link to a specific note in the **Backlinks Explorer**
When using [[wikilinks]], you can find all notes that link to a specific note in the **Connections Explorer**
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "backlinks" and run the **Explorer: Focus on Backlinks** view.
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "connections" and run the **Explorer: Focus on Connections** view.
- Keep this pane always visible to discover relationships between your thoughts
- You can drag the backlinks pane to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
- You can drag the connections panel to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
- You can filter the connections to see just backlinks, forward links, or all connections
- Finding backlinks in published Foam workspaces via [[materialized-backlinks]] is on the [[roadmap]] but not yet implemented.
[//begin]: # "Autogenerated link references for markdown compatibility"

View File

@@ -0,0 +1,29 @@
# Built-In Note Embedding Types
When embedding a note, there are a few ways to modify the scope of the content as well as its display style. The following are Foam keywords that are used to describe note embedding.
Note, this only applies to note embedding, not embedding of attachments or images.
![Note Embed Types GIF](../../assets/images/note-embed-type-demo.gif)
## Scope
- `full` - the entire note in the case of `![[note]]` or the entire section in the case of `![[note#section1]]`
- `content` - everything excluding the title of the section. So the entire note minus the title for `![[note]]`, or the entire section minus the section header for `![[note#section1]]`
## Style
- `card` - outlines the embedded note with a border
- `inline` - adds the note continuously as if the text were part of the calling note
## Default Setting
Foam expresses note display type as `<scope>-<style>`.
By default, Foam configures note embedding to be `full-card`. That is, whenever the standard embedding syntax is used, `![[note]]`, the note will have `full` scope and `card` style display. This setting is stored under `foam.preview.embedNoteStyle` and can be modified.
## Explicit Modifiers
Prepend the wikilink with one of the scope or style keywords, or a combination of the two to explicitly modify a note embedding if you would like to override the default setting.
For example, given your `foam.embedNoteStyle` is set to `content-card`, embedding a note with standard syntax `![[note-a]]` would show a bordered note without its title. Say, for a specific `note-b` you would like to display the title. You can simply use one of the above keywords to override your default setting like so: `full![[note-b]]`. In this case, `full` overrides the default `content` scope and because a style is not specified, it falls back to the default style setting, `card`. If you would like it to be inline, override that as well: `full-inline![[note-b]]`.

View File

@@ -2,23 +2,26 @@
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.
In particular, some commands can be very customizable 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
- `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.
- `title`: The title of the note (that is, the `FOAM_TITLE` variable)
- `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
- `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",
@@ -32,6 +35,7 @@ To customize a command and associate a key binding to it, open the key binding s
```
- Create a note following the `weekly-note.md` template. If the note already exists, open it
```
{
"key": "alt+g",
@@ -43,3 +47,27 @@ To customize a command and associate a key binding to it, open the key binding s
}
```
## foam-vscode.open-resource command
This command opens a resource.
Normally it receives a `URI`, which identifies the resource to open.
It is also possible to pass in a filter, which will be run against the workspace resources to find one or more matches.
- If there is one match, it will be opened
- If there is more than one match, a quick pick will show up allowing the user to select the desired resource
Examples:
```
{
"key": "alt+f",
"command": "foam-vscode.open-resource",
"args": {
"filter": {
"title": "Weekly Note*"
}
}
}
```

View File

@@ -62,7 +62,7 @@ It is possible to customize the style of a node based on the `type` property in
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.
- `note` defines the color for regular nodes whose documents have not overridden 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.

View File

@@ -4,17 +4,20 @@ In some situations it might be useful to include the content of another note in
## Including a note
Including a note can be done by adding an `!` before a wikilink defintion. For example `![[wikilink]]`.
Including a note can be done by adding an `!` before a wikilink definition. For example `![[wikilink]]`.
## Custom styling
Displaying the inclusion of notes allows for some custom styling, see [[custom-markdown-preview-styles]]
To modify how an embedded note looks and the scope of its content, see [[built-in-note-embedding-types]]
For more fine-grained custom styling, see [[custom-markdown-preview-styles]]
## Future possibilities
Work on this feature is evolving and progressing. See the [[inclusion-of-notes]] proposal for the current discussion.
[//begin]: # "Autogenerated link references for markdown compatibility"
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md "Custom Markdown Preview Styles"
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md "Inclusion of notes Proposal "
[//end]: # "Autogenerated link references"
[//begin]: # 'Autogenerated link references for markdown compatibility'
[built-in-note-embedding-types]: built-in-note-embedding-types.md 'Built-In Note Embedding Types'
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md 'Custom Markdown Preview Styles'
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md 'Inclusion of notes Proposal '
[//end]: # 'Autogenerated link references'

View File

@@ -19,6 +19,7 @@ The following example:
```
You can open the [raw markdown](https://foambubble.github.io/foam/features/link-reference-definitions.md) to see them at the bottom of this file
You can open the [raw markdown](https://foambubble.github.io/foam/user/features/link-reference-definitions.md) to see them at the bottom of this file
## Specification

View File

@@ -19,7 +19,7 @@ keywords: hello world, bonjour
---
```
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`).
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 separators 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.
@@ -27,11 +27,11 @@ This sets the `type` of this document to `feature` and sets **three** keywords f
Some properties have special meaning for Foam:
| 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]]) |
| 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:

View File

@@ -58,6 +58,7 @@ In addition, you can also use variables provided by Foam:
| -------------------- | ------------ |
| `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_TITLE_SAFE` | The title of the note in a file system safe format. If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `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. |
@@ -193,7 +194,7 @@ You can add the template metadata to its own YAML Frontmatter block at the start
foam_template:
name: My Note Template
description: This is my note template
filepath: `journal/$FOAM_TITLE.md`
filepath: 'journal/$FOAM_TITLE.md'
---
This is the rest of the template
```
@@ -205,7 +206,7 @@ If the note already has a Frontmatter block, a Foam-specific Frontmatter block c
foam_template:
name: My Note Template
description: This is my note template
filepath: `journal/$FOAM_TITLE.md`
filepath: 'journal/$FOAM_TITLE.md'
---
---

View File

@@ -0,0 +1,42 @@
# Resource Filters
Resource filters can be passed to some Foam commands to limit their scope.
A filter supports the following parameters:
- `tag`: include a resource if it has the given tag (e.g. `{"tag": "#research"}`)
- `type`: include a resource if it is of the given type (e.g. `{"type": "daily-note"}`)
- `path`: include a resource if its path matches the given regex (e.g. `{"path": "/projects/*"}`). **Note that this parameter supports regex and not globs.**
- `expression`: include a resource if it makes the given expression `true`, where `resource` represents the resource being evaluated (e.g. `{"expression": "resource.type ==='weekly-note'"}`)
- `title`: include a resource if the title matches the given regex (e.g. `{"title": "Team meeting:*"}`)
A filter also supports some logical operators:
- `and`: include a resource if it matches all the sub-parameters (e.g `{"and": [{"tag": "#research"}, {"title": "Paper *"}]}`)
- `or`: include a resource if it matches any of the sub-parameters (e.g `{"or": [{"tag": "#research"}, {"title": "Paper *"}]}`)
- `not`: invert the result of the nested filter (e.g. `{"not": {"type": "daily-note"}}`)
Here is an example of a complex filter, for example to show the Foam graph only of a subset of the workspace:
```
{
"key": "alt+f",
"command": "foam-vscode.show-graph",
"args": {
"filter": {
"and": [
{
"or": [
{ "type": 'daily-note' },
{ "type": 'weekly-note' },
{ "path": '/projects/*' },
],
"not": {
{ "tag": '#b' },
},
},
],
}
}
}
```

View File

@@ -15,6 +15,10 @@ There are two ways of creating a tag:
Tags can also be hierarchical, so you can have `#parent/child` such as #my-tag3/info.
### Tag completion
Typing the `#` character will launch VS Code's "Intellisense." This provider will show a list of possible tags that match the character. If you are editing in the frontmatter [[note-properties|note property]], you can invoke tag completion on the `tags:` line by either typing the `#` character, or using the ["trigger suggest"](https://code.visualstudio.com/docs/editor/intellisense) keybinding (usually `ctrl+space`). If the `#` is used in the frontmatter, it will be removed when the tag is inserted.
## Using *Tag Explorer*
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.
@@ -33,7 +37,7 @@ It is possible to customize the way that tags look in the Markdown Preview panel
> 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.
The end result will be a CSS file that looks similar to the content below. Now you can make your tags standout in your note previews.
```css
.foam-tag{
@@ -49,7 +53,10 @@ The end result will be a CSS file that looks similiar to the content below. Now
Given the power of backlinks, some people prefer to use them as tags.
For example you can tag your notes about books with [[book]].
[note-properties|note property]: note-properties.md "Note Properties"
[graph-visualization]: graph-visualization.md "Graph Visualization"
[//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"
[//end]: # "Autogenerated link references"

View File

@@ -20,7 +20,10 @@ Remember, with `CTRL/CMD+click` on a wikilink you can navigate to the note, or c
## Support for sections
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
- If it's an external file, `[your link will need the filename](other-file.md#that-section-I-want-to-link-to)`, but
- if it's an anchor within the same document, `[you just need an octothorpe and the section name](#that-section-above)`.
- Doesn't matter what heading-level the anchor is; whether you're linking to an `H1` like `# MEN WALK ON MOON` or an `H2` like `## Astronauts Land on Plain`, the link syntax uses a single octothorpe: `[Walk!](#men-walk-on-moon)` and `[Land!](#astronauts-land-on-plain-collect-rocks-plant-flag)`. Autocomplete is your friend here.
## Markdown compatibility

View File

@@ -5,6 +5,7 @@
- [Frequently Asked Questions](#frequently-asked-questions)
- [Links/Graphs/BackLinks don't work. How do I enable them?](#linksgraphsbacklinks-dont-work-how-do-i-enable-them)
- [I don't want Foam enabled for all my workspaces](#i-dont-want-foam-enabled-for-all-my-workspaces)
- [I want to publish the graph view to GitHub pages or Vercel](#i-want-to-publish-the-graph-view-to-github-pages-or-vercel)
## Links/Graphs/BackLinks don't work. How do I enable them?

View File

@@ -6,11 +6,11 @@ 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)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Extensions For Additional Features
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
These extensions are not defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
- [Emojisense](https://marketplace.visualstudio.com/items?itemName=bierner.emojisense)
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)

View File

@@ -1,8 +1,10 @@
# Using Foam
Foam is a collection VS Code extensions and recipes that power up the editor into a full-blown note taking system.
This folder contains user documentation describing how to get started using Foam, what its main features are, and strategies for getting the most out of Foam.
The full docs are included in the `foam-template` repo that most users start from.
Foam is a collection VS Code extensions and recipes that power up the editor
into a full-blown note taking system. This folder contains user documentation
describing how to get started using Foam, what its main features are, and
strategies for getting the most out of Foam. The full docs are included in the
`foam-template` repo that most users start from.
> See also [[frequently-asked-questions]].
@@ -12,7 +14,7 @@ The full docs are included in the `foam-template` repo that most users start fro
- [[recommended-extensions]]
- [[creating-new-notes]]
- [[write-notes-in-foam]]
- [[sync-notes-with-soruce-control]]
- [[sync-notes-with-source-control]]
- [[keyboard-shortcuts]]
## Features
@@ -55,7 +57,7 @@ See [[publishing]] for more details.
[recommended-extensions]: getting-started/recommended-extensions.md "Recommended Extensions"
[creating-new-notes]: getting-started/creating-new-notes.md "Creating New Notes"
[write-notes-in-foam]: getting-started/write-notes-in-foam.md "Writing Notes"
[sync-notes-with-soruce-control]: getting-started/sync-notes-with-soruce-control.md "Sync notes with source control"
[sync-notes-with-source-control]: getting-started/sync-notes-with-source-control.md "Sync notes with source control"
[keyboard-shortcuts]: getting-started/keyboard-shortcuts.md "Keyboard Shortcuts"
[wikilinks]: features/wikilinks.md "Wikilinks"
[tags]: features/tags.md "Tags"

View File

@@ -50,7 +50,7 @@ In our case, we'll be using the latter tag to wrap our {% raw %}`{{ content }}`{
You may have noticed that we only made modifications to the template `_layouts/page.html`, which means that `_layouts/home.html` won't have KaTeX support. If you wan't to render math in Foam's home page, you'll need to make the same modifications to `_layouts/home.html` as well.
Finally, if all goes well, then our site hosted on Vercel will support rendering math equations with KaTeX after commiting these changes to GitHub. Here's a demo of the default template with KaTeX support: [Foam Template with KaTeX support](https://foam-template.vercel.app/).
Finally, if all goes well, then our site hosted on Vercel will support rendering math equations with KaTeX after committing these changes to GitHub. Here's a demo of the default template with KaTeX support: [Foam Template with KaTeX support](https://foam-template.vercel.app/).
[//begin]: # "Autogenerated link references for markdown compatibility"
[math-support-with-mathjax]: math-support-with-mathjax.md "Math Support"

View File

@@ -65,7 +65,7 @@ gem "jekyll-katex" # Optional, the package that enables KaTeX math rendering
Besides adding the plugin `jekyll-katex` in `_config.yml` and `Gemfile`, we'll also have to follow the guides in [[math-support-with-katex]] to let our site fully support using KaTeX to render math equations.
### Commiting changes to GitHub repo
### Committing changes to GitHub repo
Finally, commit the newly created files to GitHub.

View File

@@ -2,8 +2,12 @@
This #recipe allows you to paste images on to your notes.
You can directly link and paste images that are copied to the clipboard using either the [Paste
Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
extension, or the [Markdown Image](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) extension.
VScode (since
[1.79](https://code.visualstudio.com/updates/v1_79#_copy-external-media-files-into-workspace-on-drop-or-paste-for-markdown))
now has the ability to paste images from the clipboard, or drag-and-drop image
files, directly into markdown documents. The file will be created in the
workspace, and a link generated in Markdown format.
The former does not have MDX support (yet), the latter does.
VSCode settings under `Markdown > Copy Files` and `Markdown > Editor > Drop` can
be used to configure where the files get placed in your workspace, how they're
named, how conflicts with existing files are handled, and more.

View File

@@ -6,7 +6,7 @@ With this #recipe you can create notes on your iOS device, which will automatica
* You use [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) to manage your notes
* You wish to adopt a practice such as [A writing inbox for transient and incomplete notes](https://notes.andymatuschak.org/A%20writing%20inbox%20for%20transient%20and%20incomplete%20notes)
* You wish to use [Shorcuts](https://support.apple.com/guide/shortcuts/welcome/ios) to capture quick notes into your Foam notes from your iOS device
* You wish to use [Shortcuts](https://support.apple.com/guide/shortcuts/welcome/ios) to capture quick notes into your Foam notes from your iOS device
## Other tools

View File

@@ -0,0 +1,43 @@
# Markup Converter
This #recipe allows you to convert any document into Markdown for storing them in your notes.
We will be using [Pandoc](https://pandoc.org/), a popular universal document converter. It can convert documents in Microsoft Word, HTML, LaTeX, and many other formats to various formats including markdown and many others.
## Instructions
We will go through the example of converting Microsoft Word documents to Markdown. For detailed instructions on how to use Pandoc, please refer to the [Pandoc documentation](https://pandoc.org/MANUAL.html).
1. [Install Pandoc](https://pandoc.org/installing.html)
1. Open the terminal of your choice and verify that Pandoc is installed by running `pandoc --version`
1. Copy the Microsoft Word documents that you want to convert into a new folder
1. Change the current directory to the folder containing the Microsoft Word documents
1. Copy one of the following commands (based on your operating system) into your terminal and press `Enter` to run
### Linux and macOS (Bash)
```bash
find -name "*.docx" -type f -exec sh -c '
for f; do
pandoc --extract-media=./ -f docx -t markdown -o "${f%.*}.md" "$f"
done
' find-sh {} +
```
### Windows (PowerShell)
```powershell
Get-ChildItem . -Filter *.docx |
Foreach-Object {
pandoc --extract-media=./ --from docx --to markdown $_ -o $_.Name.Replace('.docx', '.md')
}
```
### Relevant Configurations
[Pandoc](https://pandoc.org/) accepts a range of command line arguments to control the conversion process. Here, we'll mention a few that are relevant to the example above.
- `--extract-media=./` is used to extract the images from the Microsoft Word documents and store them in a subfolder named `media`
- `-t markdown` converts the Microsoft Word documents to [Pandocs Markdown](https://pandoc.org/MANUAL.html#pandocs-markdown). You can also use `-t gfm` to convert to [GitHub Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github)
Note that you may want to review the converted Markdown files to ensure that the conversion was successful. Then, You may want to delete the original Microsoft Word documents.

View File

@@ -24,6 +24,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
- Introduction to Zettelkasten [[todo]]
- Clip webpages with [[web-clipper]]
- Convert Microsoft Word files into Markdown with [[markup-converter]]
## Discover
@@ -74,7 +75,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
- Publish using community templates
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
- [foamy-nextjs](https://github.com/yenly/foamy-nextjs) by [@yenly](https://github.com/yenly)
- Make the site your own by [[publish-to-github]].
- Render math symbols, by either
- adding client-side [[math-support-with-mathjax]] to the default [[publish-to-github-pages]] site
@@ -110,6 +111,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[how-to-write-recipes]: how-to-write-recipes.md "How to Write Recipes"
[todo]: ../../dev/todo.md "Todo"
[web-clipper]: web-clipper.md "Web Clipper"
[markup-converter]: markup-converter.md "Markup Converter"
[graph-visualization]: ../features/graph-visualization.md "Graph Visualization"
[backlinking]: ../features/backlinking.md "Backlinking"
[unlinked-references]: ../../dev/unlinked-references.md "Unlinked references (stub)"

View File

@@ -53,7 +53,7 @@ If such an app was worth building, it would have to have the following features:
- Instant loading and syncing for quick notes
- Sleek, simple, beautifully designed user experience.
- Ability to search and navigate forward links and back links (onlly in paid GitJournal version)
- Ability to search and navigate forward links and back links (only in paid GitJournal version)
- Killer feature that makes it the best note taking tool for Foam (?)
Given the effort vs reward ratio, it's a low priority for core team, but if someone wants to work on this, we can provide support! Talk to us on the #mobile-apps channel on [Foam Discord](https://foambubble.github.io/join-discord/w).

View File

@@ -44,7 +44,7 @@ When editing a file, you can easily navigate `[[links]]` by hovering over them t
You can view a page's backlinks using either of the following techniques:
1. Expanding the file's node in the `Repositories` tree, since it's child nodes will represent backlinks. This makes it easy to browse your pages and their backlinks in a single hierachical view.
1. Expanding the file's node in the `Repositories` tree, since it's child nodes will represent backlinks. This makes it easy to browse your pages and their backlinks in a single hierarchical view.
1. Opening a file, and then viewing it's backlinks list at the bottom of the editor view. This makes it easy to read a page and then see its backlinks in a contextually rich way.

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.20.5"
"version": "0.25.11"
}

View File

@@ -16,16 +16,16 @@
"reset": "yarn && yarn clean && yarn build",
"clean": "lerna run clean",
"build": "lerna run build",
"test": "yarn workspace foam-vscode test",
"test": "yarn workspace foam-vscode test --stream",
"lint": "lerna run lint",
"watch": "lerna run watch --concurrency 20 --stream"
},
"devDependencies": {
"all-contributors-cli": "^6.16.1",
"lerna": "^3.22.1"
"lerna": "^6.4.1"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"husky": {
"hooks": {
@@ -33,10 +33,11 @@
}
},
"prettier": {
"arrowParens": "avoid",
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"dependencies": {}
}
}

View File

@@ -4,6 +4,201 @@ 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.25.11] - 2024-03-18
Fixes and Improvements:
- Actually fixed bug in graph computation (#1345)
## [0.25.10] - 2024-03-18
Fixes and Improvements:
- Fixed bug in graph computation (#1345)
## [0.25.9] - 2024-03-17
Fixes and Improvements:
- Improved note creation from placeholder (#1344)
## [0.25.8] - 2024-02-21
Fixes and Improvements:
- Upgraded dataformat to improve support for daily note naming (#1326 - thanks @rcyeh)
## [0.25.7] - 2024-01-16
Fixes and Improvements:
- Modifies url encoding to target only the filename and skip spaces (#1322 - thanks @MABruni)
- Minor tweak to quick action menu with suggestions for section name
## [0.25.6] - 2023-12-13
Fixes and Improvements:
- Fixed wikilink definition encoding (#1311 - thanks @MABruni)
## [0.25.5] - 2023-11-30
Fixes and Improvements:
- Using note title in preview (#1309)
## [0.25.4] - 2023-09-19
Fixes and Improvements:
- Added support for linking sections within same document (#1289)
- Fixed note embedding bug (#1286 - thanks @badsketch)
## [0.25.3] - 2023-09-07
Fixes and Improvements:
- Fixed incorrect handling of embedding of non-existing notes (#1283 - thanks @badsketch)
- Introduced Note Embedding Sytanx (#1281 - thanks @badsketch)
- Attachments are not considered when computing orphan notes (#1242)
## [0.25.2] - 2023-09-02
Fixes and Improvements:
- Added content-only embed styles (#1279 - thanks @badsketch)
- Added expand-all button to tree views (#1276)
## [0.25.1] - 2023-08-23
Fixes and Improvements:
- Added support for path parameter in filter (#1250)
- Added grouping and filtering to tag explorer (#1275)
- Added new setting to control note embedding (#1273 - thanks @badsketch)
- Added last week's days to snippets (#1248 - thanks @jimgraham)
Internal:
- Updated jest to v29 (#1271 - thanks @nicholas-l)
- Improved test cleanup and management (#1274)
## [0.25.0] - 2023-06-30
Features:
- Support for multiple extensions and custom default extension (#1235)
- Added `FOAM_TITLE_SAFE` template variable (#1232)
Fixes and Improvements:
- Connections panel tweaks (#1233)
## [0.24.0] - 2023-05-19
Features:
- Converted backlinks panel into more general connections panel (#1230)
Internal:
- Improved janitor code (#1228)
- Refactored code related to tree view panels (#1226)
- Lint and cleanup (#1224)
## [0.23.0] - 2023-05-06
Features:
- Added notes explorer (#1223)
Fixes and Improvements:
- Enabled tag completion in front matter (#1191 - thanks @jimgraham)
- Various improvements to tree views (#1220)
## [0.22.2] - 2023-04-20
Fixes and Improvements:
- Support to show placeholders only for open file in panel (#1201, #988)
- Show note block in panels on hover preview (#1201, #800)
- Show tag references within tag explorer (#1201)
- Improved structure of view related commands (#1201)
- Ignore `.foam` directory
## [0.22.1] - 2023-04-15
Fixes and Improvements:
- Allow the `#` char to trigger tag autocompletion (#1192, #1189 - thanks @jimgraham)
## [0.22.0] - 2023-04-15
Fixes and Improvements:
- Added support for deep tag hierarchy in Tag Explorer panel (#1134, #1194)
- Consolidated and improved Backlinks, Placeholders and Orphans panels (#1196)
- Fixed note resolution when using template without defined path (#1197)
## [0.21.4] - 2023-04-14
Fixes and Improvements:
- Fixed issue with generated daily note template due to path escape (#1188, #1190)
## [0.21.3] - 2023-04-12
Fixes and Improvements:
- Fixed relative path from workspace root in templates (#1188)
## [0.21.2] - 2023-04-11
Fixes and Improvements:
- Fixed embed with relative paths (#1168, #1170)
- Improved multi-root folder support for daily notes (#1126, #1175)
- Improved use of tag completion (#1183 - thanks @jimgraham)
- Fixed relative path use in note creation when using templates (#1170)
Internal:
- Sync user docs with foam-template docs (#1180 - thanks @infogulch)
## [0.21.1] - 2023-02-24
Fixes and Improvements:
- Fixed note creation from placeholder (#1172)
## [0.21.0] - 2023-02-16
Features:
- Added support for filters for the `foam-vscode.open-resource` command (#1161)
## [0.20.8] - 2023-02-10
Internal:
- Updated most dependencies (#1160)
## [0.20.7] - 2023-01-31
Fixes and Improvements:
- Inform the user that directory renaming is not supported (#1143)
- Fixed extra `web` directory in published extension (#1152 - thanks @piousdeer)
## [0.20.6] - 2023-01-21
Fixes and Improvements:
- Updated minimum VS Code version to 1.70.0 (#1140)
- Fixed preview links with sections (#1135 - thanks @badsketch)
- Added setting for creating new notes in root or current dir (#1142)
## [0.20.5] - 2023-01-04
Fixes and Improvements:
@@ -43,7 +238,7 @@ Fixes and Improvements:
## [0.20.0] - 2022-09-30
New Features:
Features:
- Added `foam-vscode.create-note` command, which can be very customized for several use cases (#1076)
@@ -99,7 +294,7 @@ Internal:
## [0.19.0] - 2022-07-07
New Features:
Features:
- Support for attachments (PDF) and images (#1027)
- Support for opening day notes for other days as well (#1026, thanks @alper)
@@ -579,7 +774,7 @@ Fixes and Improvements:
## [0.7.1] - 2020-11-27
New Feature:
Features:
- Foam logging can now be inspected in VsCode Output panel (#377)
@@ -591,7 +786,7 @@ Fixes and Improvements:
## [0.7.0] - 2020-11-25
New Features:
Features:
- Foam stays in sync with changes in notes
- Dataviz: Added multiple selection in graph (shift+click on node)
@@ -603,7 +798,7 @@ Fixes and Improvements:
## [0.6.0] - 2020-11-19
New features:
Features:
- Added command to create notes from templates (#115 - Thanks @ingalless)
@@ -618,7 +813,7 @@ Fixes and Improvements:
## [0.5.0] - 2020-11-09
New features:
Features:
- Added tags panel (#311)
@@ -632,7 +827,7 @@ Fixes and Improvements:
## [0.4.0] - 2020-10-28
New features:
Features:
- Added `Foam: Show Graph` command
- Added date snippets (/+1d, ...) to create wikilinks to dates in daily note format
@@ -660,7 +855,7 @@ Fixes and improvements:
## [0.3.0] - 2020-07-25
New features:
Features:
- [Daily Notes](https://foambubble.github.io/foam/daily-notes)
- [Janitor](https://foambubble.github.io/foam/workspace-janitor) for updating headings and link references across your workspace

View File

@@ -0,0 +1,33 @@
The MIT Licence (MIT)
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Where noted, some code uses the following license:
MIT License
Copyright (c) 2015 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

View File

@@ -2,7 +2,7 @@
[![Version](https://img.shields.io/visual-studio-marketplace/v/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Downloads](https://img.shields.io/visual-studio-marketplace/d/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode?label=VS%20Code%20Installs)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)

View File

@@ -8,32 +8,26 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.20.5",
"version": "0.25.11",
"license": "MIT",
"publisher": "foam",
"engines": {
"vscode": "^1.47.1"
"vscode": "^1.70.0"
},
"icon": "assets/icon/FOAM_ICON_256.png",
"categories": [
"Other"
],
"activationEvents": [
"workspaceContains:.vscode/foam.json",
"onView:foam-vscode.tags-explorer",
"onCommand:foam-vscode.update-wikilinks",
"onCommand:foam-vscode.open-daily-note",
"onCommand:foam-vscode.update-graph",
"onCommand:foam-vscode.open-random-note",
"onCommand:foam-vscode.janitor",
"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"
"workspaceContains:.vscode/foam.json"
],
"main": "./out/extension.js",
"capabilities": {
"untrustedWorkspaces": {
"supported": "limited",
"description": "No expressions are allowed in filters."
}
},
"contributes": {
"markdown.markdownItPlugins": true,
"markdown.previewStyles": [
@@ -62,28 +56,34 @@
"views": {
"explorer": [
{
"id": "foam-vscode.backlinks",
"name": "Backlinks",
"id": "foam-vscode.connections",
"name": "Connections",
"icon": "$(references)",
"contextualTitle": "Backlinks"
"contextualTitle": "Foam"
},
{
"id": "foam-vscode.tags-explorer",
"name": "Tag Explorer",
"icon": "$(tag)",
"contextualTitle": "Tags Explorer"
"contextualTitle": "Foam"
},
{
"id": "foam-vscode.notes-explorer",
"name": "Notes",
"icon": "$(notebook)",
"contextualTitle": "Foam"
},
{
"id": "foam-vscode.orphans",
"name": "Orphans",
"icon": "$(debug-gripper)",
"contextualTitle": "Orphans"
"contextualTitle": "Foam"
},
{
"id": "foam-vscode.placeholders",
"name": "Placeholders",
"icon": "$(debug-disconnect)",
"contextualTitle": "Placeholders"
"contextualTitle": "Foam"
}
]
},
@@ -93,8 +93,8 @@
"contents": "No tags found. Notes that contain tags will show up here. You may add tags to a note with a hashtag (#tag) or by adding a tag list to the front matter (tags: tag1, tag2)."
},
{
"view": "foam-vscode.backlinks",
"contents": "No backlinks found for selected resource."
"view": "foam-vscode.connections",
"contents": "Nothing found for the selected resource and the current filter."
},
{
"view": "foam-vscode.orphans",
@@ -102,29 +102,99 @@
},
{
"view": "foam-vscode.placeholders",
"contents": "No placeholders found. Pending links and notes without content will show up here."
"contents": "No placeholders found for selected resource or workspace."
}
],
"menus": {
"view/title": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == false",
"command": "foam-vscode.views.connections.show:backlinks",
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'all links'",
"group": "navigation"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
"command": "foam-vscode.views.connections.show:forward-links",
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'backlinks'",
"group": "navigation"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == false",
"command": "foam-vscode.views.connections.show:all-links",
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'forward links'",
"group": "navigation"
},
{
"command": "foam-vscode.group-placeholders-off",
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == true",
"command": "foam-vscode.views.orphans.group-by:folder",
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'off'",
"group": "navigation"
},
{
"command": "foam-vscode.views.orphans.group-by:off",
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'all'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.show:all",
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'for-current-file'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.group-by:folder",
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'off'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.group-by:off",
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"when": "view == foam-vscode.tags-explorer",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'all'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.show:all",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'for-current-file'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'off'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.group-by:off",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"when": "view == foam-vscode.placeholders",
"group": "navigation"
},
{
"command": "foam-vscode.views.notes-explorer.show:notes",
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'all'",
"group": "navigation"
},
{
"command": "foam-vscode.views.notes-explorer.show:all",
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'notes-only'",
"group": "navigation"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"when": "view == foam-vscode.notes-explorer",
"group": "navigation"
}
],
@@ -138,19 +208,75 @@
"when": "false"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"command": "foam-vscode.views.connections.show:all-links",
"when": "false"
},
{
"command": "foam-vscode.group-orphans-off",
"command": "foam-vscode.views.connections.show:backlinks",
"when": "false"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"command": "foam-vscode.views.connections.show:forward-links",
"when": "false"
},
{
"command": "foam-vscode.group-placeholders-off",
"command": "foam-vscode.views.orphans.group-by:folder",
"when": "false"
},
{
"command": "foam-vscode.views.orphans.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.show:all",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.group-by:folder",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.show:all",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"when": "false"
},
{
"command": "foam-vscode.views.notes-explorer.show:all",
"when": "false"
},
{
"command": "foam-vscode.views.notes-explorer.show:notes",
"when": "false"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"when": "false"
},
{
@@ -185,8 +311,8 @@
"title": "Foam: Show graph"
},
{
"command": "foam-vscode.update-wikilinks",
"title": "Foam: Update Markdown Reference List"
"command": "foam-vscode.update-wikilink-definitions",
"title": "Foam: Update wikilink definitions"
},
{
"command": "foam-vscode.open-daily-note",
@@ -221,25 +347,95 @@
"title": "Foam: Open Resource"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"title": "Foam: Group Orphans By Folder",
"command": "foam-vscode.views.orphans.group-by:folder",
"title": "Group By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-orphans-off",
"title": "Foam: Don't Group Orphans",
"command": "foam-vscode.views.connections.show:backlinks",
"title": "Show Backlinks",
"icon": "$(arrow-left)"
},
{
"command": "foam-vscode.views.connections.show:forward-links",
"title": "Show Links",
"icon": "$(arrow-right)"
},
{
"command": "foam-vscode.views.connections.show:all-links",
"title": "Show All",
"icon": "$(arrow-swap)"
},
{
"command": "foam-vscode.views.orphans.group-by:off",
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"title": "Foam: Group Placeholders By Folder",
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
"title": "Show tags in current file",
"icon": "$(file)"
},
{
"command": "foam-vscode.views.tags-explorer.show:all",
"title": "Show tags in workspace",
"icon": "$(files)"
},
{
"command": "foam-vscode.views.tags-explorer.group-by:folder",
"title": "Group By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-placeholders-off",
"title": "Foam: Don't Group Placeholders",
"command": "foam-vscode.views.tags-explorer.group-by:off",
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"title": "Show placeholders in current file",
"icon": "$(file)"
},
{
"command": "foam-vscode.views.placeholders.show:all",
"title": "Show placeholders in workspace",
"icon": "$(files)"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"title": "Group By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.views.placeholders.group-by:off",
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.notes-explorer.show:all",
"title": "Show all resources",
"icon": "$(files)"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.notes-explorer.show:notes",
"title": "Show only notes",
"icon": "$(file)"
},
{
"command": "foam-vscode.create-new-template",
"title": "Foam: Create New Template"
@@ -297,6 +493,29 @@
"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.files.notesExtensions": {
"type": "string",
"default": "",
"description": "Space separated list of extra file extensions that will be considered text notes (e.g. 'mdx txt markdown')"
},
"foam.files.defaultNoteExtension": {
"type": "string",
"default": "md",
"description": "The default extension for new notes"
},
"foam.files.newNotePath": {
"type": "string",
"default": "root",
"description": "Specifies where to create a new note. It is overruled by the template or command arguments",
"enum": [
"root",
"currentDir"
],
"enumDescriptions": [
"Use the root of the workspace",
"Use the directory of the file in the current editor"
]
},
"foam.logging.level": {
"type": "string",
"default": "info",
@@ -368,21 +587,6 @@
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the orphans report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
},
"foam.orphans.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group orphans report entries by."
},
"foam.placeholders.exclude": {
"type": [
"array"
@@ -390,21 +594,6 @@
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the placeholders report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
},
"foam.placeholders.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group blank note report entries by."
},
"foam.dateSnippets.afterCompletion": {
"type": "string",
"default": "createNote",
@@ -420,10 +609,21 @@
],
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
},
"foam.preview.embedNoteInContainer": {
"type": "boolean",
"default": true,
"description": "Wrap embedded notes in a container when displayed in preview panel"
"foam.preview.embedNoteType": {
"type": "string",
"default": "full-card",
"enum": [
"full-inline",
"full-card",
"content-inline",
"content-card"
],
"enumDescriptions": [
"Include the section with title and style inline",
"Include the section with title and style it within a container",
"Include the section without title and style inline",
"Include the section without title and style it within a container"
]
},
"foam.graph.titleMaxLength": {
"type": "number",
@@ -456,7 +656,7 @@
"test:unit": "node ./out/test/run-tests.js --unit",
"pretest:e2e": "yarn build",
"test:e2e": "node ./out/test/run-tests.js --e2e",
"lint": "tsdx lint src",
"lint": "dts lint src",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
@@ -470,42 +670,41 @@
},
"devDependencies": {
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^29.5.3",
"@types/lodash": "^4.14.157",
"@types/markdown-it": "^12.0.1",
"@types/micromatch": "^4.0.1",
"@types/node": "^13.11.0",
"@types/picomatch": "^2.2.1",
"@types/remove-markdown": "^0.1.1",
"@types/vscode": "^1.47.1",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"esbuild": "^0.14.45",
"eslint": "^6.8.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^25.3.0",
"glob": "^7.1.6",
"@types/vscode": "^1.70.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"dts-cli": "^1.6.3",
"esbuild": "^0.17.7",
"eslint": "^8.33.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"husky": "^4.2.5",
"jest": "^26.2.2",
"jest-extended": "^0.11.5",
"jest": "^29.6.2",
"jest-extended": "^3.2.3",
"markdown-it": "^12.0.4",
"micromatch": "^4.0.2",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"tsdx": "^0.13.2",
"ts-jest": "^29.1.1",
"tslib": "^2.0.0",
"typescript": "^3.9.5",
"typescript": "^4.9.5",
"vscode-test": "^1.3.0",
"wait-for-expect": "^3.0.2"
},
"dependencies": {
"dateformat": "^3.0.3",
"dateformat": "4.5.1",
"detect-newline": "^3.1.0",
"github-slugger": "^1.4.0",
"gray-matter": "^4.0.2",
"lodash": "^4.17.21",
"lru-cache": "^7.12.0",
"lru-cache": "^7.14.1",
"markdown-it-regex": "^0.2.0",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",
@@ -513,6 +712,12 @@
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2",
"yaml": "^1.10.0"
"yaml": "^2.2.2"
},
"__metadata": {
"id": "b85c6625-454b-4b61-8a22-c42f3d0f2e1e",
"publisherDisplayName": "Foam",
"publisherId": "34339645-24f0-4619-9917-12157fd92446",
"isPreReleaseVersion": false
}
}

View File

@@ -361,7 +361,7 @@ describe('SnippetParser', () => {
assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');
});
test('Parser, choise marker', () => {
test('Parser, choice marker', () => {
const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');
assert.strictEqual(placeholders.length, 1);

View File

@@ -1,43 +0,0 @@
import os from 'os';
import detectNewline from 'detect-newline';
import { Position } from '../model/position';
import { Range } from '../model/range';
export interface TextEdit {
range: Range;
newText: string;
}
/**
*
* @param text text on which the textEdit will be applied
* @param textEdit
* @returns {string} text with the applied textEdit
*/
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
const eol = detectNewline(text) || os.EOL;
const lines = text.split(eol);
const characters = text.split('');
const startOffset = getOffset(lines, textEdit.range.start, eol);
const endOffset = getOffset(lines, textEdit.range.end, eol);
const deleteCount = endOffset - startOffset;
const textToAppend = `${textEdit.newText}`;
characters.splice(startOffset, deleteCount, textToAppend);
return characters.join('');
};
const getOffset = (
lines: string[],
position: Position,
eol: string
): number => {
const eolLen = eol.length;
let offset = 0;
let i = 0;
while (i < position.line && i < lines.length) {
offset = offset + lines[i].length + eolLen;
i++;
}
return offset + Math.min(position.character, lines[i]?.length ?? 0);
};

View File

@@ -1,7 +1,7 @@
import matter from 'gray-matter';
import { TextEdit } from './apply-text-edit';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import { TextEdit } from '../services/text-edit';
import { getHeadingFromFileName } from '../utils';
export const generateHeading = async (

View File

@@ -141,7 +141,7 @@ describe('generateLinkReferences', () => {
newText: textForNote(
`
[//begin]: # "Autogenerated link references for markdown compatibility"
[Note being refered as angel]: <Note being refered as angel> "Note being refered as angel"
[Note being referred as angel]: <Note being referred as angel> "Note being referred as angel"
[//end]: # "Autogenerated link references"`
),
range: Range.create(3, 0, 3, 0),
@@ -183,13 +183,11 @@ describe('generateLinkReferences', () => {
const note = findBySlug('file-with-explicit-and-implicit-link-references');
const expected = {
newText: textForNote(
`[^footerlink]: https://foambubble.github.io/
[linkrefenrece]: https://foambubble.github.io/
[//begin]: # "Autogenerated link references for markdown compatibility"
`[//begin]: # "Autogenerated link references for markdown compatibility"
[first-document]: first-document "First Document"
[//end]: # "Autogenerated link references"`
),
range: Range.create(5, 0, 10, 42),
range: Range.create(8, 0, 10, 42),
};
const noteText = await _workspace.readAsMarkdown(note.uri);

View File

@@ -1,11 +1,8 @@
import { Resource } from '../model/note';
import { NoteLinkDefinition, Resource } from '../model/note';
import { Range } from '../model/range';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../services/markdown-provider';
import { createMarkdownReferences } from '../services/markdown-provider';
import { FoamWorkspace } from '../model/workspace';
import { TextEdit } from './apply-text-edit';
import { TextEdit } from '../services/text-edit';
import { Position } from '../model/position';
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
@@ -22,112 +19,62 @@ export const generateLinkReferences = async (
return null;
}
const markdownReferences = createMarkdownReferences(
const newWikilinkDefinitions = createMarkdownReferences(
workspace,
note.uri,
note,
includeExtensions
);
const beginDelimiterDef = note.definitions.find(
({ label }) => label === '//begin'
);
const endDelimiterDef = note.definitions.find(
({ label }) => label === '//end'
);
const lines = text.split(eol);
const targetRange =
beginDelimiterDef && endDelimiterDef
? Range.createFromPosition(
beginDelimiterDef.range.start,
endDelimiterDef.range.end
)
: Range.create(
lines.length - 1,
lines[lines.length - 1].length,
lines.length - 1,
lines[lines.length - 1].length
);
const newReferences =
markdownReferences.length === 0
newWikilinkDefinitions.length === 0
? ''
: [
LINK_REFERENCE_DEFINITION_HEADER,
...markdownReferences.map(stringifyMarkdownLinkReferenceDefinition),
...newWikilinkDefinitions.map(NoteLinkDefinition.format),
LINK_REFERENCE_DEFINITION_FOOTER,
].join(eol);
if (note.definitions.length === 0) {
if (newReferences.length === 0) {
return null;
}
// check if the new references match the existing references
const existingReferences = lines
.slice(targetRange.start.line, targetRange.end.line + 1)
.join(eol);
const lines = text.split(eol);
const end = Position.create(
lines.length - 1,
lines[lines.length - 1].length
);
const padding = end.character === 0 ? eol : `${eol}${eol}`;
return {
newText: `${padding}${newReferences}`,
range: Range.createFromPosition(end, end),
};
} else {
const first = note.definitions[0];
const last = note.definitions[note.definitions.length - 1];
// adjust padding based on whether there are existing definitions
// and, if not, whether we are on an empty line at the end of the file
const padding =
newWikilinkDefinitions.length === 0 || // no definitions
!Position.isEqual(targetRange.start, targetRange.end) // replace existing definitions
? ''
: targetRange.start.character > 0 // not an empty line
? `${eol}${eol}`
: eol;
let nonGeneratedReferenceDefinitions = note.definitions;
// if we have more definitions then referenced pages AND the page refers to a page
// we expect non-generated link definitions to be present
// Collect all non-generated definitions, by removing the generated ones
if (
note.definitions.length > markdownReferences.length &&
markdownReferences.length > 0
) {
// remove all autogenerated definitions
const beginIndex = note.definitions.findIndex(
({ label }) => label === '//begin'
);
const endIndex = note.definitions.findIndex(
({ label }) => label === '//end'
);
const generatedDefinitions = [...note.definitions].splice(
beginIndex,
endIndex - beginIndex + 1
);
nonGeneratedReferenceDefinitions = note.definitions.filter(
x => !generatedDefinitions.includes(x)
);
}
// When we only have explicitly defined link definitions &&
// no indication of previously defined generated links &&
// there is no reference to another page, return null
if (
nonGeneratedReferenceDefinitions.length > 0 &&
note.definitions.findIndex(({ label }) => label === '//begin') < 0 &&
markdownReferences.length === 0
) {
return null;
}
// Format link definitions for non-generated links
const nonGeneratedReferences = nonGeneratedReferenceDefinitions
.map(stringifyMarkdownLinkReferenceDefinition)
.join(eol);
const oldReferences = note.definitions
.map(stringifyMarkdownLinkReferenceDefinition)
.join(eol);
// When the newly formatted references match the old ones, OR
// when non-generated references are present, but no new ones are generated
// return null
if (
oldReferences === newReferences ||
(nonGeneratedReferenceDefinitions.length > 0 &&
newReferences === '' &&
markdownReferences.length > 0)
) {
return null;
}
let fullReferences = `${newReferences}`;
// If there are any non-generated definitions, add those to the output as well
if (
nonGeneratedReferenceDefinitions.length > 0 &&
markdownReferences.length > 0
) {
fullReferences = `${nonGeneratedReferences}${eol}${newReferences}`;
}
return {
// @todo: do we need to ensure new lines?
newText: `${fullReferences}`,
range: Range.createFromPosition(first.range!.start, last.range!.end),
};
}
return existingReferences === newReferences
? null
: {
newText: `${padding}${newReferences}`,
range: targetRange,
};
};

View File

@@ -5,7 +5,7 @@ import { FoamGraph } from './graph';
import { ResourceParser } from './note';
import { ResourceProvider } from './provider';
import { FoamTags } from './tags';
import { Logger } from '../utils/log';
import { Logger, withTiming, withTimingAsync } from '../utils/log';
export interface Services {
dataStore: IDataStore;
@@ -25,25 +25,28 @@ export const bootstrap = async (
watcher: IWatcher | undefined,
dataStore: IDataStore,
parser: ResourceParser,
initialProviders: ResourceProvider[]
initialProviders: ResourceProvider[],
defaultExtension: string = '.md'
) => {
const tsStart = Date.now();
const workspace = await FoamWorkspace.fromProviders(
initialProviders,
dataStore
const workspace = await withTimingAsync(
() =>
FoamWorkspace.fromProviders(
initialProviders,
dataStore,
defaultExtension
),
ms => Logger.info(`Workspace loaded in ${ms}ms`)
);
const tsWsDone = Date.now();
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
const graph = withTiming(
() => FoamGraph.fromWorkspace(workspace, true),
ms => Logger.info(`Graph loaded in ${ms}ms`)
);
const graph = FoamGraph.fromWorkspace(workspace, true);
const tsGraphDone = Date.now();
Logger.info(`Graph loaded in ${tsGraphDone - tsWsDone}ms`);
const tags = FoamTags.fromWorkspace(workspace, true);
const tsTagsEnd = Date.now();
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
const tags = withTiming(
() => FoamTags.fromWorkspace(workspace, true),
ms => Logger.info(`Tags loaded in ${ms}ms`)
);
watcher?.onDidChange(async uri => {
if (matcher.isMatch(uri)) {

View File

@@ -22,12 +22,7 @@ describe('Graph', () => {
const noteD = createTestNote({ uri: '/Page D.md' });
const noteE = createTestNote({ uri: '/page e.md' });
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE);
workspace.set(noteA).set(noteB).set(noteC).set(noteD).set(noteE);
const graph = FoamGraph.fromWorkspace(workspace);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
@@ -69,9 +64,7 @@ describe('Graph', () => {
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteA.uri)).toEqual([
{
@@ -95,9 +88,7 @@ describe('Graph', () => {
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
@@ -148,6 +139,21 @@ describe('Graph', () => {
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
});
it('should create inbound connections when targeting a section', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b#section 2' }],
});
const noteB = createTestNote({
uri: '/somewhere/page-b.md',
text: '## Section 1\n\n## Section 2',
});
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteB.uri).length).toEqual(1);
});
it('should support attachments', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
@@ -165,9 +171,7 @@ describe('Graph', () => {
uri: '/path/to/more/attachment-b.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentB);
ws.set(noteA).set(attachmentA).set(attachmentB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
@@ -189,9 +193,7 @@ describe('Graph', () => {
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentABis);
ws.set(noteA).set(attachmentA).set(attachmentABis);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
@@ -211,9 +213,7 @@ describe('Graph', () => {
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentABis)
.set(attachmentA);
ws.set(noteA).set(attachmentABis).set(attachmentA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
@@ -323,9 +323,7 @@ describe('Regenerating graph after workspace changes', () => {
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
ws.set(noteA).set(noteB).set(noteC);
let graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
@@ -512,9 +510,7 @@ describe('Updating graph on workspace state', () => {
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
ws.set(noteA).set(noteB).set(noteC);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);

View File

@@ -3,7 +3,7 @@ import { ResourceLink } from './note';
import { URI } from './uri';
import { FoamWorkspace } from './workspace';
import { IDisposable } from '../common/lifecycle';
import { Logger } from '../utils/log';
import { Logger, withTiming } from '../utils/log';
import { Emitter } from '../common/event';
export type Connection = {

View File

@@ -0,0 +1,35 @@
import { Range } from './range';
import { URI } from './uri';
import { ResourceLink } from './note';
/**
* Represents a location inside a resource, such as a line
* inside a text file.
*/
export interface Location<T> {
/**
* The resource identifier of this location.
*/
uri: URI;
/**
* The document range of this locations.
*/
range: Range;
/**
* The data associated to this location.
*/
data: T;
}
export abstract class Location<T> {
static create<T>(uri: URI, range: Range, data: T): Location<T> {
return { uri, range, data };
}
static forObjectWithRange<T extends { range: Range }>(
uri: URI,
obj: T
): Location<T> {
return Location.create(uri, obj.range, obj);
}
}

View File

@@ -5,6 +5,7 @@ export interface ResourceLink {
type: 'wikilink' | 'link';
rawText: string;
range: Range;
isEmbed: boolean;
}
export interface NoteLinkDefinition {
@@ -14,6 +15,19 @@ export interface NoteLinkDefinition {
range?: Range;
}
export abstract class NoteLinkDefinition {
static format(definition: NoteLinkDefinition) {
const url =
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
let text = `[${definition.label}]: ${url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
return text;
}
}
export interface Tag {
label: string;
range: Range;
@@ -52,6 +66,10 @@ export abstract class Resource {
return a.title.localeCompare(b.title);
}
public static sortByPath(a: Resource, b: Resource) {
return a.uri.path.localeCompare(b.uri.path);
}
public static isResource(thing: any): thing is Resource {
if (!thing) {
return false;

View File

@@ -69,4 +69,8 @@ export abstract class Range {
static isBefore(a: Range, b: Range): number {
return a.start.line - b.start.line || a.start.character - b.start.character;
}
static toString(range: Range): string {
return `${range.start.line}:${range.start.character} - ${range.end.line}:${range.end.character}`;
}
}

View File

@@ -26,7 +26,8 @@ import * as pathUtils from '../utils/path';
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
const _regexp =
/^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
export class URI {
readonly scheme: string;

View File

@@ -87,6 +87,13 @@ describe('Workspace resources', () => {
const res = ws.find('test-file#my-section');
expect(res.uri.fragment).toEqual('my-section');
});
it('should find absolute files even when no basedir is provided', () => {
const noteA = createTestNote({ uri: '/a/path/to/file.md' });
const ws = createTestWorkspace().set(noteA);
expect(ws.find('/a/path/to/file.md').uri.path).toEqual(noteA.uri.path);
});
});
describe('Identifier computation', () => {
@@ -100,10 +107,7 @@ describe('Identifier computation', () => {
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
@@ -120,10 +124,7 @@ describe('Identifier computation', () => {
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
'to/page-a#section name'
@@ -169,18 +170,14 @@ describe('Identifier computation', () => {
});
it('should ignore elements from the exclude list', () => {
const workspace = new FoamWorkspace();
const workspace = new FoamWorkspace('.md');
const noteA = createTestNote({ uri: '/path/to/note-a.md' });
const noteB = createTestNote({ uri: '/path/to/note-b.md' });
const noteC = createTestNote({ uri: '/path/to/note-c.md' });
const noteD = createTestNote({ uri: '/path/to/note-d.md' });
const noteABis = createTestNote({ uri: '/path/to/another/note-a.md' });
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD);
workspace.set(noteA).set(noteB).set(noteC).set(noteD);
expect(workspace.getIdentifier(noteABis.uri)).toEqual('another/note-a');
expect(
workspace.getIdentifier(noteABis.uri, [noteB.uri, noteA.uri])

View File

@@ -22,6 +22,11 @@ export class FoamWorkspace implements IDisposable {
*/
private _resources: Map<string, Resource> = new Map();
/**
* @param defaultExtension: The default extension for notes in this workspace (e.g. `.md`)
*/
constructor(public defaultExtension: string = '.md') {}
registerProvider(provider: ResourceProvider) {
this.providers.push(provider);
}
@@ -67,14 +72,16 @@ export class FoamWorkspace implements IDisposable {
public listByIdentifier(identifier: string): Resource[] {
const needle = normalize('/' + identifier);
const mdNeedle =
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
const resources = [];
getExtension(needle) !== this.defaultExtension
? needle + this.defaultExtension
: undefined;
const resources: Resource[] = [];
for (const key of this._resources.keys()) {
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
if (key.endsWith(mdNeedle) || key.endsWith(needle)) {
resources.push(this._resources.get(normalize(key)));
}
}
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
return resources.sort(Resource.sortByPath);
}
/**
@@ -105,7 +112,7 @@ export class FoamWorkspace implements IDisposable {
forResource.path,
amongst.map(uri => uri.path)
);
identifier = changeExtension(identifier, '.md', '');
identifier = changeExtension(identifier, this.defaultExtension, '');
if (forResource.fragment) {
identifier += `#${forResource.fragment}`;
}
@@ -121,14 +128,16 @@ export class FoamWorkspace implements IDisposable {
if (FoamWorkspace.isIdentifier(path)) {
resource = this.listByIdentifier(path)[0];
} else {
if (isAbsolute(path) || isSome(baseUri)) {
if (getExtension(path) !== '.md') {
const uri = baseUri.resolve(path + '.md');
resource = uri ? this._resources.get(normalize(uri.path)) : null;
}
if (!resource) {
const uri = baseUri.resolve(path);
resource = uri ? this._resources.get(normalize(uri.path)) : null;
const candidates = [path, path + this.defaultExtension];
for (const candidate of candidates) {
const searchKey = isAbsolute(candidate)
? candidate
: isSome(baseUri)
? baseUri.resolve(candidate).path
: null;
resource = this._resources.get(normalize(searchKey));
if (resource) {
break;
}
}
}
@@ -139,7 +148,6 @@ export class FoamWorkspace implements IDisposable {
}
public resolveLink(resource: Resource, link: ResourceLink): URI {
// TODO add tests
for (const provider of this.providers) {
if (provider.supports(resource.uri)) {
return provider.resolveLink(this, resource, link);
@@ -235,9 +243,10 @@ export class FoamWorkspace implements IDisposable {
static async fromProviders(
providers: ResourceProvider[],
dataStore: IDataStore
dataStore: IDataStore,
defaultExtension: string = '.md'
): Promise<FoamWorkspace> {
const workspace = new FoamWorkspace();
const workspace = new FoamWorkspace(defaultExtension);
await Promise.all(providers.map(p => workspace.registerProvider(p)));
const files = await dataStore.list();
await Promise.all(files.map(f => workspace.fetchAndSet(f)));

View File

@@ -3,17 +3,15 @@ import { URI } from '../model/uri';
import { FoamWorkspace } from '../model/workspace';
import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
import { getFoamVsCodeConfig } from '../../services/config';
const attachmentExtConfig = getFoamVsCodeConfig(
'files.attachmentExtensions',
''
)
.split(' ')
.map(ext => '.' + ext.trim());
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
const attachmentExtensions = [...attachmentExtConfig, ...imageExtensions];
export const imageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.webp',
];
const asResource = (uri: URI): Resource => {
const type = imageExtensions.includes(uri.getExtension())
@@ -34,9 +32,14 @@ const asResource = (uri: URI): Resource => {
export class AttachmentResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
public readonly attachmentExtensions: string[];
constructor(attachmentExtensions: string[] = []) {
this.attachmentExtensions = [...imageExtensions, ...attachmentExtensions];
}
supports(uri: URI) {
return attachmentExtensions.includes(
return this.attachmentExtensions.includes(
uri.getExtension().toLocaleLowerCase()
);
}

View File

@@ -86,7 +86,7 @@ export class GenericDataStore implements IDataStore {
/**
* 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
* The {@link refresh} function has been added to the interface to accommodate
* this matcher, far from ideal but to be refactored later
*/
export class FileListBasedMatcher implements IMatcher {

View File

@@ -107,6 +107,7 @@ describe('MarkdownLink', () => {
type: 'link',
rawText: '[link](#section)',
range: Range.create(0, 0),
isEmbed: false,
};
const parsed = MarkdownLink.analyzeLink(link);
expect(parsed.target).toEqual('');
@@ -161,7 +162,7 @@ describe('MarkdownLink', () => {
target: 'new-link',
});
expect(edit.newText).toEqual(`[[new-link#section]]`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should rename the section only', () => {
const link = parser.parse(
@@ -172,7 +173,7 @@ describe('MarkdownLink', () => {
section: 'new-section',
});
expect(edit.newText).toEqual(`[[wikilink#new-section]]`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should rename both target and section', () => {
const link = parser.parse(
@@ -184,7 +185,7 @@ describe('MarkdownLink', () => {
section: 'new-section',
});
expect(edit.newText).toEqual(`[[new-link#new-section]]`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should be able to remove the section', () => {
const link = parser.parse(
@@ -195,7 +196,7 @@ describe('MarkdownLink', () => {
section: '',
});
expect(edit.newText).toEqual(`[[wikilink]]`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should be able to rename the alias', () => {
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
@@ -204,7 +205,7 @@ describe('MarkdownLink', () => {
alias: 'new-alias',
});
expect(edit.newText).toEqual(`[[wikilink|new-alias]]`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
});
@@ -216,7 +217,7 @@ describe('MarkdownLink', () => {
target: 'to/another-path.md',
});
expect(edit.newText).toEqual(`[link](to/another-path.md)`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should rename the section only', () => {
const link = parser.parse(
@@ -227,7 +228,7 @@ describe('MarkdownLink', () => {
section: 'section2',
});
expect(edit.newText).toEqual(`[link](to/path.md#section2)`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should rename both target and section', () => {
const link = parser.parse(
@@ -239,7 +240,7 @@ describe('MarkdownLink', () => {
section: 'section2',
});
expect(edit.newText).toEqual(`[link](to/another-path.md#section2)`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
it('should be able to remove the section', () => {
const link = parser.parse(
@@ -250,7 +251,7 @@ describe('MarkdownLink', () => {
section: '',
});
expect(edit.newText).toEqual(`[link](to/path.md)`);
expect(edit.selection).toEqual(link.range);
expect(edit.range).toEqual(link.range);
});
});
});

View File

@@ -46,16 +46,17 @@ export abstract class MarkdownLink {
const newAlias = delta.alias ?? alias ?? '';
const sectionDivider = newSection ? '#' : '';
const aliasDivider = newAlias ? '|' : '';
const embed = link.isEmbed ? '!' : '';
if (link.type === 'wikilink') {
return {
newText: `[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
selection: link.range,
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
range: link.range,
};
}
if (link.type === 'link') {
return {
newText: `[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
selection: link.range,
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
range: link.range,
};
}
throw new Error(

View File

@@ -1,8 +1,13 @@
import { createMarkdownParser, ParserPlugin } from './markdown-parser';
import {
createMarkdownParser,
getBlockFor,
ParserPlugin,
} from './markdown-parser';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { Range } from '../model/range';
import { getRandomURI } from '../../test/test-utils';
import { Position } from '../model/position';
Logger.setLevel('error');
@@ -39,6 +44,7 @@ describe('Markdown parsing', () => {
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
expect(link.isEmbed).toBeFalsy();
});
it('should detect links that have formatting in label', () => {
@@ -48,6 +54,15 @@ describe('Markdown parsing', () => {
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.isEmbed).toBeFalsy();
});
it('should detect embed links', () => {
const note = createNoteFromMarkdown('this is ![link](../doc/page-b.md)');
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.isEmbed).toBeTruthy();
});
it('should detect wikilinks', () => {
@@ -61,6 +76,16 @@ describe('Markdown parsing', () => {
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[a file]]');
expect(link.isEmbed).toBeFalsy();
});
it('should detect wikilink embeds', () => {
const note = createNoteFromMarkdown('Some content and ![[an embed]]');
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('![[an embed]]');
expect(link.isEmbed).toBeTruthy();
});
it('should detect wikilinks that have aliases', () => {
@@ -74,6 +99,7 @@ describe('Markdown parsing', () => {
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[other link | spaced]]');
expect(link.isEmbed).toBeFalsy();
});
it('should skip wikilinks in codeblocks', () => {
@@ -438,3 +464,121 @@ But with some content.
]);
});
});
describe('Block detection for lists', () => {
const md = `
- this is block 1
- this is [[block]] 2
- this is block 2.1
- this is block 3
- this is block 3.1
- this is block 3.1.1
- this is block 3.2
- this is block 4
this is a simple line
this is another simple line
`;
it('can detect block', () => {
const { block } = getBlockFor(md, 1);
expect(block).toEqual('- this is block 1');
});
it('supports nested blocks 1', () => {
const { block } = getBlockFor(md, 2);
expect(block).toEqual(`- this is [[block]] 2
- this is block 2.1`);
});
it('supports nested blocks 2', () => {
const { block } = getBlockFor(md, 5);
expect(block).toEqual(` - this is block 3.1
- this is block 3.1.1`);
});
it('returns the line if no block is detected', () => {
const { block } = getBlockFor(md, 9);
expect(block).toEqual(`this is a simple line`);
});
it('is compatible with Range object', () => {
const note = parser.parse(URI.file('/path/to/a'), md);
const { start } = note.links[0].range;
const { block } = getBlockFor(md, start);
expect(block).toEqual(`- this is [[block]] 2
- this is block 2.1`);
});
});
describe('block detection for sections', () => {
const markdown = `
# Section 1
- this is block 1
- this is [[block]] 2
- this is block 2.1
# Section 2
this is a simple line
this is another simple line
## Section 2.1
- this is block 3.1
- this is block 3.1.1
- this is block 3.2
# Section 3
# Section 4
some text
some text
`;
it('should return correct block for valid markdown string with line number', () => {
const { block, nLines } = getBlockFor(markdown, 1);
expect(block).toEqual(`# Section 1
- this is block 1
- this is [[block]] 2
- this is block 2.1
`);
expect(nLines).toEqual(5);
});
it('should return correct block for valid markdown string with position', () => {
const { block, nLines } = getBlockFor(markdown, 6);
expect(block).toEqual(`# Section 2
this is a simple line
this is another simple line
## Section 2.1
- this is block 3.1
- this is block 3.1.1
- this is block 3.2
`);
expect(nLines).toEqual(9);
});
it('should return single line for section with no content', () => {
const { block, nLines } = getBlockFor(markdown, 15);
expect(block).toEqual('# Section 3');
expect(nLines).toEqual(1);
});
it('should return till end of file for last section', () => {
const { block, nLines } = getBlockFor(markdown, 16);
expect(block).toEqual(`# Section 4
some text
some text`);
expect(nLines).toEqual(3);
});
it('should return single line for non-existing line number', () => {
const { block, nLines } = getBlockFor(markdown, 100);
expect(block).toEqual('');
expect(nLines).toEqual(1);
});
it('should return single line for non-existing position', () => {
const { block, nLines } = getBlockFor(markdown, Position.create(100, 2));
expect(block).toEqual('');
expect(nLines).toEqual(1);
});
});

View File

@@ -241,7 +241,7 @@ const sectionsPlugin: ParserPlugin = {
astPointToFoamPosition(tree.position.end).line + 1,
0
);
// Close all the remainig sections
// Close all the remaining sections
while (sectionStack.length > 0) {
const section = sectionStack.pop();
note.sections.push({
@@ -268,7 +268,7 @@ const titlePlugin: ParserPlugin = {
}
},
onDidFindProperties: (props, note) => {
// Give precendence to the title from the frontmatter if it exists
// Give precedence to the title from the frontmatter if it exists
note.title = props.title?.toString() ?? note.title;
},
onDidVisitTree: (tree, note) => {
@@ -299,18 +299,33 @@ const wikilinkPlugin: ParserPlugin = {
name: 'wikilink',
visit: (node, note, noteSource) => {
if (node.type === 'wikiLink') {
const isEmbed =
noteSource.charAt(node.position!.start.offset - 1) === '!';
const literalContent = noteSource.substring(
node.position!.start.offset!,
isEmbed
? node.position!.start.offset! - 1
: node.position!.start.offset!,
node.position!.end.offset!
);
const range = isEmbed
? Range.create(
node.position.start.line - 1,
node.position.start.column - 2,
node.position.end.line - 1,
node.position.end.column - 1
)
: astPositionToFoamRange(node.position!);
note.links.push({
type: 'wikilink',
rawText: literalContent,
range: astPositionToFoamRange(node.position!),
range,
isEmbed,
});
}
if (node.type === 'link') {
if (node.type === 'link' || node.type === 'image') {
const targetUri = (node as any).url;
const uri = note.uri.resolve(targetUri);
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
@@ -324,6 +339,7 @@ const wikilinkPlugin: ParserPlugin = {
type: 'link',
rawText: literalContent,
range: astPositionToFoamRange(node.position!),
isEmbed: literalContent.startsWith('!'),
});
}
},
@@ -408,3 +424,46 @@ const astPositionToFoamRange = (pos: AstPosition): Range =>
pos.end.line - 1,
pos.end.column - 1
);
const blockParser = unified().use(markdownParse, { gfm: true });
export const getBlockFor = (
markdown: string,
line: number | Position
): { block: string; nLines: number } => {
const searchLine = typeof line === 'number' ? line : line.line;
const tree = blockParser.parse(markdown);
const lines = markdown.split('\n');
let startLine = -1;
let endLine = -1;
// For list items, we also include the sub-lists
visit(tree, ['listItem'], (node: any) => {
if (node.position.start.line === searchLine + 1) {
startLine = node.position.start.line - 1;
endLine = node.position.end.line;
return visit.EXIT;
}
});
// For headings, we also include the sub-sections
let headingLevel = -1;
visit(tree, ['heading'], (node: any) => {
if (startLine > -1 && node.depth <= headingLevel) {
endLine = node.position.start.line - 1;
return visit.EXIT;
}
if (node.position.start.line === searchLine + 1) {
headingLevel = node.depth;
startLine = node.position.start.line - 1;
endLine = lines.length - 1; // in case it's the last section
}
});
let nLines = startLine == -1 ? 1 : endLine - startLine;
let block =
startLine == -1
? lines[searchLine] ?? ''
: lines.slice(startLine, endLine).join('\n');
return { block, nLines };
};

View File

@@ -51,10 +51,7 @@ describe('Link resolution', () => {
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b 2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
});
@@ -63,10 +60,7 @@ describe('Link resolution', () => {
const noteA = createNoteFromMarkdown('Link to [[page b]]', '/page-a.md');
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
@@ -80,10 +74,7 @@ describe('Link resolution', () => {
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.set(noteB3);
ws.set(noteA).set(noteB1).set(noteB2).set(noteB3);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteB3.uri);
@@ -97,10 +88,7 @@ describe('Link resolution', () => {
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(workspace.resolveLink(noteA, noteA.links[1])).toEqual(noteB.uri);
});
@@ -157,9 +145,7 @@ describe('Link resolution', () => {
],
});
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
noteB.uri.withFragment('section')
@@ -180,6 +166,65 @@ describe('Link resolution', () => {
noteA.uri.withFragment('section')
);
});
it('should resolve wikilinks with special characters', () => {
const ws = createTestWorkspace();
const noteA = createNoteFromMarkdown(
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
[[page /j]]`
);
const noteB = createNoteFromMarkdown(
'Note containing :',
'/dir1/page: a.md'
);
const noteC = createNoteFromMarkdown(
'Note containing %',
'/dir1/page %b%.md'
);
const noteD = createNoteFromMarkdown(
'Note containing ?',
'/dir1/page? c.md'
);
const noteE = createNoteFromMarkdown(
'Note containing ]',
'/dir1/[page] d.md'
);
const noteF = createNoteFromMarkdown(
'Note containing ^',
'/dir1/page ^e^.md'
);
const noteG = createNoteFromMarkdown(
'Note containing `',
'/dir1/page `f`.md'
);
const noteH = createNoteFromMarkdown(
'Note containing { and }',
'/dir1/page {g}.md'
);
const noteI = createNoteFromMarkdown(
'Note containing ~',
'/dir1/page ~i.md'
);
ws.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE)
.set(noteF)
.set(noteG)
.set(noteH)
.set(noteI);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteC.uri);
expect(ws.resolveLink(noteA, noteA.links[2])).toEqual(noteD.uri);
expect(ws.resolveLink(noteA, noteA.links[3])).toEqual(noteE.uri);
expect(ws.resolveLink(noteA, noteA.links[4])).toEqual(noteF.uri);
expect(ws.resolveLink(noteA, noteA.links[5])).toEqual(noteG.uri);
expect(ws.resolveLink(noteA, noteA.links[6])).toEqual(noteH.uri);
expect(ws.resolveLink(noteA, noteA.links[7])).toEqual(noteI.uri);
});
});
describe('Markdown direct links', () => {
@@ -258,10 +303,7 @@ describe('Link resolution', () => {
);
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD);
ws.set(noteA).set(noteB).set(noteC).set(noteD);
expect(ws.resolveLink(noteB, noteB.links[0])).toEqual(noteA.uri);
expect(ws.resolveLink(noteC, noteC.links[0])).toEqual(noteA.uri);
@@ -328,9 +370,78 @@ describe('Generation of markdown references', () => {
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => r.url)).toEqual([
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
it('should generate links for embedded notes that are formatted properly', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to ![[page-b]] and [[page-c]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => [decodeURIComponent(r.url), r.label])).toEqual([
['../dir2/page-b.md', 'page-b'],
['../dir3/page-c.md', 'page-c'],
]);
});
it('should not generate links for placeholders', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to ![[page-b]] and [[page-c]] and [[does-not-exist]] and ![[does-not-exist-either]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
it('should encode special characters in links', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
[[page /j]]`
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Note containing :', '/dir1/page: a.md'))
.set(createNoteFromMarkdown('Note containing %', '/dir1/page %b%.md'))
.set(createNoteFromMarkdown('Note containing ?', '/dir1/page? c.md'))
.set(createNoteFromMarkdown('Note containing ]', '/dir1/[page] d.md'))
.set(createNoteFromMarkdown('Note containing ^', '/dir1/page ^e^.md'))
.set(createNoteFromMarkdown('Note containing `', '/dir1/page `f`.md'))
.set(
createNoteFromMarkdown('Note containing { and }', '/dir1/page {g}.md')
)
.set(createNoteFromMarkdown('Note containing ~', '/dir1/page ~i.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
'../dir1/page: a.md',
'../dir1/page %b%.md',
'../dir1/page? c.md',
'../dir1/[page] d.md',
'../dir1/page ^e^.md',
'../dir1/page `f`.md',
'../dir1/page {g}.md',
'../dir1/page ~i.md',
]);
});
});

View File

@@ -12,17 +12,19 @@ import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
import { MarkdownLink } from './markdown-link';
import { IDataStore } from './datastore';
import { uniqBy } from 'lodash';
export class MarkdownResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
constructor(
private readonly dataStore: IDataStore,
private readonly parser: ResourceParser
private readonly parser: ResourceParser,
public readonly noteExtensions: string[] = ['.md']
) {}
supports(uri: URI) {
return uri.isMarkdown();
return this.noteExtensions.includes(uri.getExtension());
}
async readAsMarkdown(uri: URI): Promise<string | null> {
@@ -106,27 +108,19 @@ export class MarkdownResourceProvider implements ResourceProvider {
export function createMarkdownReferences(
workspace: FoamWorkspace,
noteUri: URI,
source: Resource | URI,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = workspace.find(noteUri);
// Should never occur since we're already in a file,
if (source?.type !== 'note') {
console.warn(
`Note ${noteUri.toString()} note found in workspace when attempting \
to generate markdown reference list`
);
return [];
}
const resource = source instanceof URI ? workspace.find(source) : source;
return source.links
const definitions = resource.links
.filter(link => link.type === 'wikilink')
.map(link => {
const targetUri = workspace.resolveLink(source, link);
const targetUri = workspace.resolveLink(resource, link);
const target = workspace.find(targetUri);
if (isNone(target)) {
Logger.warn(
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
`Link ${targetUri.toString()} in ${resource.uri.toString()} is not valid.`
);
return null;
}
@@ -135,34 +129,33 @@ to generate markdown reference list`
return null;
}
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
if (!includeExtension && relativeUri.path.endsWith('.md')) {
let relativeUri = target.uri.relativeTo(resource.uri.getDirectory());
if (
!includeExtension &&
relativeUri.path.endsWith(workspace.defaultExtension)
) {
relativeUri = relativeUri.changeExtension('*', '');
}
// Extract base path and link name separately.
const basePath = relativeUri.path.split('/').slice(0, -1).join('/');
const linkName = relativeUri.path.split('/').pop();
const encodedURL = encodeURIComponent(linkName).replace(/%20/g, ' ');
// [wikilink-text]: path/to/file.md "Page title"
return {
label:
link.rawText.indexOf('[[') > -1
? link.rawText.substring(2, link.rawText.length - 2)
: link.rawText,
url: relativeUri.path,
// embedded looks like ![[note-a]]
// regular note looks like [[note-a]]
label: link.rawText.substring(
link.isEmbed ? 3 : 2,
link.rawText.length - 2
),
url: `${basePath ? basePath + '/' : ''}${encodedURL}`,
title: target.title,
};
})
.filter(isSome)
.sort();
}
export function stringifyMarkdownLinkReferenceDefinition(
definition: NoteLinkDefinition
) {
const url =
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
let text = `[${definition.label}]: ${url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
return text;
return uniqBy(definitions, def => NoteLinkDefinition.format(def));
}

View File

@@ -0,0 +1,132 @@
import { Logger } from '../utils/log';
import { createTestNote } from '../../test/test-utils';
import { createFilter } from './resource-filter';
Logger.setLevel('error');
describe('Resource Filter', () => {
describe('Filter parameters', () => {
it('should support the path regex', () => {
const noteA = createTestNote({
uri: '/path/to/foo.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: '/path/to/bar.md',
});
const filter = createFilter({ path: 'foo' }, false);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeFalsy();
});
it('should support expressions when code execution is enabled', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
expression: 'resource.type === "type-1"',
},
true
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeFalsy();
});
it('should not allow expressions when code execution is not enabled', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
expression: 'resource.type === "type-1"',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
});
it('should support resource type', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
type: 'type-1',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeFalsy();
});
it('should support resource title', () => {
const noteA = createTestNote({
uri: 'note-a.md',
title: 'title-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
title: 'title-2',
});
const noteC = createTestNote({
uri: 'note-c.md',
title: 'another title',
});
const filter = createFilter(
{
title: '^title',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
expect(filter(noteC)).toBeFalsy();
});
});
describe('Filter operators', () => {
it('should support the OR operator', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
or: [{ type: 'type-1' }, { type: 'type-2' }],
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,80 @@
import { negate } from 'lodash';
import { Resource } from '../model/note';
export interface FilterDescriptor
extends FilterDescriptorOp,
FilterDescriptorParam {}
interface FilterDescriptorOp {
and?: FilterDescriptor[];
or?: FilterDescriptor[];
not?: FilterDescriptor;
}
interface FilterDescriptorParam {
/**
* A regex of the path to include
*/
path?: string;
/**
* A tag
*/
tag?: string;
/**
* A note type
*/
type?: string;
/**
* The title of the note
*/
title?: string;
/**
* An expression to evaluate to JS, use `resource` to reference the resource object
*/
expression?: string;
}
type ResourceFilter = (r: Resource) => boolean;
export function createFilter(
filter: FilterDescriptor,
enableCode: boolean
): ResourceFilter {
filter = filter ?? {};
const expressionFn =
enableCode && filter.expression
? resource => eval(filter.expression) // eslint-disable-line no-eval
: undefined;
return resource => {
if (expressionFn && !expressionFn(resource)) {
return false;
}
if (filter.path && !resource.uri.path.match(filter.path)) {
return false;
}
if (filter.type && resource.type !== filter.type) {
return false;
}
if (filter.title && !resource.title.match(filter.title)) {
return false;
}
if (filter.and) {
return filter.and
.map(pred => createFilter(pred, enableCode))
.every(fn => fn(resource));
}
if (filter.or) {
return filter.or
.map(pred => createFilter(pred, enableCode))
.some(fn => fn(resource));
}
if (filter.not) {
return negate(createFilter(filter.not, enableCode))(resource);
}
return true;
};
}

View File

@@ -1,6 +1,6 @@
import { Range } from '../model/range';
import { Logger } from '../utils/log';
import { applyTextEdit } from './apply-text-edit';
import { TextEdit } from './text-edit';
Logger.setLevel('error');
@@ -23,7 +23,7 @@ describe('applyTextEdit', () => {
3. this is third line
4. this is fourth line`;
const actual = applyTextEdit(text, textEdit);
const actual = TextEdit.apply(text, textEdit);
expect(actual).toBe(expected);
});
@@ -45,7 +45,7 @@ describe('applyTextEdit', () => {
3. this is third line
`;
const actual = applyTextEdit(text, textEdit);
const actual = TextEdit.apply(text, textEdit);
expect(actual).toBe(expected);
});
@@ -68,7 +68,7 @@ describe('applyTextEdit', () => {
3. this is third line
`;
const actual = applyTextEdit(text, textEdit);
const actual = TextEdit.apply(text, textEdit);
expect(actual).toBe(expected);
});

View File

@@ -0,0 +1,44 @@
import detectNewline from 'detect-newline';
import { Position } from '../model/position';
import { Range } from '../model/range';
export interface TextEdit {
range: Range;
newText: string;
}
export abstract class TextEdit {
/**
*
* @param text text on which the textEdit will be applied
* @param textEdit
* @returns {string} text with the applied textEdit
*/
public static apply(text: string, textEdit: TextEdit): string {
const eol = detectNewline.graceful(text);
const lines = text.split(eol);
const characters = text.split('');
const startOffset = getOffset(lines, textEdit.range.start, eol);
const endOffset = getOffset(lines, textEdit.range.end, eol);
const deleteCount = endOffset - startOffset;
const textToAppend = `${textEdit.newText}`;
characters.splice(startOffset, deleteCount, textToAppend);
return characters.join('');
}
}
const getOffset = (
lines: string[],
position: Position,
eol: string
): number => {
const eolLen = eol.length;
let offset = 0;
let i = 0;
while (i < position.line && i < lines.length) {
offset = offset + lines[i].length + eolLen;
i++;
}
return offset + Math.min(position.character, lines[i]?.length ?? 0);
};

View File

@@ -21,7 +21,4 @@ export function isNumeric(value: string): boolean {
}
export const hash = (text: string) =>
crypto
.createHash('sha1')
.update(text)
.digest('hex');
crypto.createHash('sha1').update(text).digest('hex');

View File

@@ -1,6 +1,8 @@
import { isSome } from './core';
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 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

@@ -89,3 +89,25 @@ export class Logger {
Logger.defaultLogger = logger;
}
}
export const withTiming = <T>(
fn: () => T,
onDidComplete: (elapsed: number) => void
): T => {
const tsStart = Date.now();
const res = fn();
const tsEnd = Date.now();
onDidComplete(tsEnd - tsStart);
return res;
};
export const withTimingAsync = async <T>(
fn: () => Promise<T>,
onDidComplete: (elapsed: number) => void
): Promise<T> => {
const tsStart = Date.now();
const res = await fn();
const tsEnd = Date.now();
onDidComplete(tsEnd - tsStart);
return res;
};

View File

@@ -1,72 +1,72 @@
import { workspace } from 'vscode';
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
import { isWindows } from './core/common/platform';
import {
cleanWorkspace,
closeEditors,
createFile,
deleteFile,
showInEditor,
withModifiedFoamConfiguration,
} from './test/test-utils-vscode';
import { fromVsCodeUri } from './utils/vsc-utils';
describe('getDailyNotePath', () => {
const date = new Date('2021-02-07T00:00:00Z');
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const isoDate = `${year}-0${month}-0${day}`;
test('Adds the root directory to relative directories', async () => {
const config = 'journal';
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(config, `${isoDate}.md`);
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
);
});
test('Uses absolute directories without modification', async () => {
const config = isWindows
? 'C:\\absolute_path\\journal'
: '/absolute_path/journal';
const expectedPath = isWindows
? `${config}\\${isoDate}.md`
: `${config}/${isoDate}.md`;
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
);
});
});
describe('Daily note template', () => {
it('Uses the daily note variables in the template', async () => {
const targetDate = new Date(2021, 8, 12);
const template = await createFile(
// eslint-disable-next-line no-template-curly-in-string
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
['.foam', 'templates', 'daily-note.md']
);
const uri = getDailyNotePath(targetDate);
await createDailyNoteIfNotExists(targetDate);
const doc = await showInEditor(uri);
const content = doc.editor.document.getText();
expect(content).toEqual('hello September 12 hello');
await deleteFile(template.uri);
});
afterAll(async () => {
await cleanWorkspace();
await closeEditors();
});
});
import { workspace } from 'vscode';
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
import { isWindows } from './core/common/platform';
import {
cleanWorkspace,
closeEditors,
createFile,
deleteFile,
showInEditor,
withModifiedFoamConfiguration,
} from './test/test-utils-vscode';
import { fromVsCodeUri } from './utils/vsc-utils';
describe('getDailyNotePath', () => {
const date = new Date('2021-02-07T00:00:00Z');
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const isoDate = `${year}-0${month}-0${day}`;
test('Adds the root directory to relative directories', async () => {
const config = 'journal';
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(config, `${isoDate}.md`);
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
);
});
test('Uses absolute directories without modification', async () => {
const config = isWindows
? 'C:\\absolute_path\\journal'
: '/absolute_path/journal';
const expectedPath = isWindows
? `${config}\\${isoDate}.md`
: `${config}/${isoDate}.md`;
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
);
});
});
describe('Daily note template', () => {
it('Uses the daily note variables in the template', async () => {
const targetDate = new Date(2021, 8, 12);
const template = await createFile(
// eslint-disable-next-line no-template-curly-in-string
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
['.foam', 'templates', 'daily-note.md']
);
const uri = getDailyNotePath(targetDate);
await createDailyNoteIfNotExists(targetDate);
const doc = await showInEditor(uri);
const content = doc.editor.document.getText();
expect(content).toEqual('hello September 12 hello');
await deleteFile(template.uri);
});
afterAll(async () => {
await cleanWorkspace();
await closeEditors();
});
});

View File

@@ -1,8 +1,6 @@
import { workspace } from 'vscode';
import dateFormat from 'dateformat';
import { focusNote } from './utils';
import { URI } from './core/model/uri';
import { toVsCodeUri } from './utils/vsc-utils';
import { NoteFactory } from './services/templates';
import { getFoamVsCodeConfig } from './services/config';
import { asAbsoluteWorkspaceUri } from './services/editor';
@@ -76,7 +74,7 @@ export function getDailyNoteFileName(date: Date): string {
* this function will create all folders in the path.
*
* @param currentDate The current date, to be used as a title.
* @returns Wether the file was created.
* @returns Whether the file was created.
*/
export async function createDailyNoteIfNotExists(targetDate: Date) {
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
@@ -86,9 +84,7 @@ export async function createDailyNoteIfNotExists(targetDate: Date) {
const templateFallbackText = `---
foam_template:
filepath: "${workspace.asRelativePath(
toVsCodeUri(pathFromLegacyConfiguration)
)}"
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
---
# ${dateFormat(targetDate, titleFormat, false)}
`;

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import { workspace, ExtensionContext, window, commands } from 'vscode';
import { MarkdownResourceProvider } from './core/services/markdown-provider';
import { bootstrap } from './core/model/foam';
@@ -5,12 +7,17 @@ import { Logger } from './core/utils/log';
import { features } from './features';
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
import { getIgnoredFilesSetting } from './settings';
import {
getAttachmentsExtensions,
getIgnoredFilesSetting,
getNotesExtensions,
} from './settings';
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';
import { getFoamVsCodeConfig } from './services/config';
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -27,11 +34,8 @@ export async function activate(context: ExtensionContext) {
// Prepare Foam
const excludes = getIgnoredFilesSetting().map(g => g.toString());
const {
matcher,
dataStore,
excludePatterns,
} = await createMatcherAndDataStore(excludes);
const { matcher, dataStore, excludePatterns } =
await createMatcherAndDataStore(excludes);
Logger.info('Loading from directories:');
for (const folder of workspace.workspaceFolders) {
@@ -46,16 +50,32 @@ export async function activate(context: ExtensionContext) {
const parserCache = new VsCodeBasedParserCache(context);
const parser = createMarkdownParser([], parserCache);
const markdownProvider = new MarkdownResourceProvider(dataStore, parser);
const attachmentProvider = new AttachmentResourceProvider();
const { notesExtensions, defaultExtension } = getNotesExtensions();
const foamPromise = bootstrap(matcher, watcher, dataStore, parser, [
markdownProvider,
attachmentProvider,
]);
const markdownProvider = new MarkdownResourceProvider(
dataStore,
parser,
notesExtensions
);
const attachmentExtConfig = getAttachmentsExtensions();
const attachmentProvider = new AttachmentResourceProvider(
attachmentExtConfig
);
const foamPromise = bootstrap(
matcher,
watcher,
dataStore,
parser,
[markdownProvider, attachmentProvider],
defaultExtension
);
// Load the features
const resPromises = features.map(f => f.activate(context, foamPromise));
const featuresPromises = features.map(feature =>
feature(context, foamPromise)
);
const foam = await foamPromise;
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
@@ -67,17 +87,32 @@ export async function activate(context: ExtensionContext) {
attachmentProvider,
commands.registerCommand('foam-vscode.clear-cache', () =>
parserCache.clear()
)
),
workspace.onDidChangeConfiguration(e => {
if (
[
'foam.files.ignore',
'foam.files.attachmentExtensions',
'foam.files.noteExtensions',
'foam.files.defaultNoteExtension',
].some(setting => e.affectsConfiguration(setting))
) {
window.showInformationMessage(
'Foam: Reload the window to use the updated settings'
);
}
})
);
const res = (await Promise.all(resPromises)).filter(r => r != null);
const feats = (await Promise.all(featuresPromises)).filter(r => r != null);
return {
extendMarkdownIt: (md: markdownit) => {
return res.reduce((acc: markdownit, r: any) => {
return feats.reduce((acc: markdownit, r: any) => {
return r.extendMarkdownIt ? r.extendMarkdownIt(acc) : acc;
}, md);
},
foam,
};
} catch (e) {
Logger.error('An error occurred while bootstrapping Foam', e);

View File

@@ -1,17 +1,14 @@
import { window, env, ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../../types';
import { removeBrackets } from '../../utils';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.copy-without-brackets',
copyWithoutBrackets
)
);
},
};
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.copy-without-brackets',
copyWithoutBrackets
)
);
}
async function copyWithoutBrackets() {
// Get the active text editor
@@ -34,5 +31,3 @@ async function copyWithoutBrackets() {
window.showInformationMessage('Successfully copied to clipboard!');
}
}
export default feature;

View File

@@ -1,28 +0,0 @@
import { commands, window } from 'vscode';
import * as editor from '../../services/editor';
describe('create-note-from-default-template command', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('can be cancelled while resolving FOAM_TITLE', async () => {
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
await commands.executeCommand(
'foam-vscode.create-note-from-default-template'
);
expect(spy).toBeCalledWith({
prompt: `Enter a title for the new note`,
value: 'Title of my New Note',
validateInput: expect.anything(),
});
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
});
});

View File

@@ -1,16 +1,8 @@
import { commands, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
import { createTemplate } from '../../services/templates';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.create-new-template',
createTemplate
)
);
},
};
export default feature;
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand('foam-vscode.create-new-template', createTemplate)
);
}

View File

@@ -1,28 +0,0 @@
import { commands, window } from 'vscode';
import * as editor from '../../services/editor';
describe('create-note-from-default-template command', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('can be cancelled while resolving FOAM_TITLE', async () => {
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
await commands.executeCommand(
'foam-vscode.create-note-from-default-template'
);
expect(spy).toBeCalledWith({
prompt: `Enter a title for the new note`,
value: 'Title of my New Note',
validateInput: expect.anything(),
});
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
});
});

View File

@@ -1,42 +0,0 @@
import { commands, window, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
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(
getDefaultTemplateUri(),
resolver,
undefined,
`---
foam_template:
name: New Note
description: Foam's default new note template
---
# \${FOAM_TITLE}
\${FOAM_SELECTED_TEXT}
`
);
}
)
);
},
};
export default feature;

View File

@@ -14,7 +14,7 @@ describe('create-note-from-template command', () => {
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(['Yes', 'No'], {
expect(spy).toHaveBeenCalledWith(['Yes', 'No'], {
placeHolder:
'No templates available. Would you like to create one instead?',
});
@@ -38,7 +38,7 @@ describe('create-note-from-template command', () => {
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
[
expect.objectContaining({ label: 'template-a.md' }),
expect.objectContaining({ label: 'template-b.md' }),
@@ -71,7 +71,7 @@ Template A
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
[
expect.objectContaining({
label: 'My Template',

View File

@@ -1,25 +1,20 @@
import { commands, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
import { askUserForTemplate, NoteFactory } from '../../services/templates';
import { Resolver } from '../../services/variable-resolver';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.create-note-from-template',
async () => {
const templateUri = await askUserForTemplate();
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.create-note-from-template',
async () => {
const templateUri = await askUserForTemplate();
if (templateUri) {
const resolver = new Resolver(new Map(), new Date());
if (templateUri) {
const resolver = new Resolver(new Map(), new Date());
await NoteFactory.createFromTemplate(templateUri, resolver);
}
await NoteFactory.createFromTemplate(templateUri, resolver);
}
)
);
},
};
export default feature;
}
)
);
}

View File

@@ -7,7 +7,15 @@ import {
deleteFile,
expectSameUri,
getUriInWorkspace,
showInEditor,
} from '../../test/test-utils-vscode';
import { fromVsCodeUri } from '../../utils/vsc-utils';
import { CREATE_NOTE_COMMAND, createNote } from './create-note';
import { Location } from '../../core/model/location';
import { Range } from '../../core/model/range';
import { ResourceLink } from '../../core/model/note';
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
import { createMarkdownParser } from '../../core/services/markdown-parser';
describe('create-note command', () => {
afterEach(() => {
@@ -20,7 +28,7 @@ describe('create-note command', () => {
.mockImplementationOnce(jest.fn(() => Promise.resolve('Test note')));
await commands.executeCommand('foam-vscode.create-note');
expect(spy).toBeCalled();
expect(spy).toHaveBeenCalled();
const target = asAbsoluteWorkspaceUri(URI.file('Test note.md'));
expectSameUri(target, window.activeTextEditor?.document.uri);
await deleteFile(target);
@@ -122,8 +130,126 @@ describe('create-note command', () => {
text: 'test ask',
onFileExists: 'ask',
});
expect(spy).toBeCalled();
expect(spy).toHaveBeenCalled();
await deleteFile(target);
});
it('supports various options to deal with relative paths', async () => {
const base = await createFile('relative path tests base file', [
'create-note-tests',
'base-file.md',
]);
await closeEditors();
await showInEditor(base.uri);
const target = getUriInWorkspace();
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.getBasename(),
text: 'test resolving from root',
onRelativeNotePath: 'resolve-from-root',
});
expect(window.activeTextEditor.document.getText()).toEqual(
'test resolving from root'
);
expectSameUri(window.activeTextEditor.document.uri, target);
await closeEditors();
await showInEditor(base.uri);
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.getBasename(),
text: 'test resolving from current dir',
onRelativeNotePath: 'resolve-from-current-dir',
});
expect(window.activeTextEditor.document.getText()).toEqual(
'test resolving from current dir'
);
expect(fromVsCodeUri(window.activeTextEditor.document.uri).path).toEqual(
fromVsCodeUri(window.activeTextEditor.document.uri)
.getDirectory()
.joinPath(target.getBasename()).path
);
await closeEditors();
await showInEditor(base.uri);
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.getBasename(),
text: 'test cancelling',
onRelativeNotePath: 'cancel',
});
expectSameUri(window.activeTextEditor.document.uri, base.uri);
await closeEditors();
await showInEditor(base.uri);
const spy = jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
await commands.executeCommand('foam-vscode.create-note', {
notePath: target.getBasename(),
text: 'test asking',
onRelativeNotePath: 'ask',
});
expect(spy).toHaveBeenCalled();
await deleteFile(base);
});
});
describe('factories', () => {
describe('forPlaceholder', () => {
it('adds the .md extension to notes created for placeholders', async () => {
await closeEditors();
const link: ResourceLink = {
type: 'wikilink',
rawText: '[[my-placeholder]]',
range: Range.create(0, 0, 0, 0),
isEmbed: false,
};
const command = CREATE_NOTE_COMMAND.forPlaceholder(
Location.forObjectWithRange(URI.file(''), link),
'.md'
);
await commands.executeCommand(command.name, command.params);
const doc = window.activeTextEditor.document;
expect(doc.uri.path).toMatch(/my-placeholder.md$/);
expect(doc.getText()).toMatch(/^# my-placeholder/);
});
it('replaces the original placeholder based on the new note identifier (#1327)', async () => {
await closeEditors();
const templateA = await createFile(
`---
foam_template:
name: 'Example Template'
description: 'An example for reproducing a bug'
filepath: '$FOAM_SLUG-world.md'
---`,
['.foam', 'templates', 'template-a.md']
);
const noteA = await createFile(`this is my [[hello]]`);
const parser = createMarkdownParser();
const res = parser.parse(noteA.uri, noteA.content);
const command = CREATE_NOTE_COMMAND.forPlaceholder(
Location.forObjectWithRange(noteA.uri, res.links[0]),
'.md',
{
templatePath: templateA.uri.path,
}
);
const results: Awaited<ReturnType<typeof createNote>> =
await commands.executeCommand(command.name, command.params);
expect(results.didCreateFile).toBeTruthy();
expect(results.uri.path.endsWith('hello-world.md')).toBeTruthy();
const newNoteDoc = window.activeTextEditor.document;
expect(newNoteDoc.uri.path).toMatch(/hello-world.md$/);
const { doc } = await showInEditor(noteA.uri);
expect(doc.getText()).toEqual(`this is my [[hello-world]]`);
});
});
});

View File

@@ -1,5 +1,4 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { URI } from '../../core/model/uri';
import {
askUserForTemplate,
@@ -7,10 +6,27 @@ import {
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';
import { CommandDescriptor } from '../../utils/commands';
import { Foam } from '../../core/model/foam';
import { Location } from '../../core/model/location';
import { MarkdownLink } from '../../core/services/markdown-link';
import { ResourceLink } from '../../core/model/note';
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
export default async function activate(
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
context.subscriptions.push(
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, args =>
createNote(args, foam)
)
);
}
interface CreateNoteArgs {
/**
@@ -34,31 +50,50 @@ interface CreateNoteArgs {
/**
* Variables to use in the text or template
*/
variables?: Map<string, string>;
variables?: { [key: string]: string };
/**
* The date used to resolve the FOAM_DATE_* variables. in YYYY-MM-DD format
*/
date?: string;
/**
* The title of the note (translates into the FOAM_TITLE variable)
*/
title?: string;
/**
* The source link that triggered the creation of the note.
* It will be updated with the appropriate identifier to the note, if necessary.
*/
sourceLink?: Location<ResourceLink>;
/**
* What to do in case the target file already exists
*/
onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel';
/**
* What to do if the new note path is relative
*/
onRelativeNotePath?:
| 'resolve-from-root'
| 'resolve-from-current-dir'
| 'ask'
| 'cancel';
}
const DEFAULT_NEW_NOTE_TEXT = `# \${FOAM_TITLE}
\${FOAM_SELECTED_TEXT}`;
async function createNote(args: CreateNoteArgs) {
export async function createNote(args: CreateNoteArgs, foam: Foam) {
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
);
if (args.title) {
resolver.define('FOAM_TITLE', args.title);
}
const text = args.text ?? DEFAULT_NEW_NOTE_TEXT;
const noteUri =
args.notePath && asAbsoluteWorkspaceUri(URI.file(args.notePath));
const noteUri = args.notePath && URI.file(args.notePath);
let templateUri: URI;
if (args.askForTemplate) {
const selectedTemplate = await askUserForTemplate();
@@ -73,40 +108,73 @@ async function createNote(args: CreateNoteArgs) {
: 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
);
const createdNote = (await fileExists(templateUri))
? await NoteFactory.createFromTemplate(
templateUri,
resolver,
noteUri,
text,
args.onFileExists
)
: await NoteFactory.createNote(
noteUri ?? (await getPathFromTitle(resolver)),
text,
resolver,
args.onFileExists,
args.onRelativeNotePath
);
if (args.sourceLink) {
const identifier = foam.workspace.getIdentifier(createdNote.uri);
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
target: identifier,
});
if (edit.newText != args.sourceLink.data.rawText) {
const updateLink = new vscode.WorkspaceEdit();
const uri = toVsCodeUri(args.sourceLink.uri);
updateLink.replace(
uri,
toVsCodeRange(args.sourceLink.range),
edit.newText
);
await vscode.workspace.applyEdit(updateLink);
}
}
return createdNote;
}
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)
);
/**
* Creates a command descriptor to create a note from the given placeholder.
*
* @param placeholder the placeholder
* @param defaultExtension the default extension (e.g. '.md')
* @param extra extra command arguments
* @returns the command descriptor
*/
forPlaceholder: (
sourceLink: Location<ResourceLink>,
defaultExtension: string,
extra: Partial<CreateNoteArgs> = {}
): CommandDescriptor<CreateNoteArgs> => {
const endsWithDefaultExtension = new RegExp(defaultExtension + '$');
const { target: placeholder } = MarkdownLink.analyzeLink(sourceLink.data);
const title = placeholder.endsWith(defaultExtension)
? placeholder.replace(endsWithDefaultExtension, '')
: placeholder;
const notePath = placeholder.endsWith(defaultExtension)
? placeholder
: placeholder + defaultExtension;
return {
name: CREATE_NOTE_COMMAND.command,
params: {
title,
notePath,
sourceLink,
...extra,
},
};
},
};
export default feature;

View File

@@ -1,5 +1,4 @@
export { default as copyWithoutBracketsCommand } from './copy-without-brackets';
export { default as createFromDefaultTemplateCommand } from './create-note-from-default-template';
export { default as createFromTemplateCommand } from './create-note-from-template';
export { default as createNewTemplate } from './create-new-template';
export { default as janitorCommand } from './janitor';

View File

@@ -5,12 +5,7 @@ import {
commands,
ProgressLocation,
} from 'vscode';
import { FoamFeature } from '../../types';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting,
} from '../../settings';
import { getWikilinkDefinitionSetting } from '../../settings';
import {
toVsCodePosition,
toVsCodeRange,
@@ -20,18 +15,19 @@ import { Foam } from '../../core/model/foam';
import { Resource } from '../../core/model/note';
import { generateHeading, generateLinkReferences } from '../../core/janitor';
import { Range } from '../../core/model/range';
import { applyTextEdit } from '../../core/janitor/apply-text-edit';
import detectNewline from 'detect-newline';
import { TextEdit } from '../../core/services/text-edit';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
commands.registerCommand('foam-vscode.janitor', async () =>
janitor(await foamPromise)
)
);
},
};
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
context.subscriptions.push(
commands.registerCommand('foam-vscode.janitor', async () =>
janitor(await foamPromise)
)
);
}
async function janitor(foam: Foam) {
try {
@@ -109,14 +105,14 @@ async function runJanitor(foam: Foam) {
}
const definitions =
wikilinkSetting === LinkReferenceDefinitionsSetting.off
wikilinkSetting === 'off'
? null
: await generateLinkReferences(
note,
noteText,
noteEol,
foam.workspace,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
wikilinkSetting === 'withExtensions'
);
if (definitions) {
updatedDefinitionListCount += 1;
@@ -130,8 +126,8 @@ async function runJanitor(foam: Foam) {
// Note: The ordering matters. Definitions need to be inserted
// before heading, since inserting a heading changes line numbers below
let text = noteText;
text = definitions ? applyTextEdit(text, definitions) : text;
text = heading ? applyTextEdit(text, heading) : text;
text = definitions ? TextEdit.apply(text, definitions) : text;
text = heading ? TextEdit.apply(text, heading) : text;
return workspace.fs.writeFile(toVsCodeUri(note.uri), Buffer.from(text));
});
@@ -151,14 +147,14 @@ async function runJanitor(foam: Foam) {
// Get edits
const heading = await generateHeading(note, noteText, eol);
const definitions =
wikilinkSetting === LinkReferenceDefinitionsSetting.off
wikilinkSetting === 'off'
? null
: await generateLinkReferences(
note,
noteText,
eol,
foam.workspace,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
wikilinkSetting === 'withExtensions'
);
if (heading || definitions) {
@@ -192,5 +188,3 @@ async function runJanitor(foam: Foam) {
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount,
};
}
export default feature;

View File

@@ -9,7 +9,7 @@ describe('open-daily-note-for-date command', () => {
await commands.executeCommand('foam-vscode.open-daily-note-for-date');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
expect.objectContaining([
expect.objectContaining({
label: expect.stringContaining(

View File

@@ -1,32 +1,33 @@
import { ExtensionContext, commands, window, QuickPickItem } from 'vscode';
import { FoamFeature } from '../../types';
import { openDailyNoteFor } from '../../dated-notes';
import { FoamWorkspace } from '../../core/model/workspace';
import { range } from 'lodash';
import dateFormat from 'dateformat';
import { Foam } from '../../core/model/foam';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise) => {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.open-daily-note-for-date',
async () => {
const ws = (await foamPromise).workspace;
const date = await window
.showQuickPick<DateItem>(generateDateItems(ws), {
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
matchOnDescription: true,
matchOnDetail: true,
})
.then(item => {
return item?.date;
});
return openDailyNoteFor(date);
}
)
);
},
};
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
context.subscriptions.push(
commands.registerCommand(
'foam-vscode.open-daily-note-for-date',
async () => {
const ws = (await foamPromise).workspace;
const date = await window
.showQuickPick<DateItem>(generateDateItems(ws), {
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
matchOnDescription: true,
matchOnDetail: true,
})
.then(item => {
return item?.date;
});
return openDailyNoteFor(date);
}
)
);
}
class DateItem implements QuickPickItem {
public label: string;
@@ -68,5 +69,3 @@ function generateDateItems(ws: FoamWorkspace): DateItem[] {
return items;
}
export default feature;

View File

@@ -1,20 +1,15 @@
import { ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../../types';
import { getFoamVsCodeConfig } from '../../services/config';
import { openDailyNoteFor } from '../../dated-notes';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise) => {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-daily-note', () =>
openDailyNoteFor(new Date())
)
);
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-daily-note', () =>
openDailyNoteFor(new Date())
)
);
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
commands.executeCommand('foam-vscode.open-daily-note');
}
},
};
export default feature;
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
commands.executeCommand('foam-vscode.open-daily-note');
}
}

View File

@@ -1,24 +1,19 @@
import { ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../../types';
import { getFoamVsCodeConfig } from '../../services/config';
import {
createDailyNoteIfNotExists,
openDailyNoteFor,
} from '../../dated-notes';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-dated-note', date => {
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
case 'navigateToNote':
return openDailyNoteFor(date);
case 'createNote':
return createDailyNoteIfNotExists(date);
}
})
);
},
};
export default feature;
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-dated-note', date => {
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
case 'navigateToNote':
return openDailyNoteFor(date);
case 'createNote':
return createDailyNoteIfNotExists(date);
}
})
);
}

View File

@@ -1,31 +1,29 @@
import { ExtensionContext, commands, window } from 'vscode';
import { FoamFeature } from '../../types';
import { focusNote } from '../../utils';
import { Foam } from '../../core/model/foam';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-random-note', async () => {
const foam = await foamPromise;
const currentFile = window.activeTextEditor?.document.uri.path;
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
if (notes.length <= 1) {
window.showInformationMessage(
'Could not find another note to open. If you believe this is a bug, please file an issue.'
);
return;
}
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-random-note', async () => {
const foam = await foamPromise;
const currentFile = window.activeTextEditor?.document.uri.path;
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
if (notes.length <= 1) {
window.showInformationMessage(
'Could not find another note to open. If you believe this is a bug, please file an issue.'
);
return;
}
let randomNoteIndex = Math.floor(Math.random() * notes.length);
if (notes[randomNoteIndex].uri.path === currentFile) {
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
}
let randomNoteIndex = Math.floor(Math.random() * notes.length);
if (notes[randomNoteIndex].uri.path === currentFile) {
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
}
focusNote(notes[randomNoteIndex].uri, false);
})
);
},
};
export default feature;
focusNote(notes[randomNoteIndex].uri, false);
})
);
}

View File

@@ -0,0 +1,99 @@
import { commands, window } from 'vscode';
import { CommandDescriptor } from '../../utils/commands';
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
import * as filter from '../../core/services/resource-filter';
import { URI } from '../../core/model/uri';
import { closeEditors, createFile } from '../../test/test-utils-vscode';
import { deleteFile } from '../../services/editor';
import waitForExpect from 'wait-for-expect';
describe('open-resource command', () => {
beforeEach(async () => {
await jest.resetAllMocks();
await closeEditors();
});
it('URI param has precedence over filter', async () => {
const spy = jest.spyOn(filter, 'createFilter');
const noteA = await createFile('Note A for open command');
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: noteA.uri,
filter: { title: 'note 1' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
expect(spy).not.toHaveBeenCalled();
await deleteFile(noteA.uri);
});
it('URI param accept URI object, or path', async () => {
const noteA = await createFile('Note A for open command');
const uriCommand: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: URI.file('path/to/file.md'),
},
};
await commands.executeCommand(uriCommand.name, uriCommand.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
await closeEditors();
const pathCommand: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: URI.file('path/to/file.md'),
},
};
await commands.executeCommand(pathCommand.name, pathCommand.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
await deleteFile(noteA.uri);
});
it('User is notified if no resource is found', async () => {
const spy = jest.spyOn(window, 'showInformationMessage');
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
filter: { title: 'note 1 with no existing title' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(spy).toHaveBeenCalled();
});
});
it('filter with multiple results will show a quick pick', async () => {
const spy = jest
.spyOn(window, 'showQuickPick')
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
filter: { title: '.*' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(spy).toHaveBeenCalled();
});
});
});

View File

@@ -1,68 +1,113 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { URI } from '../../core/model/uri';
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
import { NoteFactory } from '../../services/templates';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { Foam } from '../../core/model/foam';
import {
createFilter,
FilterDescriptor,
} from '../../core/services/resource-filter';
import { CommandDescriptor } from '../../utils/commands';
import { FoamWorkspace } from '../../core/model/workspace';
import { Resource } from '../../core/model/note';
import { isSome, isNone } from '../../core/utils';
export default async function activate(
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
context.subscriptions.push(
vscode.commands.registerCommand(OPEN_COMMAND.command, args => {
return openResource(foam.workspace, args);
})
);
}
export interface OpenResourceArgs {
/**
* The URI of the resource to open.
* If present the `filter` param is ignored
*/
uri?: URI | string | vscode.Uri;
/**
* The filter object that describes which notes to consider
* for opening
*/
filter?: FilterDescriptor;
}
export const OPEN_COMMAND = {
command: 'foam-vscode.open-resource',
title: 'Foam: Open Resource',
asURI: (uri: URI) =>
vscode.Uri.parse(`command:${OPEN_COMMAND.command}`).with({
query: encodeURIComponent(JSON.stringify({ uri })),
}),
};
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
vscode.commands.registerCommand(
OPEN_COMMAND.command,
async (params: { uri: URI }) => {
const uri = new URI(params.uri);
switch (uri.scheme) {
case 'file': {
const targetUri =
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.commands.executeCommand('vscode.open', targetUri);
}
case 'placeholder': {
const title = uri.getName();
if (uri.isAbsolute()) {
return NoteFactory.createForPlaceholderWikilink(
title,
URI.file(uri.path)
);
}
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(uri, true)
.changeExtension('', '.md');
await NoteFactory.createForPlaceholderWikilink(title, target);
return;
}
}
}
)
);
forURI: (uri: URI): CommandDescriptor<OpenResourceArgs> => {
return {
name: OPEN_COMMAND.command,
params: {
uri: uri,
},
};
},
};
export default feature;
async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
args = args ?? {};
let item: { uri: URI } | null = null;
if (args.uri) {
const path = typeof args.uri === 'string' ? args.uri : args.uri.path;
item = workspace.find(path);
}
if (isNone(item) && args.filter) {
const resources = workspace.list();
const candidates = resources.filter(
createFilter(args.filter, vscode.workspace.isTrusted)
);
if (candidates.length === 0) {
vscode.window.showInformationMessage(
'Foam: No note matches given filters.'
);
return;
}
item =
candidates.length === 1
? candidates[0]
: await vscode.window.showQuickPick(
candidates.map(createQuickPickItemForResource)
);
}
if (isSome(item)) {
const targetUri =
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
? vscode.window.activeTextEditor?.document.uri
: toVsCodeUri(item.uri.asPlain());
return vscode.commands.executeCommand('vscode.open', targetUri);
}
}
interface ResourceItem extends vscode.QuickPickItem {
label: string;
description: string;
uri: URI;
detail?: string;
}
const createQuickPickItemForResource = (resource: Resource): ResourceItem => {
const icon = 'file';
const sections = resource.sections
.map(s => s.label)
.filter(l => l !== resource.title);
const detail = sections.length > 0 ? 'Sections: ' + sections.join(', ') : '';
return {
label: `$(${icon}) ${resource.title}`,
description: vscode.workspace.asRelativePath(resource.uri.toFsPath()),
uri: resource.uri,
detail: detail,
};
};

View File

@@ -1,17 +1,16 @@
import { commands, ExtensionContext } from 'vscode';
import { FoamFeature } from '../../types';
import { Foam } from '../../core/model/foam';
export const UPDATE_GRAPH_COMMAND_NAME = 'foam-vscode.update-graph';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise) => {
context.subscriptions.push(
commands.registerCommand(UPDATE_GRAPH_COMMAND_NAME, async () => {
const foam = await foamPromise;
return foam.graph.update();
})
);
},
};
export default feature;
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
context.subscriptions.push(
commands.registerCommand(UPDATE_GRAPH_COMMAND_NAME, async () => {
const foam = await foamPromise;
return foam.graph.update();
})
);
}

View File

@@ -1,4 +1,3 @@
import { uniq } from 'lodash';
import {
CancellationToken,
CodeLens,
@@ -12,209 +11,164 @@ import {
workspace,
Position,
} from 'vscode';
import {
hasEmptyTrailing,
docConfig,
loadDocConfig,
isMdEditor,
mdDocSelector,
getText,
} from '../../utils';
import { FoamFeature } from '../../types';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting,
} from '../../settings';
import { isMdEditor, mdDocSelector } from '../../utils';
import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../../core/services/markdown-provider';
import {
LINK_REFERENCE_DEFINITION_FOOTER,
LINK_REFERENCE_DEFINITION_HEADER,
generateLinkReferences,
} from '../../core/janitor/generate-link-references';
import { fromVsCodeUri } from '../../utils/vsc-utils';
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
import { getEditorEOL } from '../../services/editor';
import { ResourceParser } from '../../core/model/note';
import { getWikilinkDefinitionSetting } from '../../settings';
import { IMatcher } from '../../core/services/datastore';
const feature: FoamFeature = {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
const foam = await foamPromise;
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
context.subscriptions.push(
commands.registerCommand('foam-vscode.update-wikilinks', () =>
updateReferenceList(foam.workspace)
),
workspace.onWillSaveTextDocument(e => {
if (
e.document.languageId === 'markdown' &&
foam.services.matcher.isMatch(fromVsCodeUri(e.document.uri))
) {
e.waitUntil(updateReferenceList(foam.workspace));
}
}),
languages.registerCodeLensProvider(
mdDocSelector,
new WikilinkReferenceCodeLensProvider(foam.workspace)
context.subscriptions.push(
commands.registerCommand('foam-vscode.update-wikilink-definitions', () => {
return updateWikilinkDefinitions(
foam.workspace,
foam.services.parser,
foam.services.matcher
);
}),
workspace.onWillSaveTextDocument(e => {
e.waitUntil(
updateWikilinkDefinitions(
foam.workspace,
foam.services.parser,
foam.services.matcher
)
);
}),
languages.registerCodeLensProvider(
mdDocSelector,
new WikilinkReferenceCodeLensProvider(
foam.workspace,
foam.services.parser
)
);
},
};
async function createReferenceList(foam: FoamWorkspace) {
const editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
return;
}
const refs = await generateReferenceList(foam, editor.document);
if (refs && refs.length) {
await editor.edit(function(editBuilder) {
if (editor) {
const spacing = hasEmptyTrailing(editor.document)
? docConfig.eol
: docConfig.eol + docConfig.eol;
editBuilder.insert(
new Position(editor.document.lineCount, 0),
spacing + refs.join(docConfig.eol)
);
}
});
}
)
);
}
async function updateReferenceList(foam: FoamWorkspace) {
async function updateWikilinkDefinitions(
fWorkspace: FoamWorkspace,
fParser: ResourceParser,
fMatcher: IMatcher
) {
const editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
return;
}
loadDocConfig();
const doc = editor.document;
const range = detectReferenceListRange(doc);
if (!range) {
await createReferenceList(foam);
} else {
const refs = generateReferenceList(foam, doc);
// references must always be preceded by an empty line
const spacing = doc.lineAt(range.start.line - 1).isEmptyOrWhitespace
? ''
: docConfig.eol;
await editor.edit(editBuilder => {
editBuilder.replace(range, spacing + refs.join(docConfig.eol));
});
}
}
function generateReferenceList(
foam: FoamWorkspace,
doc: TextDocument
): string[] {
const wikilinkSetting = getWikilinkDefinitionSetting();
if (wikilinkSetting === LinkReferenceDefinitionsSetting.off) {
return [];
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
return;
}
const note = foam.get(fromVsCodeUri(doc.uri));
const setting = getWikilinkDefinitionSetting();
const eol = getEditorEOL();
const text = doc.getText();
// Should never happen as `doc` is usually given by `editor.document`, which
// binds to an opened note.
if (!note) {
console.warn(
`Can't find note for URI ${doc.uri.path} before attempting to generate its markdown reference list`
);
return [];
if (setting === 'off') {
const { range } = detectDocumentWikilinkDefinitions(text, eol);
if (range) {
await editor.edit(editBuilder => {
editBuilder.delete(toVsCodeRange(range));
});
}
return;
}
const references = uniq(
createMarkdownReferences(
foam,
note.uri,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
).map(stringifyMarkdownLinkReferenceDefinition)
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
const update = await generateLinkReferences(
resource,
text,
eol,
fWorkspace,
setting === 'withExtensions'
);
if (references.length) {
return [
LINK_REFERENCE_DEFINITION_HEADER,
...references,
LINK_REFERENCE_DEFINITION_FOOTER,
];
if (update) {
await editor.edit(editBuilder => {
const gap = doc.lineAt(update.range.start.line - 1).isEmptyOrWhitespace
? ''
: eol;
editBuilder.replace(toVsCodeRange(update.range), gap + update.newText);
});
}
return [];
}
/**
* Find the range of existing reference list
* @param doc
* Detects the range of the wikilink definitions in the document.
*/
function detectReferenceListRange(doc: TextDocument): Range | null {
const fullText = doc.getText();
function detectDocumentWikilinkDefinitions(text: string, eol: string) {
const lines = text.split(eol);
const headerIndex = fullText.indexOf(LINK_REFERENCE_DEFINITION_HEADER);
const footerIndex = fullText.lastIndexOf(LINK_REFERENCE_DEFINITION_FOOTER);
if (headerIndex < 0) {
return null;
}
const headerLine =
fullText.substring(0, headerIndex).split(docConfig.eol).length - 1;
const footerLine =
fullText.substring(0, footerIndex).split(docConfig.eol).length - 1;
if (headerLine >= footerLine) {
return null;
}
return new Range(
new Position(headerLine, 0),
new Position(footerLine, LINK_REFERENCE_DEFINITION_FOOTER.length)
const headerLine = lines.findIndex(
line => line === LINK_REFERENCE_DEFINITION_HEADER
);
const footerLine = lines.findIndex(
line => line === LINK_REFERENCE_DEFINITION_FOOTER
);
if (headerLine < 0 || footerLine < 0 || headerLine >= footerLine) {
return { range: null, definitions: null };
}
const range = new Range(
new Position(headerLine, 0),
new Position(footerLine, lines[footerLine].length)
);
const definitions = lines.slice(headerLine, footerLine).join(eol);
return { range, definitions };
}
/**
* Provides a code lens to update the wikilink definitions in the document.
*/
class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
private foam: FoamWorkspace;
constructor(
private fWorkspace: FoamWorkspace,
private fParser: ResourceParser
) {}
constructor(foam: FoamWorkspace) {
this.foam = foam;
}
public provideCodeLenses(
public async provideCodeLenses(
document: TextDocument,
_: CancellationToken
): CodeLens[] | Thenable<CodeLens[]> {
loadDocConfig();
): Promise<CodeLens[]> {
const eol = getEditorEOL();
const text = document.getText();
const range = detectReferenceListRange(document);
const { range } = detectDocumentWikilinkDefinitions(text, eol);
if (!range) {
return [];
}
const setting = getWikilinkDefinitionSetting();
const refs = generateReferenceList(this.foam, document);
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
const newRefs = refs.join(docConfig.eol);
const resource = this.fParser.parse(fromVsCodeUri(document.uri), text);
const update = await generateLinkReferences(
resource,
text,
eol,
this.fWorkspace,
setting === 'withExtensions'
);
const status = oldRefs === newRefs ? 'up to date' : 'out of date';
const status = update == null ? 'up to date' : 'out of date';
return [
new CodeLens(range, {
command:
update == null ? '' : 'foam-vscode.update-wikilink-definitions',
title: `Wikilink definitions (${status})`,
arguments: [],
title: `Link references (${status})`,
command: '',
}),
];
}
}
export default feature;

View File

@@ -9,7 +9,17 @@ import {
} from 'vscode';
import { getDailyNoteFileName } from '../dated-notes';
import { getFoamVsCodeConfig } from '../services/config';
import { FoamFeature } from '../types';
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
languages.registerCompletionItemProvider('markdown', completions, '/'),
languages.registerCompletionItemProvider(
'markdown',
datesCompletionProvider,
'/'
)
);
}
interface DateSnippet {
snippet: string;
@@ -28,21 +38,42 @@ const daysOfWeek = [
];
const generateDayOfWeekSnippets = (): DateSnippet[] => {
const getTarget = (day: number) => {
const getFutureTarget = (day: number) => {
const target = new Date();
const currentDay = target.getDay();
const distance = (day + 7 - currentDay) % 7;
target.setDate(target.getDate() + distance);
return target;
};
// needs work
const getPastTarget = (day: number) => {
const target = new Date();
const currentDay = target.getDay();
const distance = currentDay === day ? 7 : (7 + currentDay - day) % 7;
target.setDate(target.getDate() - distance);
return target;
};
const snippets = daysOfWeek.map(({ day, index }) => {
const target = getTarget(index);
const target = getFutureTarget(index);
return {
date: target,
detail: `Get a daily note link for ${day}`,
snippet: `/${day}`,
};
});
// append snippets previous days
snippets.push(
...daysOfWeek.map(({ day, index }) => {
const target = getPastTarget(index);
return {
date: target,
detail: `Get a daily note link for last ${day}`,
snippet: `/-${day}`,
};
})
);
return snippets;
};
@@ -198,18 +229,3 @@ export const datesCompletionProvider: CompletionItemProvider = {
return new CompletionList(completionItems, true);
},
};
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
languages.registerCompletionItemProvider('markdown', completions, '/'),
languages.registerCompletionItemProvider(
'markdown',
datesCompletionProvider,
'/'
)
);
},
};
export default feature;

View File

@@ -1,6 +1,5 @@
import { debounce } from 'lodash';
import * as vscode from 'vscode';
import { FoamFeature } from '../types';
import { ResourceParser } from '../core/model/note';
import { FoamWorkspace } from '../core/model/workspace';
import { Foam } from '../core/model/foam';
@@ -14,67 +13,62 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
cursor: 'pointer',
});
const updateDecorations = (
parser: ResourceParser,
workspace: FoamWorkspace
) => (editor: vscode.TextEditor) => {
if (!editor || editor.document.languageId !== 'markdown') {
return;
}
const note = parser.parse(
fromVsCodeUri(editor.document.uri),
editor.document.getText()
);
const placeholderRanges = [];
note.links.forEach(link => {
const linkUri = workspace.resolveLink(note, link);
if (linkUri.isPlaceholder()) {
placeholderRanges.push(
Range.create(
link.range.start.line,
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
link.range.end.line,
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
)
);
const updateDecorations =
(parser: ResourceParser, workspace: FoamWorkspace) =>
(editor: vscode.TextEditor) => {
if (!editor || editor.document.languageId !== 'markdown') {
return;
}
});
editor.setDecorations(placeholderDecoration, placeholderRanges);
};
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
let activeEditor = vscode.window.activeTextEditor;
const immediatelyUpdateDecorations = updateDecorations(
foam.services.parser,
foam.workspace
const note = parser.parse(
fromVsCodeUri(editor.document.uri),
editor.document.getText()
);
const placeholderRanges = [];
note.links.forEach(link => {
const linkUri = workspace.resolveLink(note, link);
if (linkUri.isPlaceholder()) {
placeholderRanges.push(
Range.create(
link.range.start.line,
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
link.range.end.line,
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
)
);
}
});
editor.setDecorations(placeholderDecoration, placeholderRanges);
};
const debouncedUpdateDecorations = debounce(
immediatelyUpdateDecorations,
500
);
export default async function activate(
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
let activeEditor = vscode.window.activeTextEditor;
immediatelyUpdateDecorations(activeEditor);
const immediatelyUpdateDecorations = updateDecorations(
foam.services.parser,
foam.workspace
);
context.subscriptions.push(
placeholderDecoration,
vscode.window.onDidChangeActiveTextEditor(editor => {
activeEditor = editor;
immediatelyUpdateDecorations(activeEditor);
}),
vscode.workspace.onDidChangeTextDocument(event => {
if (activeEditor && event.document === activeEditor.document) {
debouncedUpdateDecorations(activeEditor);
}
})
);
},
};
const debouncedUpdateDecorations = debounce(
immediatelyUpdateDecorations,
500
);
export default feature;
immediatelyUpdateDecorations(activeEditor);
context.subscriptions.push(
placeholderDecoration,
vscode.window.onDidChangeActiveTextEditor(editor => {
activeEditor = editor;
immediatelyUpdateDecorations(activeEditor);
}),
vscode.workspace.onDidChangeTextDocument(event => {
if (activeEditor && event.document === activeEditor.document) {
debouncedUpdateDecorations(activeEditor);
}
})
);
}

View File

@@ -92,9 +92,7 @@ describe('Hover provider', () => {
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
@@ -133,9 +131,7 @@ describe('Hover provider', () => {
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -164,9 +160,7 @@ describe('Hover provider', () => {
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -190,9 +184,7 @@ describe('Hover provider', () => {
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -220,9 +212,7 @@ The content of file B`);
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);

Some files were not shown because too many files have changed in this diff Show More