Compare commits

...

46 Commits

Author SHA1 Message Date
Riccardo Ferretti
4457e83e38 v0.9.0 2021-01-27 23:19:01 +01:00
Riccardo Ferretti
fb15672e6a preparation for 0.9.0 release 2021-01-27 23:18:40 +01:00
Riccardo Ferretti
2ef2a217ee updated links to discord 2021-01-27 21:54:45 +01:00
léon h
8a73cba1f0 Welcome screen for Tag Explorer (#460)
* Welcome screen for Tag Explorer

* Front matter
2021-01-27 16:38:05 +01:00
allcontributors[bot]
b0ea08b84f docs: add leonhfr as a contributor (#458)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-26 15:57:07 +01:00
léon h
ea0edc5149 Orphaned notes (#457)
* Orphaned notes

* Refactor to panel

* Toggle groupBy option

* Hide command from palette

* Icon toggle

* Docs

* Tests

* Extract isMatch logic
2021-01-26 15:56:14 +01:00
Riccardo Ferretti
42dabfbf9d added discord badge 2021-01-22 23:42:18 +01:00
Riccardo Ferretti
85d3aef2ff updated discord link 2021-01-21 12:32:05 +01:00
Riccardo Ferretti
8bd3109325 removed paste-image extension from recommendation list as it's now part of foam-template 2021-01-19 23:06:37 +01:00
Riccardo Ferretti
6ca800b500 Focus graph documentation on Foam implementation 2021-01-15 19:31:38 +01:00
Riccardo Ferretti
3a798b520f v0.8.0 2021-01-15 13:36:00 +01:00
Riccardo Ferretti
8db4d2897f Preparation for 0.8.0 release 2021-01-15 13:24:35 +01:00
Riccardo
e0bcb6bd92 Style graph nodes by type (#449)
* added styling of graph nodes by type

* removed unused css file

* added style by type to documentation
2021-01-14 20:22:44 +01:00
Riccardo
cd92468311 Prettier format and chores (#448)
* fix #442 - link to known-issues replaced

* formatted settings + enable format on save + improved jest config

* removed editorconfig in foam-cli

* formatted foam-vscode

* removed author in package.json files

* minor changes to readme files

* fixed husky pre-commit hook
2021-01-14 17:00:03 +01:00
Riccardo Ferretti
5a44fbc26f improved foam-vscode readme 2021-01-14 15:05:40 +01:00
allcontributors[bot]
4412f860dd docs: add elswork as a contributor (#445)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-14 13:16:44 +01:00
elswork
3e3d36b954 Update unlinked-references.md (#444)
Fix small mistake
2021-01-14 13:08:51 +01:00
José Duarte
eee6b7bd3a Allow for the reuse of the graph panel (#443) 2021-01-14 13:07:59 +01:00
José Duarte
792e66b061 Add custom graph styling support (#438)
* Add graph style to VSCode settings

* Update default to an empty object

* Add function to retrieve the graph style from the settings

* Implement the graph custom styling setting

The implementation makes use of the webview communication mechanism to
switch messages between the webview and the graph.
It works as follows:

- When the webview is loaded, it now sends a single request to VSCode,
the request asks VSCode for the graph style
- When VSCode answers with the style, the graph style is updated and
the webview loading process continues as normal.

The style change does not modify the API, in fact it makes use of the
shadiest programming tatic ever, *global variables* to remain
compatible.

The style object *currently* supports four base fields:
- background: string
- fontSize: int
- highlightedForeground: string
- node: object
  - note: string
  - nonExistingNote: string
  - unknown: string

* Simplify null handling logic

* Remove debug logs

* Rename style setting

* Rename message style type

* Remove forgotten debug log

* Refactor the code to match model & action

* Add missing break

* Allow for dynamic style updates

* Fix the window loading bug

* Implement a permanent fix to the bug

* Replace `nonExistingNote` with `placeholder`

* Include the new graph style feature in the docs

* Remove unnecessary async

* Remove unused case
2021-01-13 17:55:58 +00:00
Mike Cluck
e5589f0555 Add ability to open a random note (#440)
* Add Open Random Note command
2021-01-12 22:29:03 +01:00
Riccardo
32f40fa0de fix(#473) - link now points to correct location 2021-01-10 22:33:44 +01:00
Riccardo
97e3b20112 Added support for direct links (#433)
* support direct links

* added support for labels with formatting

* added documentation and removed lint warning

* ignore external links

* improved uri parsing

* filter links pointing to same note (e.g. sections within the note)

* check that note exists before navigating to it

* fixed compilation error
2021-01-08 17:54:40 +00:00
Brian Anglin
0060ea2a3a Fix typo in link (#434) 2021-01-06 22:53:38 +01:00
allcontributors[bot]
a11dee89ba docs: add anglinb as a contributor (#431)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-04 15:07:49 +01:00
Brian Anglin
62e46e87b0 Adds iOS Shortcut and GitHub Actions recipe (#430) 2021-01-04 15:04:59 +01:00
Riccardo
ef7ea8d23b End 2 end test for the extension in vscode (#428)
* added support for e2e vscode tests

* using github action that supports headless test run

* loading vscode test instance in empty dir to speed up bootstrap
2021-01-03 19:27:51 +01:00
Riccardo
36a632f218 consolidated prettier configuration in root package.json file (#429)
also fixed linting and consolidated .gitignore and moved .editorconfig
2021-01-03 18:35:33 +01:00
Riccardo
f9331ad327 refactor: better organization and simplification of core model (#406)
* using URI as note identifier

* cleanup: put definitions together with related code, moved note-graph, removed slug field from Note model
2021-01-03 13:04:05 +01:00
allcontributors[bot]
a944d993fc docs: add themaxdavitt as a contributor (#426)
for fixing recipe link in foam-template
2021-01-02 22:53:08 +01:00
Riccardo Ferretti
adf2dfa779 v0.7.7 2020-12-31 11:40:34 +01:00
Riccardo Ferretti
c04bc347ed Preparation for 0.7.7 release 2020-12-31 11:40:03 +01:00
Riccardo Ferretti
5f8a064af2 Added 0.7.6 info to CHANGELOG 2020-12-29 13:46:49 +01:00
allcontributors[bot]
eaa522fe1b docs: add bpugh as a contributor (#419)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-29 12:01:51 +01:00
Mike Cluck
3868cc5a17 Dated snippets create standard wikilinks (#416) 2020-12-29 11:58:18 +01:00
Brandon Pugh
76d70d40f8 Fallback to word-based suggestions when not triggered by trigger character (#417)
Fixes #415

Currently you have to return undefined or an empty array for suggestions
in order for vscode to fallback to word-based suggestions.
2020-12-29 11:56:32 +01:00
Riccardo Ferretti
2e373c4624 v0.7.6 2020-12-20 19:45:35 +01:00
Riccardo Ferretti
71948062b1 updated documentation links via janitor command 2020-12-20 19:39:16 +01:00
Riccardo
94711cf11b fix(#410): using fsPath property in janitor (#411) 2020-12-20 19:38:33 +01:00
Riccardo Ferretti
7d0858246d v0.7.5 2020-12-17 20:40:24 +01:00
Riccardo Ferretti
d998c8d482 Prepare for 0.7.5 release 2020-12-17 20:39:44 +01:00
allcontributors[bot]
85e0784c54 docs: add MCluck90 as a contributor (#408)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-17 20:28:57 +01:00
Mike Cluck
035fb7b634 Use fsPath to get normalized path for current OS (#407) 2020-12-17 17:20:04 +01:00
Riccardo Ferretti
50075a384b v0.7.4 2020-12-16 19:54:28 +01:00
Riccardo Ferretti
71e3581409 Preparation for 0.7.4 2020-12-16 17:53:08 +01:00
Riccardo Ferretti
5249edab38 use path to open doc selected in dataviz graph 2020-12-16 17:50:53 +01:00
Riccardo
6baeec4db6 Using URI across the board for better Windows support (#391)
* add windows-2019 to CI os matrix
* use actual URI object instead of strings to represent paths/uris
* added datastore tests
* chore: make Foam IDisposable
2020-12-16 17:47:31 +01:00
106 changed files with 4005 additions and 1127 deletions

View File

@@ -526,6 +526,60 @@
"contributions": [
"doc"
]
},
{
"login": "MCluck90",
"name": "Mike Cluck",
"avatar_url": "https://avatars1.githubusercontent.com/u/1753801?v=4",
"profile": "https://mcluck.tech",
"contributions": [
"code"
]
},
{
"login": "bpugh",
"name": "Brandon Pugh",
"avatar_url": "https://avatars1.githubusercontent.com/u/684781?v=4",
"profile": "http://brandonpugh.com",
"contributions": [
"code"
]
},
{
"login": "themaxdavitt",
"name": "Max Davitt",
"avatar_url": "https://avatars1.githubusercontent.com/u/27709025?v=4",
"profile": "https://max.davitt.me",
"contributions": [
"doc"
]
},
{
"login": "anglinb",
"name": "Brian Anglin",
"avatar_url": "https://avatars3.githubusercontent.com/u/2637602?v=4",
"profile": "http://briananglin.me",
"contributions": [
"doc"
]
},
{
"login": "elswork",
"name": "elswork",
"avatar_url": "https://avatars1.githubusercontent.com/u/1455507?v=4",
"profile": "http://deft.work",
"contributions": [
"doc"
]
},
{
"login": "leonhfr",
"name": "léon h",
"avatar_url": "https://avatars.githubusercontent.com/u/19996318?v=4",
"profile": "http://leonh.fr/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Question
url: https://discord.gg/8c4BChMfSu
url: https://foambubble.github.io/join-discord/g
about: Please ask and answer questions here.

View File

@@ -36,7 +36,7 @@ jobs:
name: Build and Test
strategy:
matrix:
os: [macos-10.15, ubuntu-18.04] # add windows-2019 after fixing tests for it
os: [macos-10.15, ubuntu-18.04, windows-2019]
runs-on: ${{ matrix.os }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
env:
@@ -59,4 +59,6 @@ jobs:
- name: Build Packages
run: yarn build
- name: Run Tests
run: yarn test
uses: GabrielBB/xvfb-action@v1.0
with:
run: yarn test

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules
*.tsbuildinfo
*.vsix
*.log
out
dist
docs/_site
docs/.sass-cache

42
.vscode/settings.json vendored
View File

@@ -1,21 +1,25 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
// set these to true to hide folders with the compiled JS files,
"packages/**/out": false,
"packages/**/dist": false
},
"search.exclude": {
// set this to false to include compiled JS folders in search results
"packages/**/out": true,
"packages/**/dist": true
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"foam.files.ignore": [
"**/.vscode/**/*",
"**/_layouts/**/*",
"**/_site/**/*",
"**/node_modules/**/*"
]
}
"files.exclude": {
// set these to true to hide folders with the compiled JS files,
"packages/**/out": false,
"packages/**/dist": false
},
"search.exclude": {
// set this to false to include compiled JS folders in search results
"packages/**/out": true,
"packages/**/dist": true
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"foam.files.ignore": [
"**/.vscode/**/*",
"**/_layouts/**/*",
"**/_site/**/*",
"**/node_modules/**/*"
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.requireConfig": true,
"editor.formatOnSave": true,
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -15,4 +15,4 @@
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: dev/todo.md "Todo"
[//end]: # "Autogenerated link references"
[//end]: # "Autogenerated link references"

View File

@@ -17,7 +17,7 @@ to make it easier for new contributors we provide some resources:
- [[architecture]] - This document describes the architecture of Foam and how the repository is structured.
You can also see [existing issues](https://github.com/foambubble/foam/issues) and help out!
Finally, the easiest way to help, is to use it and provide feedback by [submitting issues](https://github.com/foambubble/foam/issues/new/choose) or participating in the [Foam Community Discord](https://discord.gg/rtdZKgj)!
Finally, the easiest way to help, is to use it and provide feedback by [submitting issues](https://github.com/foambubble/foam/issues/new/choose) or participating in the [Foam Community Discord](https://foambubble.github.io/join-discord/g)!
## Contributing
@@ -37,6 +37,16 @@ If you're interested in contributing, this short guide will help you get things
You should now be ready to start working!
### Testing
Code needs to come with tests.
We use the following convention in Foam:
- *.test.ts are unit tests
- *.spec.ts are integration tests
Also, note that tests in `foam-core` and `foam-cli` live in the `test` directory.
Tests in `foam-vscode` live alongside the code in `src`.
### The VS Code Extension
This guide assumes you read the previous instructions and you're set up to work on Foam.

View File

@@ -16,6 +16,6 @@ Exceptions to the monorepo are:
- All other [[recommended-extensions]] live in their respective GitHub repos.
[//begin]: # "Autogenerated link references for markdown compatibility"
[recipes]: ../recipes/recipes "Recipes"
[recommended-extensions]: ../recommended-extensions "Recommended Extensions"
[recipes]: ../recipes/recipes.md "Recipes"
[recommended-extensions]: ../recommended-extensions.md "Recommended Extensions"
[//end]: # "Autogenerated link references"

View File

@@ -14,4 +14,5 @@ The idea would be to automatically generate lists of backlinks (and optionally,
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: todo.md "Todo"
[roadmap]: roadmap.md "Roadmap"
[//end]: # "Autogenerated link references"

View File

@@ -6,4 +6,5 @@ If you're interested in working on it, please start a conversation in [GitHub is
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: todo.md "Todo"
[roadmap]: roadmap.md "Roadmap"
[//end]: # "Autogenerated link references"

View File

@@ -11,4 +11,5 @@ If you're interested in working on it, please start a conversation in [GitHub is
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: todo.md "Todo"
[roadmap]: roadmap.md "Roadmap"
[//end]: # "Autogenerated link references"

View File

@@ -1,4 +1,3 @@
@@ -0,0 +1,125 @@
# Roadmap
Some of these items can be achieved by combining existing tools, but others may require us to build bespoke software solutions. See [[build-vs-assemble]] to understand trade-offs between these approaches. If a feature can be implemented by contributing to [[recipes]], it should.
@@ -87,7 +86,7 @@ The community is working on a number of automated scripts to help you migrate to
[build-vs-assemble]: build-vs-assemble.md "Build vs Assemble"
[recipes]: ../recipes/recipes.md "Recipes"
[contribution-guide]: ../contribution-guide.md "Contribution Guide"
[git-integration]: ../features/git-integration.md "Git integration"
[git-integration]: ../features/git-integration.md "Git Integration"
[wiki-links]: ../wiki-links.md "Wiki Links"
[foam-file-format]: foam-file-format.md "Foam File Format"
[unlinked-references]: unlinked-references.md "Unlinked references (stub)"

View File

@@ -14,5 +14,6 @@ Features belong on the [[roadmap]].
For more things to do, check backlinks for Pages that annotate [[todo]].
[//begin]: # "Autogenerated link references for markdown compatibility"
[roadmap]: roadmap.md "Roadmap"
[todo]: todo.md "Todo"
[//end]: # "Autogenerated link references"

View File

@@ -6,7 +6,7 @@ If you're interested in working on it, please start a conversation in [GitHub is
## Notes
One of Roam's big features is the ability to find all instances of a reference, create a page for it and update all the references to link to the new page.
One of Foam's big features is the ability to find all instances of a reference, create a page for it and update all the references to link to the new page.
Implementing this is on the [[roadmap]], but for the time being you can achieve similar things by:
@@ -16,4 +16,5 @@ Implementing this is on the [[roadmap]], but for the time being you can achieve
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: todo.md "Todo"
[roadmap]: roadmap.md "Roadmap"
[//end]: # "Autogenerated link references"

View File

@@ -8,5 +8,5 @@
[//begin]: # "Autogenerated link references for markdown compatibility"
[search-for-notes]: ../recipes/search-for-notes.md "Search for Notes"
[graph-visualisation]: graph-visualisation.md "Graph visualisation"
[graph-visualisation]: graph-visualisation.md "Graph Visualisation"
[//end]: # "Autogenerated link references"

View File

@@ -1,16 +1,59 @@
# Graph Visualisation
Foam comes with a graph visualisation.
Foam comes with a graph visualisation of your notes. To see the graph execute the `Foam: Show Graph` command.
The graph will:
- allow you to highlight a node by hovering on it, to quickly see how it's connected to the rest of your notes
- allow you to select one or more (by keeping `SHIFT` pressed while selecting) nodes by clicking on them, to better understand the structure of your notes
- to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
- allow you to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
- automatically center the graph on the currently edited note, to immediately see it's connections
### Markdown Links
Another extension that provides a great graph visualisation is [Markdown Links](https://marketplace.visualstudio.com/items?itemName=tchayen.markdown-links).
The extension doesn't use the Foam model, so discrepancies might arise, but it's a great visualisation extension nonetheless!
## Custom Graph Styles
Currently, custom graph styles are supported through the `foam.graph.style` setting.
![Graph style demo](../assets/images/graph-style.gif)
A sample configuration object is provided below:
```json
"foam.graph.style": {
"background": "#202020",
"fontSize": 12,
"highlightedForeground": "#f9c74f",
"node": {
"note": "#277da1",
"placeholder": "#545454",
}
}
```
### Style nodes by type
It is possible to customize the style of a node based on the `type` property in the YAML frontmatter of the corresponding document.
For example the following `backlinking.md` note:
```
---
type: feature
---
# Backlinking
...
```
And the following `settings.json`:
```json
"foam.graph.style": {
"node": {
"feature": "red",
}
}
```
Will result in the following graph:
![Style node by type](../assets/images/style-node-by-type.png)
- Use the `Markdown Links: Show Graph` command to see the graph
![Demo of graph visualiser](../assets/images/foam-navigation-demo.gif)

View File

@@ -16,7 +16,7 @@ The following example:
[wiki-links]: wiki-links "Wiki Links"
[github-pages]: github-pages "Github Pages"
```
You can open the [raw markdown](https://foambubble.github.io/foam/foam-file-format.md) to see them at the bottom of this file
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
## Specification

10
docs/features/orphans.md Normal file
View File

@@ -0,0 +1,10 @@
# Orphans
Foam helps you to find orphans: notes that have neither forward links nor backlinks.
Orphans can be found in the Orphans panel.
Two settings allows you to control the behaviour of the Orphans panel:
- `foam.orphans.exclude`: list of glob patterns that will be used to exclude directories. For example, a value of `["journal"]` would exclude your daily notes.
- `foam.orphans.groupBy`: sets the default view mode of the Orphans panel: either groups by folder (by default), or lists all orphans. The view can be toggled on the fly from the panel, but it won't overwrite the setting.

View File

@@ -7,7 +7,7 @@ You can use **Foam** for organising your research, keeping re-discoverable notes
**Foam** is free, open source, and extremely extensible to suit your personal workflow. You own the information you create with Foam, and you're free to share it, and collaborate on it with anyone you want.
<p class="announcement">
<b>New!</b> Join <a href="https://discord.gg/rtdZKgj" target="_blank">Foam community Discord</a> for users and contributors!
<b>New!</b> Join <a href="https://foambubble.github.io/join-discord/w" target="_blank">Foam community Discord</a> for users and contributors!
</p>
<div class="website-only">
@@ -76,7 +76,7 @@ To learn more about how to use **Foam**, read the [[recipes]].
Getting stuck in the setup? Read the [[frequently-asked-questions]].
There are [[known-issues]], and I'm sure, many unknown issues! Please [report them on GitHub](http://github.com/foambubble/foam/issues)!
Check our [issues on GitHub](http://github.com/foambubble/foam/issues) if you get stuck on something, and create a new one if something doesn't seem right!
## Features
@@ -107,81 +107,90 @@ If that sounds like something you're interested in, I'd love to have you along o
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt=""/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt=""/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
**Foam** was inspired by [Roam Research](https://roamresearch.com/) and the [Zettelkasten methodology](https://zettelkasten.de/posts/overview)
@@ -193,7 +202,7 @@ If that sounds like something you're interested in, I'd love to have you along o
Foam is licensed under the [MIT license](license).
[//begin]: # "Autogenerated link references for markdown compatibility"
[graph-visualisation]: features/graph-visualisation.md "Graph visualisation"
[graph-visualisation]: features/graph-visualisation.md "Graph Visualisation"
[backlinking]: features/backlinking.md "Backlinking"
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
[recipes]: recipes/recipes.md "Recipes"

View File

@@ -0,0 +1,57 @@
# Capture Notes With Shortcuts and GitHub Actions
With this #recipe you can create notes on your iOS device, which will automatically be imported into Foam.
## Context
* 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
## Other tools
* We assume you are familiar with how to use GitHub (if you are using Foam this is implicit)
* You have an iOS device.
## Instructions
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
```
name: Manually triggered workflow
on:
workflow_dispatch:
inputs:
data:
description: 'What information to put in the knowledge base.'
required: true
jobs:
store_data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: anglinb/foam-capture-action@main
with:
capture: ${{ github.event.inputs.data }}
- run: |
git config --local user.email "example@gmail.com"
git config --local user.name "Your name"
git commit -m "Captured from workflow trigger" -a
git push -u origin master
```
2. In GitHub [create a Personal Access Token](https://github.com/settings/tokens) and give it `repo` scope - make a note of the token
3. Run this command to find your `workflow-id` to be used in the Shortcut.
```bash
curl \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: Bearer <GITHUB_TOKEN>" \
https://api.github.com/repos/<owner>/<repository>/actions/workflows
```
4. Copy this [Shortcut](https://www.icloud.com/shortcuts/57d2ed90c40e43a5badcc174ebfaaf1d) to your iOS devices and edit the contents of the last step, `GetContentsOfURL`
- Make sure you update the URL of the shortcut step with the `owner`, `repository`, `workflow-id` (from the previous step)
- Make sure you update the headers of the shortcut step, replaceing `[GITHUB_TOKEN]` with your Personal Access Token (from step 2)
5. Run the shortcut & celebrate! ✨ (You should see a GitHub Action run start and the text you entered show up in `inbox.md` in your repository.)

View File

@@ -37,6 +37,9 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
- Link documents with [[wiki-links]].
- Use shortcuts for [[creating-new-notes]]
- Instantly create and access your [[daily-notes]]
- Add and explore [[tags]]
- Create [[note-templates]]
- Find [[orphans]]
- Use custom [[note-macros]] to create weekly, monthly etc. notes
- Draw [[diagrams-in-markdown]]
- Prettify your links, [[automatically-expand-urls-to-well-titled-links]]
@@ -82,6 +85,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
## Workflow
- Capture notes from Drafts app on iOS [[capture-notes-with-drafts-pro]]
- Capture notes from iOS Shortcuts [[capture-notes-with-shortcuts-and-github-actions]]
## Creative ideas
@@ -102,12 +106,15 @@ _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"
[graph-visualisation]: ../features/graph-visualisation.md "Graph visualisation"
[graph-visualisation]: ../features/graph-visualisation.md "Graph Visualisation"
[backlinking]: ../features/backlinking.md "Backlinking"
[unlinked-references]: ../dev/unlinked-references.md "Unlinked references (stub)"
[wiki-links]: ../wiki-links.md "Wiki Links"
[creating-new-notes]: ../features/creating-new-notes.md "Creating New Notes"
[daily-notes]: ../features/daily-notes.md "Daily notes"
[tags]: ../features/tags.md "Tags"
[note-templates]: ../features/note-templates.md "Note Templates"
[orphans]: ../features/orphans.md "Orphans"
[note-macros]: note-macros.md "Custom Note Macros"
[diagrams-in-markdown]: diagrams-in-markdown.md "Diagrams in Markdown"
[automatically-expand-urls-to-well-titled-links]: automatically-expand-urls-to-well-titled-links.md "Automatically Expand URLs to Well-Titled Links"
@@ -115,7 +122,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[add-images-to-notes]: add-images-to-notes.md "Add images to your notes"
[shows-image-preview-on-hover]: shows-image-preview-on-hover.md "Shows Image Preview on Hover"
[good-first-task]: ../dev/good-first-task.md "Good First Task"
[git-integration]: ../features/git-integration.md "Git integration"
[git-integration]: ../features/git-integration.md "Git Integration"
[write-your-notes-in-github-gist]: write-your-notes-in-github-gist.md "Write your notes in GitHub Gist"
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
[publish-to-gitlab-pages]: ../publishing/publish-to-gitlab-pages.md "GitLab Pages"
@@ -127,4 +134,5 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
[capture-notes-with-shortcuts-and-github-actions]: capture-notes-with-shortcuts-and-github-actions.md "Capture Notes With Shortcuts and GitHub Actions"
[//end]: # "Autogenerated link references"

View File

@@ -52,7 +52,7 @@ If such an app was worth building, it would have to have the following features:
- Ability to search and navigate forward links and back links (onlly 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://discord.gg/rtdZKgj).
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).
[//begin]: # "Autogenerated link references for markdown compatibility"

View File

@@ -18,7 +18,6 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)
- [Mermaid Support for Preview](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
- [VSCode PDF Viewing](https://marketplace.visualstudio.com/items?itemName=tomoki1207.pdf)
- [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
- [Markdown Extended](https://marketplace.visualstudio.com/items?itemName=jebbs.markdown-extended) (with `kbd` option disabled, `kbd` turns wiki-links into non-clickable buttons)

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.7.3"
"version": "0.9.0"
}

View File

@@ -3,7 +3,6 @@
"version": "0.2.0",
"description": "Foam",
"repository": "git@github.com:foambubble/foam.git",
"author": "Jani Eväkallio <jani.evakallio@gmail.com>",
"license": "MIT",
"private": "true",
"workspaces": [
@@ -33,7 +32,7 @@
},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
"pre-commit": "yarn lint"
}
},
"prettier": {

View File

@@ -1,11 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -19,7 +19,7 @@ $ npm install -g foam-cli
$ foam COMMAND
running command...
$ foam (-v|--version|version)
foam-cli/0.7.3 darwin-x64 node-v12.18.2
foam-cli/0.9.0 darwin-x64 node-v12.18.2
$ foam --help [COMMAND]
USAGE
$ foam COMMAND
@@ -65,7 +65,7 @@ EXAMPLE
$ foam-cli janitor path-to-foam-workspace
```
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.7.3/src/commands/janitor.ts)_
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.9.0/src/commands/janitor.ts)_
## `foam migrate [WORKSPACEPATH]`
@@ -84,7 +84,7 @@ EXAMPLE
Successfully generated link references and heading!
```
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.7.3/src/commands/migrate.ts)_
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.9.0/src/commands/migrate.ts)_
<!-- commandsstop -->
## Development

View File

@@ -1,8 +1,7 @@
{
"name": "foam-cli",
"description": "Foam CLI",
"version": "0.7.3",
"author": "Jani Eväkallio @jevakallio",
"version": "0.9.0",
"bin": {
"foam": "./bin/run"
},
@@ -11,7 +10,7 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^3",
"foam-core": "^0.7.3",
"foam-core": "^0.9.0",
"ora": "^4.0.4",
"tslib": "^1"
},

View File

@@ -8,6 +8,7 @@ import {
applyTextEdit,
Services,
FileDataStore,
URI,
} from 'foam-core';
import { writeFileToDisk } from '../utils/write-file-to-disk';
import { isValidDirectory } from '../utils';
@@ -40,7 +41,7 @@ export default class Janitor extends Command {
const { workspacePath = './' } = args;
if (isValidDirectory(workspacePath)) {
const config = createConfigFromFolders([workspacePath]);
const config = createConfigFromFolders([URI.file(workspacePath)]);
const services: Services = {
dataStore: new FileDataStore(config),
};
@@ -74,7 +75,7 @@ export default class Janitor extends Command {
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.source.uri, file);
return writeFileToDisk(note.uri, file);
}
return Promise.resolve(null);

View File

@@ -66,7 +66,7 @@ Successfully generated link references and heading!
if (note.title != null) {
const kebabCasedFileName = getKebabCaseFileName(note.title);
if (kebabCasedFileName) {
return renameFile(note.source.uri, kebabCasedFileName);
return renameFile(note.uri, kebabCasedFileName);
}
}
return Promise.resolve(null);
@@ -100,7 +100,7 @@ Successfully generated link references and heading!
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.source.uri, file);
return writeFileToDisk(note.uri, file);
}
return Promise.resolve(null);

View File

@@ -1,15 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import { URI } from 'foam-core';
/**
*
* @param fileUri absolute path for the file that needs to renamed
* @param newFileName "new file name" without the extension
*/
export const renameFile = async (fileUri: string, newFileName: string) => {
const dirName = path.dirname(fileUri);
const extension = path.extname(fileUri);
export const renameFile = async (fileUri: URI, newFileName: string) => {
const filePath = fileUri.fsPath;
const dirName = path.dirname(filePath);
const extension = path.extname(filePath);
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
return fs.promises.rename(fileUri, newFileUri);
return fs.promises.rename(filePath, newFileUri);
};

View File

@@ -1,5 +1,6 @@
import * as fs from 'fs';
import { URI } from 'foam-core';
export const writeFileToDisk = async (fileUri: string, data: string) => {
return fs.promises.writeFile(fileUri, data);
export const writeFileToDisk = async (fileUri: URI, data: string) => {
return fs.promises.writeFile(fileUri.fsPath, data);
};

View File

@@ -1,18 +1,19 @@
import { renameFile } from '../src/utils/rename-file';
import * as fs from 'fs';
import mockFS from 'mock-fs';
import { URI } from 'foam-core';
const doesFileExist = path =>
const doesFileExist = (path: string) =>
fs.promises
.access(path)
.then(() => true)
.catch(() => false);
describe('renameFile', () => {
const fileUri = './test/oldFileName.md';
const fileUri = URI.file('/test/oldFileName.md');
beforeAll(() => {
mockFS({ [fileUri]: '' });
mockFS({ [fileUri.fsPath]: '' });
});
afterAll(() => {
@@ -20,11 +21,11 @@ describe('renameFile', () => {
});
it('should rename existing file', async () => {
expect(await doesFileExist(fileUri)).toBe(true);
expect(await doesFileExist(fileUri.fsPath)).toBe(true);
renameFile(fileUri, 'new-file-name');
expect(await doesFileExist(fileUri)).toBe(false);
expect(await doesFileExist('./test/new-file-name.md')).toBe(true);
expect(await doesFileExist(fileUri.fsPath)).toBe(false);
expect(await doesFileExist('/test/new-file-name.md')).toBe(true);
});
});

View File

@@ -1,23 +1,26 @@
import { writeFileToDisk } from '../src/utils/write-file-to-disk';
import * as fs from 'fs';
import mockFS from 'mock-fs';
import { URI } from 'foam-core';
describe('writeFileToDisk', () => {
const fileUri = './test-file.md';
const fileUri = URI.file('./test-file.md');
beforeAll(() => {
mockFS({ [fileUri]: 'content in the existing file' });
mockFS({ [fileUri.fsPath]: 'content in the existing file' });
});
afterAll(() => {
fs.unlinkSync(fileUri);
fs.unlinkSync(fileUri.fsPath);
mockFS.restore();
});
it('should overrwrite existing file in the disk with the new data', async () => {
const expected = `content in the new file`;
await writeFileToDisk(fileUri, expected);
const actual = await fs.promises.readFile(fileUri, { encoding: 'utf8' });
const actual = await fs.promises.readFile(fileUri.fsPath, {
encoding: 'utf8',
});
expect(actual).toBe(expected);
});
});

View File

@@ -1,20 +1,21 @@
# Foam Core
Repository for tooling used by the other modules
This module contains the core functions, model, and API of Foam.
It is used by its clients to integrate Foam in various use cases, from VsCode extension, to CLI, to CI integrations.
## Local Development
Below is a list of commands you will probably find useful.
### `npm start` or `yarn start`
### `yarn watch`
Runs the project in development/watch mode. Your project will be rebuilt upon changes.
### `npm run build` or `yarn build`
### `yarn build`
Bundles the package to the `dist` folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
### `npm test` or `yarn test`
### `yarn test`
Runs the test watcher (Jest) in an interactive mode.
By default, runs tests related to files changed since the last commit.

View File

@@ -1,8 +1,7 @@
{
"name": "foam-core",
"author": "Jani Eväkallio",
"repository": "https://github.com/foambubble/foam",
"version": "0.7.3",
"version": "0.9.0",
"license": "MIT",
"files": [
"dist"

View File

@@ -1,9 +1,10 @@
import { createGraph } from './note-graph';
import { createGraph } from './model/note-graph';
import { createMarkdownParser } from './markdown-provider';
import { FoamConfig, Foam, Services } from './index';
import { loadPlugins } from './plugins';
import { isSome } from './utils';
import { isDisposable } from './common/lifecycle';
import { Logger } from './utils/log';
export const bootstrap = async (config: FoamConfig, services: Services) => {
const plugins = await loadPlugins(config);
@@ -17,9 +18,12 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
const files = await services.dataStore.listFiles();
await Promise.all(
files.map(async uri => {
const content = await services.dataStore.read(uri);
if (isSome(content)) {
graph.setNote(parser.parse(uri, content));
Logger.info('Found: ' + uri);
if (uri.path.endsWith('md')) {
const content = await services.dataStore.read(uri);
if (isSome(content)) {
graph.setNote(parser.parse(uri, content));
}
}
})
);
@@ -32,9 +36,8 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
const content = await services.dataStore.read(uri);
graph.setNote(await parser.parse(uri, content));
});
services.dataStore.onDidDelete(async uri => {
const note = graph.getNoteByURI(uri);
note && graph.deleteNote(note.id);
services.dataStore.onDidDelete(uri => {
graph.deleteNote(uri);
});
return {

View File

@@ -0,0 +1,436 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export const enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent
U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent
U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent
U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde
U_Combining_Macron = 0x0304, // U+0304 Combining Macron
U_Combining_Overline = 0x0305, // U+0305 Combining Overline
U_Combining_Breve = 0x0306, // U+0306 Combining Breve
U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above
U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis
U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above
U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above
U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent
U_Combining_Caron = 0x030c, // U+030C Combining Caron
U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above
U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above
U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent
U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu
U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve
U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above
U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above
U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above
U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right
U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below
U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below
U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below
U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below
U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above
U_Combining_Horn = 0x031b, // U+031B Combining Horn
U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below
U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below
U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below
U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below
U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below
U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below
U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below
U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below
U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below
U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below
U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below
U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla
U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek
U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below
U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below
U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below
U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below
U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below
U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below
U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below
U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below
U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below
U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line
U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line
U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay
U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay
U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay
U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay
U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay
U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below
U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below
U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below
U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below
U_Combining_X_Above = 0x033d, // U+033D Combining X Above
U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde
U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline
U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark
U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark
U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni
U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis
U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos
U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni
U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above
U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below
U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below
U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below
U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above
U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above
U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above
U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below
U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below
U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner
U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above
U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above
U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata
U_Combining_X_Below = 0x0353, // U+0353 Combining X Below
U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below
U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below
U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below
U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above
U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right
U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below
U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below
U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above
U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below
U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve
U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron
U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below
U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde
U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve
U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below
U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A
U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E
U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I
U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O
U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U
U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C
U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D
U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H
U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M
U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R
U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T
U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V
U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X
/**
* Unicode Character 'LINE SEPARATOR' (U+2028)
* http://www.fileformat.info/info/unicode/char/2028/index.htm
*/
LINE_SEPARATOR = 0x2028,
/**
* Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
* http://www.fileformat.info/info/unicode/char/2029/index.htm
*/
PARAGRAPH_SEPARATOR = 0x2029,
/**
* Unicode Character 'NEXT LINE' (U+0085)
* http://www.fileformat.info/info/unicode/char/0085/index.htm
*/
NEXT_LINE = 0x0085,
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX
U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT
U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS
U_MACRON = 0x00af, // U+00AF MACRON
U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT
U_CEDILLA = 0x00b8, // U+00B8 CEDILLA
U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD
U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD
U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD
U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD
U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING
U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING
U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK
U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK
U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN
U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN
U_BREVE = 0x02d8, // U+02D8 BREVE
U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE
U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE
U_OGONEK = 0x02db, // U+02DB OGONEK
U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE
U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT
U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK
U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT
U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR
U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR
U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR
U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR
U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR
U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK
U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK
U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED
U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD
U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD
U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD
U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD
U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING
U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT
U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE
U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON
U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE
U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE
U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE
U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE
U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF
U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF
U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW
U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN
U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS
U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS
U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS
U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI
U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI
U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI
U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA
U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA
U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI
U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA
U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA
U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI
U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA
U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA
U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA
U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA
U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA
U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE'
/**
* UTF-8 BOM
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
* http://www.fileformat.info/info/unicode/char/feff/index.htm
*/
UTF8_BOM = 65279,
}

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
const LANGUAGE_DEFAULT = 'en';
let _isWindows = false;
let _isMacintosh = false;
let _isLinux = false;
let _isNative = false;
let _isWeb = false;
let _isIOS = false;
let _locale: string | undefined = undefined;
let _language: string = LANGUAGE_DEFAULT;
let _translationsConfigFile: string | undefined = undefined;
let _userAgent: string | undefined = undefined;
interface NLSConfig {
locale: string;
availableLanguages: { [key: string]: string };
_translationsConfigFile: string;
}
export interface IProcessEnvironment {
[key: string]: string;
}
export interface INodeProcess {
platform: 'win32' | 'linux' | 'darwin';
env: IProcessEnvironment;
nextTick: Function;
versions?: {
electron?: string;
};
sandboxed?: boolean; // Electron
type?: string;
cwd(): string;
}
declare const process: INodeProcess;
declare const global: any;
interface INavigator {
userAgent: string;
language: string;
maxTouchPoints?: number;
}
declare const navigator: INavigator;
declare const self: any;
const _globals =
typeof self === 'object'
? self
: typeof global === 'object'
? global
: ({} as any);
let nodeProcess: INodeProcess | undefined = undefined;
if (typeof process !== 'undefined') {
// Native environment (non-sandboxed)
nodeProcess = process;
} else if (typeof _globals.vscode !== 'undefined') {
// Native environment (sandboxed)
nodeProcess = _globals.vscode.process;
}
const isElectronRenderer =
typeof nodeProcess?.versions?.electron === 'string' &&
nodeProcess.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
// Web environment
if (typeof navigator === 'object' && !isElectronRenderer) {
_userAgent = navigator.userAgent;
_isWindows = _userAgent.indexOf('Windows') >= 0;
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
_isIOS =
(_userAgent.indexOf('Macintosh') >= 0 ||
_userAgent.indexOf('iPad') >= 0 ||
_userAgent.indexOf('iPhone') >= 0) &&
!!navigator.maxTouchPoints &&
navigator.maxTouchPoints > 0;
_isLinux = _userAgent.indexOf('Linux') >= 0;
_isWeb = true;
_locale = navigator.language;
_language = _locale;
}
// Native environment
else if (typeof nodeProcess === 'object') {
_isWindows = nodeProcess.platform === 'win32';
_isMacintosh = nodeProcess.platform === 'darwin';
_isLinux = nodeProcess.platform === 'linux';
_locale = LANGUAGE_DEFAULT;
_language = LANGUAGE_DEFAULT;
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
if (rawNlsConfig) {
try {
const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig);
const resolved = nlsConfig.availableLanguages['*'];
_locale = nlsConfig.locale;
// VSCode's default language is 'en'
_language = resolved ? resolved : LANGUAGE_DEFAULT;
_translationsConfigFile = nlsConfig._translationsConfigFile;
} catch (e) {}
}
_isNative = true;
}
// Unknown environment
else {
console.error('Unable to resolve platform.');
}
export const enum Platform {
Web,
Mac,
Linux,
Windows,
}
export function PlatformToString(platform: Platform) {
switch (platform) {
case Platform.Web:
return 'Web';
case Platform.Mac:
return 'Mac';
case Platform.Linux:
return 'Linux';
case Platform.Windows:
return 'Windows';
}
}
let _platform: Platform = Platform.Web;
if (_isMacintosh) {
_platform = Platform.Mac;
} else if (_isWindows) {
_platform = Platform.Windows;
} else if (_isLinux) {
_platform = Platform.Linux;
}
export const isWindows = _isWindows;
export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isNative = _isNative;
export const isWeb = _isWeb;
export const isIOS = _isIOS;
export const platform = _platform;
export const userAgent = _userAgent;
/**
* The language used for the user interface. The format of
* the string is all lower case (e.g. zh-tw for Traditional
* Chinese)
*/
export const language = _language;
export namespace Language {
export function value(): string {
return language;
}
export function isDefaultVariant(): boolean {
if (language.length === 2) {
return language === 'en';
} else if (language.length >= 3) {
return language[0] === 'e' && language[1] === 'n' && language[2] === '-';
} else {
return false;
}
}
export function isDefault(): boolean {
return language === 'en';
}
}
/**
* The OS locale or the locale specified by --locale. The format of
* the string is all lower case (e.g. zh-tw for Traditional
* Chinese). The UI is not necessarily shown in the provided locale.
*/
export const locale = _locale;
/**
* The translatios that are available through language packs.
*/
export const translationsConfigFile = _translationsConfigFile;
export const globals: any = _globals;
interface ISetImmediate {
(callback: (...args: any[]) => void): void;
}

View File

@@ -0,0 +1,748 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
import { isWindows } from './platform';
import { CharCode } from './charCode';
import * as paths from 'path';
const _schemePattern = /^\w[\w\d+.-]*$/;
const _singleSlashStart = /^\//;
const _doubleSlashStart = /^\/\//;
function _validateUri(ret: URI, _strict?: boolean): void {
// scheme, must be set
if (!ret.scheme && _strict) {
throw new Error(
`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`
);
}
// scheme, https://tools.ietf.org/html/rfc3986#section-3.1
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
if (ret.scheme && !_schemePattern.test(ret.scheme)) {
throw new Error('[UriError]: Scheme contains illegal characters.');
}
// path, http://tools.ietf.org/html/rfc3986#section-3.3
// If a URI contains an authority component, then the path component
// must either be empty or begin with a slash ("/") character. If a URI
// does not contain an authority component, then the path cannot begin
// with two slash characters ("//").
if (ret.path) {
if (ret.authority) {
if (!_singleSlashStart.test(ret.path)) {
throw new Error(
'[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'
);
}
} else {
if (_doubleSlashStart.test(ret.path)) {
throw new Error(
'[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'
);
}
}
}
}
// for a while we allowed uris *without* schemes and this is the migration
// for them, e.g. an uri without scheme and without strict-mode warns and falls
// back to the file-scheme. that should cause the least carnage and still be a
// clear warning
function _schemeFix(scheme: string, _strict: boolean): string {
if (!scheme && !_strict) {
return 'file';
}
return scheme;
}
// implements a bit of https://tools.ietf.org/html/rfc3986#section-5
function _referenceResolution(scheme: string, path: string): string {
// the slash-character is our 'default base' as we don't
// support constructing URIs relative to other URIs. This
// also means that we alter and potentially break paths.
// see https://tools.ietf.org/html/rfc3986#section-5.1.4
switch (scheme) {
case 'https':
case 'http':
case 'file':
if (!path) {
path = _slash;
} else if (path[0] !== _slash) {
path = _slash + path;
}
break;
}
return path;
}
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
/**
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
* This class is a simple parser which creates the basic component parts
* (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
* and encoding.
*
* ```txt
* foo://example.com:8042/over/there?name=ferret#nose
* \_/ \______________/\_________/ \_________/ \__/
* | | | | |
* scheme authority path query fragment
* | _____________________|__
* / \ / \
* urn:example:animal:ferret:nose
* ```
*/
export class URI implements UriComponents {
static isUri(thing: any): thing is URI {
if (thing instanceof URI) {
return true;
}
if (!thing) {
return false;
}
return (
typeof (thing as URI).authority === 'string' &&
typeof (thing as URI).fragment === 'string' &&
typeof (thing as URI).path === 'string' &&
typeof (thing as URI).query === 'string' &&
typeof (thing as URI).scheme === 'string' &&
typeof (thing as URI).fsPath === 'function' &&
typeof (thing as URI).with === 'function' &&
typeof (thing as URI).toString === 'function'
);
}
/**
* scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'.
* The part before the first colon.
*/
readonly scheme: string;
/**
* authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'.
* The part between the first double slashes and the next slash.
*/
readonly authority: string;
/**
* path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly path: string;
/**
* query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly query: string;
/**
* fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly fragment: string;
/**
* @internal
*/
protected constructor(
scheme: string,
authority?: string,
path?: string,
query?: string,
fragment?: string,
_strict?: boolean
);
/**
* @internal
*/
protected constructor(components: UriComponents);
/**
* @internal
*/
protected constructor(
schemeOrData: string | UriComponents,
authority?: string,
path?: string,
query?: string,
fragment?: string,
_strict: boolean = false
) {
if (typeof schemeOrData === 'object') {
this.scheme = schemeOrData.scheme || _empty;
this.authority = schemeOrData.authority || _empty;
this.path = schemeOrData.path || _empty;
this.query = schemeOrData.query || _empty;
this.fragment = schemeOrData.fragment || _empty;
// no validation because it's this URI
// that creates uri components.
// _validateUri(this);
} else {
this.scheme = _schemeFix(schemeOrData, _strict);
this.authority = authority || _empty;
this.path = _referenceResolution(this.scheme, path || _empty);
this.query = query || _empty;
this.fragment = fragment || _empty;
_validateUri(this, _strict);
}
}
// ---- filesystem path -----------------------
/**
* Returns a string representing the corresponding file system path of this URI.
* Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
* platform specific path separator.
*
* * Will *not* validate the path for invalid characters and semantics.
* * Will *not* look at the scheme of this URI.
* * The result shall *not* be used for display purposes but for accessing a file on disk.
*
*
* The *difference* to `URI#path` is the use of the platform specific separator and the handling
* of UNC paths. See the below sample of a file-uri with an authority (UNC path).
*
* ```ts
const u = URI.parse('file://server/c$/folder/file.txt')
u.authority === 'server'
u.path === '/shares/c$/file.txt'
u.fsPath === '\\server\c$\folder\file.txt'
```
*
* Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
* namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
* with URIs that represent files on disk (`file` scheme).
*/
get fsPath(): string {
// if (this.scheme !== 'file') {
// console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
// }
return uriToFsPath(this, false);
}
// ---- modify to new -------------------------
with(change: {
scheme?: string;
authority?: string | null;
path?: string | null;
query?: string | null;
fragment?: string | null;
}): URI {
if (!change) {
return this;
}
let { scheme, authority, path, query, fragment } = change;
if (scheme === undefined) {
scheme = this.scheme;
} else if (scheme === null) {
scheme = _empty;
}
if (authority === undefined) {
authority = this.authority;
} else if (authority === null) {
authority = _empty;
}
if (path === undefined) {
path = this.path;
} else if (path === null) {
path = _empty;
}
if (query === undefined) {
query = this.query;
} else if (query === null) {
query = _empty;
}
if (fragment === undefined) {
fragment = this.fragment;
} else if (fragment === null) {
fragment = _empty;
}
if (
scheme === this.scheme &&
authority === this.authority &&
path === this.path &&
query === this.query &&
fragment === this.fragment
) {
return this;
}
return new Uri(scheme, authority, path, query, fragment);
}
// ---- parse & validate ------------------------
/**
* Creates a new URI from a string, e.g. `http://www.msft.com/some/path`,
* `file:///usr/home`, or `scheme:with/path`.
*
* @param value A string which represents an URI (see `URI#toString`).
*/
static parse(value: string, _strict: boolean = false): URI {
const match = _regexp.exec(value);
if (!match) {
return new Uri(_empty, _empty, _empty, _empty, _empty);
}
return new Uri(
match[2] || _empty,
percentDecode(match[4] || _empty),
percentDecode(match[5] || _empty),
percentDecode(match[7] || _empty),
percentDecode(match[9] || _empty),
_strict
);
}
/**
* Creates a new URI from a file system path, e.g. `c:\my\files`,
* `/usr/home`, or `\\server\share\some\path`.
*
* The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
* as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
* `URI.parse('file://' + path)` because the path might contain characters that are
* interpreted (# and ?). See the following sample:
* ```ts
const good = URI.file('/coding/c#/project1');
good.scheme === 'file';
good.path === '/coding/c#/project1';
good.fragment === '';
const bad = URI.parse('file://' + '/coding/c#/project1');
bad.scheme === 'file';
bad.path === '/coding/c'; // path is now broken
bad.fragment === '/project1';
```
*
* @param path A file system path (see `URI#fsPath`)
*/
static file(path: string): URI {
let authority = _empty;
// normalize to fwd-slashes on windows,
// on other systems bwd-slashes are valid
// filename character, eg /f\oo/ba\r.txt
if (isWindows) {
path = path.replace(/\\/g, _slash);
}
// check for authority as used in UNC shares
// or use the path as given
if (path[0] === _slash && path[1] === _slash) {
const idx = path.indexOf(_slash, 2);
if (idx === -1) {
authority = path.substring(2);
path = _slash;
} else {
authority = path.substring(2, idx);
path = path.substring(idx) || _slash;
}
}
return new Uri('file', authority, path, _empty, _empty);
}
static from(components: {
scheme: string;
authority?: string;
path?: string;
query?: string;
fragment?: string;
}): URI {
return new Uri(
components.scheme,
components.authority,
components.path,
components.query,
components.fragment
);
}
/**
* Join a URI path with path fragments and normalizes the resulting path.
*
* @param uri The input URI.
* @param pathFragment The path fragment to add to the URI path.
* @returns The resulting URI.
*/
static joinPath(uri: URI, ...pathFragment: string[]): URI {
if (!uri.path) {
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
}
let newPath: string;
if (isWindows && uri.scheme === 'file') {
newPath = URI.file(
paths.win32.join(uriToFsPath(uri, true), ...pathFragment)
).path;
} else {
newPath = paths.posix.join(uri.path, ...pathFragment);
}
return uri.with({ path: newPath });
}
// ---- printing/externalize ---------------------------
/**
* Creates a string representation for this URI. It's guaranteed that calling
* `URI.parse` with the result of this function creates an URI which is equal
* to this URI.
*
* * The result shall *not* be used for display purposes but for externalization or transport.
* * The result will be encoded using the percentage encoding and encoding happens mostly
* ignore the scheme-specific encoding rules.
*
* @param skipEncoding Do not encode the result, default is `false`
*/
toString(skipEncoding: boolean = false): string {
return _asFormatted(this, skipEncoding);
}
toJSON(): UriComponents {
return this;
}
static revive(data: UriComponents | URI): URI;
static revive(data: UriComponents | URI | undefined): URI | undefined;
static revive(data: UriComponents | URI | null): URI | null;
static revive(
data: UriComponents | URI | undefined | null
): URI | undefined | null;
static revive(
data: UriComponents | URI | undefined | null
): URI | undefined | null {
if (!data) {
return data;
} else if (data instanceof URI) {
return data;
} else {
const result = new Uri(data);
result._formatted = (data as UriState).external;
result._fsPath =
(data as UriState)._sep === _pathSepMarker
? (data as UriState).fsPath
: null;
return result;
}
}
}
export interface UriComponents {
scheme: string;
authority: string;
path: string;
query: string;
fragment: string;
}
interface UriState extends UriComponents {
$mid: number;
external: string;
fsPath: string;
_sep: 1 | undefined;
}
const _pathSepMarker = isWindows ? 1 : undefined;
// This class exists so that URI is compatibile with vscode.Uri (API).
class Uri extends URI {
_formatted: string | null = null;
_fsPath: string | null = null;
get fsPath(): string {
if (!this._fsPath) {
this._fsPath = uriToFsPath(this, false);
}
return this._fsPath;
}
toString(skipEncoding: boolean = false): string {
if (!skipEncoding) {
if (!this._formatted) {
this._formatted = _asFormatted(this, false);
}
return this._formatted;
} else {
// we don't cache that
return _asFormatted(this, true);
}
}
toJSON(): UriComponents {
const res = {
$mid: 1,
} as UriState;
// cached state
if (this._fsPath) {
res.fsPath = this._fsPath;
res._sep = _pathSepMarker;
}
if (this._formatted) {
res.external = this._formatted;
}
// uri components
if (this.path) {
res.path = this.path;
}
if (this.scheme) {
res.scheme = this.scheme;
}
if (this.authority) {
res.authority = this.authority;
}
if (this.query) {
res.query = this.query;
}
if (this.fragment) {
res.fragment = this.fragment;
}
return res;
}
}
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
const encodeTable: { [ch: number]: string } = {
[CharCode.Colon]: '%3A', // gen-delims
[CharCode.Slash]: '%2F',
[CharCode.QuestionMark]: '%3F',
[CharCode.Hash]: '%23',
[CharCode.OpenSquareBracket]: '%5B',
[CharCode.CloseSquareBracket]: '%5D',
[CharCode.AtSign]: '%40',
[CharCode.ExclamationMark]: '%21', // sub-delims
[CharCode.DollarSign]: '%24',
[CharCode.Ampersand]: '%26',
[CharCode.SingleQuote]: '%27',
[CharCode.OpenParen]: '%28',
[CharCode.CloseParen]: '%29',
[CharCode.Asterisk]: '%2A',
[CharCode.Plus]: '%2B',
[CharCode.Comma]: '%2C',
[CharCode.Semicolon]: '%3B',
[CharCode.Equals]: '%3D',
[CharCode.Space]: '%20',
};
function encodeURIComponentFast(
uriComponent: string,
allowSlash: boolean
): string {
let res: string | undefined = undefined;
let nativeEncodePos = -1;
for (let pos = 0; pos < uriComponent.length; pos++) {
const code = uriComponent.charCodeAt(pos);
// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
if (
(code >= CharCode.a && code <= CharCode.z) ||
(code >= CharCode.A && code <= CharCode.Z) ||
(code >= CharCode.Digit0 && code <= CharCode.Digit9) ||
code === CharCode.Dash ||
code === CharCode.Period ||
code === CharCode.Underline ||
code === CharCode.Tilde ||
(allowSlash && code === CharCode.Slash)
) {
// check if we are delaying native encode
if (nativeEncodePos !== -1) {
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
nativeEncodePos = -1;
}
// check if we write into a new string (by default we try to return the param)
if (res !== undefined) {
res += uriComponent.charAt(pos);
}
} else {
// encoding needed, we need to allocate a new string
if (res === undefined) {
res = uriComponent.substr(0, pos);
}
// check with default table first
const escaped = encodeTable[code];
if (escaped !== undefined) {
// check if we are delaying native encode
if (nativeEncodePos !== -1) {
res += encodeURIComponent(
uriComponent.substring(nativeEncodePos, pos)
);
nativeEncodePos = -1;
}
// append escaped variant to result
res += escaped;
} else if (nativeEncodePos === -1) {
// use native encode only when needed
nativeEncodePos = pos;
}
}
}
if (nativeEncodePos !== -1) {
res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
}
return res !== undefined ? res : uriComponent;
}
function encodeURIComponentMinimal(path: string): string {
let res: string | undefined = undefined;
for (let pos = 0; pos < path.length; pos++) {
const code = path.charCodeAt(pos);
if (code === CharCode.Hash || code === CharCode.QuestionMark) {
if (res === undefined) {
res = path.substr(0, pos);
}
res += encodeTable[code];
} else {
if (res !== undefined) {
res += path[pos];
}
}
}
return res !== undefined ? res : path;
}
/**
* Compute `fsPath` for the given uri
*/
export function uriToFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
let value: string;
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
// unc path: file://shares/c$/far/boo
value = `//${uri.authority}${uri.path}`;
} else if (
uri.path.charCodeAt(0) === CharCode.Slash &&
((uri.path.charCodeAt(1) >= CharCode.A &&
uri.path.charCodeAt(1) <= CharCode.Z) ||
(uri.path.charCodeAt(1) >= CharCode.a &&
uri.path.charCodeAt(1) <= CharCode.z)) &&
uri.path.charCodeAt(2) === CharCode.Colon
) {
if (!keepDriveLetterCasing) {
// windows drive letter: file:///c:/far/boo
value = uri.path[1].toLowerCase() + uri.path.substr(2);
} else {
value = uri.path.substr(1);
}
} else {
// other path
value = uri.path;
}
if (isWindows) {
value = value.replace(/\//g, '\\');
}
return value;
}
/**
* Create the external version of a uri
*/
function _asFormatted(uri: URI, skipEncoding: boolean): string {
const encoder = !skipEncoding
? encodeURIComponentFast
: encodeURIComponentMinimal;
let res = '';
let { scheme, authority, path, query, fragment } = uri;
if (scheme) {
res += scheme;
res += ':';
}
if (authority || scheme === 'file') {
res += _slash;
res += _slash;
}
if (authority) {
let idx = authority.indexOf('@');
if (idx !== -1) {
// <user>@<auth>
const userinfo = authority.substr(0, idx);
authority = authority.substr(idx + 1);
idx = userinfo.indexOf(':');
if (idx === -1) {
res += encoder(userinfo, false);
} else {
// <user>:<pass>@<auth>
res += encoder(userinfo.substr(0, idx), false);
res += ':';
res += encoder(userinfo.substr(idx + 1), false);
}
res += '@';
}
authority = authority.toLowerCase();
idx = authority.indexOf(':');
if (idx === -1) {
res += encoder(authority, false);
} else {
// <auth>:<port>
res += encoder(authority.substr(0, idx), false);
res += authority.substr(idx);
}
}
if (path) {
// lower-case windows drive letters in /C:/fff or C:/fff
if (
path.length >= 3 &&
path.charCodeAt(0) === CharCode.Slash &&
path.charCodeAt(2) === CharCode.Colon
) {
const code = path.charCodeAt(1);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
}
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
const code = path.charCodeAt(0);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
}
}
// encode the rest of the path
res += encoder(path, true);
}
if (query) {
res += '?';
res += encoder(query, false);
}
if (fragment) {
res += '#';
res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment;
}
return res;
}
// --- decode
function decodeURIComponentGraceful(str: string): string {
try {
return decodeURIComponent(str);
} catch {
if (str.length > 3) {
return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3));
} else {
return str;
}
}
}
const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
function percentDecode(str: string): string {
if (!str.match(_rEncodedAsHex)) {
return str;
}
return str.replace(_rEncodedAsHex, match =>
decodeURIComponentGraceful(match)
);
}

View File

@@ -1,8 +1,10 @@
import { readFileSync } from 'fs';
import { merge } from 'lodash';
import { Logger } from './utils/log';
import { URI } from './common/uri';
export interface FoamConfig {
workspaceFolders: string[];
workspaceFolders: URI[];
includeGlobs: string[];
ignoreGlobs: string[];
get<T>(path: string): T | undefined;
@@ -14,7 +16,7 @@ const DEFAULT_INCLUDES = ['**/*'];
const DEFAULT_IGNORES = ['**/node_modules/**'];
export const createConfigFromObject = (
workspaceFolders: string[],
workspaceFolders: URI[],
include: string[],
ignore: string[],
settings: any
@@ -33,7 +35,7 @@ export const createConfigFromObject = (
};
export const createConfigFromFolders = (
workspaceFolders: string[] | string,
workspaceFolders: URI[] | URI,
options: {
include?: string[];
ignore?: string[];
@@ -43,7 +45,7 @@ export const createConfigFromFolders = (
workspaceFolders = [workspaceFolders];
}
const workspaceConfig: any = workspaceFolders.reduce(
(acc, f) => merge(acc, parseConfig(`${f}/config.json`)),
(acc, f) => merge(acc, parseConfig(URI.joinPath(f, 'config.json'))),
{}
);
// For security reasons local plugins can only be
@@ -52,7 +54,7 @@ export const createConfigFromFolders = (
delete workspaceConfig['experimental']['localPlugins'];
}
const userConfig = parseConfig(`~/.foam/config.json`);
const userConfig = parseConfig(URI.file(`~/.foam/config.json`));
const settings = merge(workspaceConfig, userConfig);
@@ -64,10 +66,10 @@ export const createConfigFromFolders = (
);
};
const parseConfig = (path: string) => {
const parseConfig = (path: URI) => {
try {
return JSON.parse(readFileSync(path, 'utf8'));
return JSON.parse(readFileSync(path.fsPath, 'utf8'));
} catch {
console.warn('Could not read configuration from ' + path);
Logger.debug('Could not read configuration from ' + path);
}
};

View File

@@ -1,2 +0,0 @@
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;

View File

@@ -1,16 +1,19 @@
import { Note, NoteLink, URI } from './types';
import { NoteGraph, NoteGraphAPI } from './note-graph';
import { Note, NoteLink } from './model/note';
import { URI } from './common/uri';
import { NoteGraph, NoteGraphAPI } from './model/note-graph';
import { FoamConfig } from './config';
import { IDataStore, FileDataStore } from './services/datastore';
import { ILogger } from './utils/log';
import { IDisposable, isDisposable } from './common/lifecycle';
export { IDataStore, FileDataStore };
export { ILogger };
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
export { IDisposable, isDisposable } from './common/lifecycle';
export { Event, Emitter } from './common/event';
export { FoamConfig };
export { IDisposable, isDisposable };
export {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
@@ -21,6 +24,8 @@ export {
generateHeading,
generateLinkReferences,
getKebabCaseFileName,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './janitor';
export { applyTextEdit } from './janitor/apply-text-edit';
@@ -31,16 +36,11 @@ export { bootstrap } from './bootstrap';
export { NoteGraph, NoteGraphAPI, Note, NoteLink, URI };
export {
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './definitions';
export interface Services {
dataStore: IDataStore;
}
export interface Foam {
export interface Foam extends IDisposable {
notes: NoteGraphAPI;
config: FoamConfig;
parse: (uri: URI, text: string, eol: string) => Note;

View File

@@ -1,16 +1,15 @@
import { Position } from 'unist';
import GithubSlugger from 'github-slugger';
import { GraphNote, NoteGraphAPI } from '../note-graph';
import { Note } from '../types';
import {
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from '../definitions';
import { NoteGraphAPI } from '../model/note-graph';
import { Note } from '../model/note';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../markdown-provider';
import { getHeadingFromFileName } from '../utils';
import { getHeadingFromFileName, uriToSlug } from '../utils';
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
const slugger = new GithubSlugger();
@@ -20,7 +19,7 @@ export interface TextEdit {
}
export const generateLinkReferences = (
note: GraphNote,
note: Note,
ng: NoteGraphAPI,
includeExtensions: boolean
): TextEdit | null => {
@@ -30,7 +29,7 @@ export const generateLinkReferences = (
const markdownReferences = createMarkdownReferences(
ng,
note.id,
note.uri,
includeExtensions
);
@@ -113,7 +112,7 @@ export const generateHeading = (note: Note): TextEdit | null => {
return {
newText: `${paddingStart}# ${getHeadingFromFileName(
note.slug
uriToSlug(note.uri)
)}${paddingEnd}`,
range: {
start: note.source.contentStart,

View File

@@ -1,3 +1,4 @@
import { Node } from 'unist';
import unified from 'unified';
import markdownParse from 'remark-parse';
import wikiLinkPlugin from 'remark-wiki-link';
@@ -7,18 +8,34 @@ import visit from 'unist-util-visit';
import { Parent, Point } from 'unist';
import detectNewline from 'detect-newline';
import os from 'os';
import * as path from 'path';
import { NoteGraphAPI } from './note-graph';
import { NoteLinkDefinition, Note, NoteParser } from './types';
import { NoteGraphAPI } from './model/note-graph';
import { NoteLinkDefinition, Note, NoteParser } from './model/note';
import { dropExtension, extractHashtags, extractTagsFromProp } from './utils';
import {
dropExtension,
uriToSlug,
extractHashtags,
extractTagsFromProp,
} from './utils';
import { ID } from './types';
computeRelativePath,
getBasename,
parseUri,
} from './utils/uri';
import { ParserPlugin } from './plugins';
import { Logger } from './utils/log';
import { URI } from './common/uri';
/**
* Traverses all the children of the given node, extracts
* the text from them, and returns it concatenated.
*
* @param root the node from which to start collecting text
*/
const getTextFromChildren = (root: Node): string => {
let text = '';
visit(root, 'text', node => {
if (node.type === 'text') {
text = text + node.value;
}
});
return text;
};
const tagsPlugin: ParserPlugin = {
name: 'tags',
@@ -45,7 +62,7 @@ const titlePlugin: ParserPlugin = {
},
onDidVisitTree: (tree, note) => {
if (note.title == null) {
note.title = path.parse(note.source.uri).name;
note.title = getBasename(note.uri);
}
},
};
@@ -60,6 +77,19 @@ const wikilinkPlugin: ParserPlugin = {
position: node.position!,
});
}
if (node.type === 'link') {
const targetUri = (node as any).url;
const uri = parseUri(note.uri, targetUri);
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
return;
}
const label = getTextFromChildren(node);
note.links.push({
type: 'link',
target: targetUri,
label: label,
});
}
},
};
@@ -83,12 +113,12 @@ const definitionsPlugin: ParserPlugin = {
const handleError = (
plugin: ParserPlugin,
fnName: string,
uri: string | undefined,
uri: URI | undefined,
e: Error
): void => {
const name = plugin.name || '';
Logger.warn(
`Error while executing [${fnName}] in plugin [${name}] for file [${uri}]`,
`Error while executing [${fnName}] in plugin [${name}] for file [${uri?.path}]`,
e
);
};
@@ -116,7 +146,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
});
const foamParser: NoteParser = {
parse: (uri: string, markdown: string): Note => {
parse: (uri: URI, markdown: string): Note => {
Logger.debug('Parsing:', uri);
markdown = plugins.reduce((acc, plugin) => {
try {
@@ -130,14 +160,13 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
const eol = detectNewline(markdown) || os.EOL;
var note: Note = {
slug: uriToSlug(uri),
uri: uri,
properties: {},
title: null,
tags: new Set(),
links: [],
definitions: [],
source: {
uri: uri,
text: markdown,
contentStart: tree.position!.start,
end: tree.position!.end,
@@ -240,23 +269,26 @@ export function stringifyMarkdownLinkReferenceDefinition(
}
export function createMarkdownReferences(
graph: NoteGraphAPI,
noteId: ID,
noteUri: URI,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = graph.getNote(noteId);
const source = graph.getNote(noteUri);
// Should never occur since we're already in a file,
// but better safe than sorry.
if (!source) {
console.warn(
`Note ${noteId} was not added to NoteGraph before attempting to generate markdown reference list`
`Note ${noteUri} was not added to NoteGraph before attempting to generate markdown reference list`
);
return [];
}
return graph
.getForwardLinks(noteId)
.getForwardLinks(noteUri)
.map(link => {
if (link.link.type !== 'wikilink') {
return null;
}
let target = graph.getNote(link.to);
// if we don't find the target by ID we search the graph by slug
if (!target) {
@@ -272,15 +304,12 @@ export function createMarkdownReferences(
// but int the future we may want to surface these too
if (!target) {
Logger.info(
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
`Warning: Link '${link.to}' in '${noteUri}' points to a non-existing note.`
);
return null;
}
const relativePath = path.relative(
path.dirname(source.source.uri),
target.source.uri
);
const relativePath = computeRelativePath(source.uri, target.uri);
const pathToNote = includeExtension
? relativePath
@@ -290,7 +319,7 @@ export function createMarkdownReferences(
return {
label: link.link.slug,
url: pathToNote,
title: target.title || target.slug,
title: target.title || uriToSlug(target.uri),
};
})
.filter(Boolean)

View File

@@ -0,0 +1,171 @@
import { Graph } from 'graphlib';
import { URI } from '../common/uri';
import { Note, NoteLink } from '../model/note';
import {
computeRelativeURI,
nameToSlug,
isSome,
uriToSlug,
parseUri,
} from '../utils';
import { Event, Emitter } from '../common/event';
export interface GraphConnection {
from: URI;
to: URI;
link: NoteLink;
}
export type NoteGraphEventHandler = (e: { note: Note }) => void;
export type NotesQuery = { slug: string } | { title: string };
export interface NoteGraphAPI {
setNote(note: Note): Note;
deleteNote(noteUri: URI): Note | null;
getNotes(query?: NotesQuery): Note[];
getNote(noteUri: URI): Note | null;
getAllLinks(noteUri: URI): GraphConnection[];
getForwardLinks(noteUri: URI): GraphConnection[];
getBacklinks(noteUri: URI): GraphConnection[];
onDidAddNote: Event<Note>;
onDidUpdateNote: Event<Note>;
onDidDeleteNote: Event<Note>;
}
export type Middleware = (next: NoteGraphAPI) => Partial<NoteGraphAPI>;
export const createGraph = (middlewares: Middleware[]): NoteGraphAPI => {
const graph: NoteGraphAPI = new NoteGraph();
return middlewares.reduce((acc, m) => backfill(acc, m), graph);
};
const uriToId = (uri: URI) => uri.path;
export class NoteGraph implements NoteGraphAPI {
onDidAddNote: Event<Note>;
onDidUpdateNote: Event<Note>;
onDidDeleteNote: Event<Note>;
private graph: Graph;
private onDidAddNoteEmitter = new Emitter<Note>();
private onDidUpdateNoteEmitter = new Emitter<Note>();
private onDidDeleteEmitter = new Emitter<Note>();
constructor() {
this.graph = new Graph();
this.onDidAddNote = this.onDidAddNoteEmitter.event;
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
}
public setNote(note: Note): Note {
const oldNote = this.getNote(note.uri);
if (isSome(oldNote)) {
this.removeForwardLinks(note.uri);
}
this.graph.setNode(uriToId(note.uri), note);
note.links.forEach(link => {
let targetUri = null;
if (link.type === 'wikilink') {
const definitionUri = note.definitions.find(
def => def.label === link.slug
)?.url;
targetUri = computeRelativeURI(note.uri, definitionUri ?? link.slug);
} else {
targetUri = parseUri(note.uri, link.target);
}
const connection: GraphConnection = {
from: note.uri,
to: targetUri,
link: link,
};
this.graph.setEdge(uriToId(note.uri), uriToId(targetUri), connection);
});
isSome(oldNote)
? this.onDidUpdateNoteEmitter.fire(note)
: this.onDidAddNoteEmitter.fire(note);
return note;
}
public deleteNote(noteUri: URI): Note | null {
return this.doDelete(noteUri, true);
}
private doDelete(noteUri: URI, fireEvent: boolean): Note | null {
const note = this.getNote(noteUri);
if (isSome(note)) {
if (this.getBacklinks(noteUri).length >= 1) {
this.graph.setNode(uriToId(noteUri), null); // Changes node to the "no file" style
} else {
this.graph.removeNode(uriToId(noteUri));
}
fireEvent && this.onDidDeleteEmitter.fire(note);
}
return note;
}
public getNotes(query?: NotesQuery): Note[] {
// prettier-ignore
const filterFn =
query == null ? (note: Note | null) => note != null
: 'slug' in query ? (note: Note | null) => note && [nameToSlug(query.slug), query.slug].includes(uriToSlug(note.uri))
: 'title' in query ? (note: Note | null) => note?.title === query.title
: (note: Note | null) => note != null;
return this.graph
.nodes()
.map(id => this.graph.node(id))
.filter(filterFn);
}
public getNote(noteUri: URI): Note | null {
return this.graph.node(uriToId(noteUri)) ?? null;
}
public getAllLinks(noteUri: URI): GraphConnection[] {
return (this.graph.nodeEdges(uriToId(noteUri)) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public getForwardLinks(noteUri: URI): GraphConnection[] {
return (this.graph.outEdges(uriToId(noteUri)) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public removeForwardLinks(noteUri: URI) {
(this.graph.outEdges(uriToId(noteUri)) || []).forEach(edge => {
this.graph.removeEdge(edge);
});
}
public getBacklinks(noteUri: URI): GraphConnection[] {
return (this.graph.inEdges(uriToId(noteUri)) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public dispose() {
this.onDidAddNoteEmitter.dispose();
this.onDidUpdateNoteEmitter.dispose();
this.onDidDeleteEmitter.dispose();
}
}
const backfill = (next: NoteGraphAPI, middleware: Middleware): NoteGraphAPI => {
const m = middleware(next);
return {
setNote: m.setNote || next.setNote,
deleteNote: m.deleteNote || next.deleteNote,
getNotes: m.getNotes || next.getNotes,
getNote: m.getNote || next.getNote,
getAllLinks: m.getAllLinks || next.getAllLinks,
getForwardLinks: m.getForwardLinks || next.getForwardLinks,
getBacklinks: m.getBacklinks || next.getBacklinks,
onDidAddNote: next.onDidAddNote,
onDidUpdateNote: next.onDidUpdateNote,
onDidDeleteNote: next.onDidDeleteNote,
};
};

View File

@@ -1,13 +1,8 @@
// this file can't simply be .d.ts because the TS compiler wouldn't copy it to the dist directory
// see https://stackoverflow.com/questions/56018167/typescript-does-not-copy-d-ts-files-to-build
import { Position, Point } from 'unist';
import { URI } from '../common/uri';
export { Position, Point };
export type URI = string;
export type ID = string;
export interface NoteSource {
uri: URI;
text: string;
contentStart: Point;
end: Point;
@@ -20,8 +15,13 @@ export interface WikiLink {
position: Position;
}
// at the moment we only model wikilink
export type NoteLink = WikiLink;
export interface DirectLink {
type: 'link';
label: string;
target: string;
}
export type NoteLink = WikiLink | DirectLink;
export interface NoteLinkDefinition {
label: string;
@@ -31,8 +31,8 @@ export interface NoteLinkDefinition {
}
export interface Note {
uri: URI;
title: string | null;
slug: string; // note: this slug is not necessarily unique
properties: any;
// sections: NoteSection[]
tags: Set<string>;
@@ -42,5 +42,5 @@ export interface Note {
}
export interface NoteParser {
parse: (uri: string, text: string) => Note;
parse: (uri: URI, text: string) => Note;
}

View File

@@ -1,174 +0,0 @@
import { Graph } from 'graphlib';
import { URI, ID, Note, NoteLink } from './types';
import { computeRelativeURI, nameToSlug, isSome } from './utils';
import { Event, Emitter } from './common/event';
export type GraphNote = Note & {
id: ID;
};
export interface GraphConnection {
from: ID;
to: ID;
link: NoteLink;
}
export type NoteGraphEventHandler = (e: { note: GraphNote }) => void;
export type NotesQuery = { slug: string } | { title: string };
export interface NoteGraphAPI {
setNote(note: Note): GraphNote;
deleteNote(noteId: ID): GraphNote | null;
getNotes(query?: NotesQuery): GraphNote[];
getNote(noteId: ID): GraphNote | null;
getNoteByURI(uri: URI): GraphNote | null;
getAllLinks(noteId: ID): GraphConnection[];
getForwardLinks(noteId: ID): GraphConnection[];
getBacklinks(noteId: ID): GraphConnection[];
onDidAddNote: Event<GraphNote>;
onDidUpdateNote: Event<GraphNote>;
onDidDeleteNote: Event<GraphNote>;
}
export type Middleware = (next: NoteGraphAPI) => Partial<NoteGraphAPI>;
export const createGraph = (middlewares: Middleware[]): NoteGraphAPI => {
const graph: NoteGraphAPI = new NoteGraph();
return middlewares.reduce((acc, m) => backfill(acc, m), graph);
};
export class NoteGraph implements NoteGraphAPI {
onDidAddNote: Event<GraphNote>;
onDidUpdateNote: Event<GraphNote>;
onDidDeleteNote: Event<GraphNote>;
private graph: Graph;
private createIdFromURI: (uri: URI) => ID;
private onDidAddNoteEmitter = new Emitter<GraphNote>();
private onDidUpdateNoteEmitter = new Emitter<GraphNote>();
private onDidDeleteEmitter = new Emitter<GraphNote>();
constructor() {
this.graph = new Graph();
this.onDidAddNote = this.onDidAddNoteEmitter.event;
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
this.createIdFromURI = uri => uri;
}
public setNote(note: Note): GraphNote {
const id = this.createIdFromURI(note.source.uri);
const oldNote = this.getNote(id);
if (isSome(oldNote)) {
this.removeForwardLinks(id);
}
const graphNote: GraphNote = {
...note,
id: id,
};
this.graph.setNode(id, graphNote);
note.links.forEach(link => {
const relativePath =
note.definitions.find(def => def.label === link.slug)?.url ?? link.slug;
const targetPath = computeRelativeURI(note.source.uri, relativePath);
const targetId = this.createIdFromURI(targetPath);
const connection: GraphConnection = {
from: graphNote.id,
to: targetId,
link: link,
};
this.graph.setEdge(graphNote.id, targetId, connection);
});
isSome(oldNote)
? this.onDidUpdateNoteEmitter.fire(graphNote)
: this.onDidAddNoteEmitter.fire(graphNote);
return graphNote;
}
public deleteNote(noteId: ID): GraphNote | null {
return this.doDelete(noteId, true);
}
private doDelete(noteId: ID, fireEvent: boolean): GraphNote | null {
const note = this.getNote(noteId);
if (isSome(note)) {
if (this.getBacklinks(noteId).length >= 1) {
this.graph.setNode(noteId, null); // Changes node to the "no file" style
} else {
this.graph.removeNode(noteId);
}
fireEvent && this.onDidDeleteEmitter.fire(note);
}
return note;
}
public getNotes(query?: NotesQuery): GraphNote[] {
// prettier-ignore
const filterFn =
query == null ? (note: Note | null) => note != null
: 'slug' in query ? (note: Note | null) => [nameToSlug(query.slug), query.slug].includes(note?.slug as string)
: 'title' in query ? (note: Note | null) => note?.title === query.title
: (note: Note | null) => note != null;
return this.graph
.nodes()
.map(id => this.graph.node(id))
.filter(filterFn);
}
public getNote(noteId: ID): GraphNote | null {
return this.graph.node(noteId) ?? null;
}
public getNoteByURI(uri: URI): GraphNote | null {
return this.getNote(this.createIdFromURI(uri));
}
public getAllLinks(noteId: ID): GraphConnection[] {
return (this.graph.nodeEdges(noteId) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public getForwardLinks(noteId: ID): GraphConnection[] {
return (this.graph.outEdges(noteId) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public removeForwardLinks(noteId: ID) {
(this.graph.outEdges(noteId) || []).forEach(edge => {
this.graph.removeEdge(edge);
});
}
public getBacklinks(noteId: ID): GraphConnection[] {
return (this.graph.inEdges(noteId) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public dispose() {
this.onDidAddNoteEmitter.dispose();
this.onDidUpdateNoteEmitter.dispose();
this.onDidDeleteEmitter.dispose();
}
}
const backfill = (next: NoteGraphAPI, middleware: Middleware): NoteGraphAPI => {
const m = middleware(next);
return {
setNote: m.setNote || next.setNote,
deleteNote: m.deleteNote || next.deleteNote,
getNotes: m.getNotes || next.getNotes,
getNote: m.getNote || next.getNote,
getNoteByURI: m.getNoteByURI || next.getNoteByURI,
getAllLinks: m.getAllLinks || next.getAllLinks,
getForwardLinks: m.getForwardLinks || next.getForwardLinks,
getBacklinks: m.getBacklinks || next.getBacklinks,
onDidAddNote: next.onDidAddNote,
onDidUpdateNote: next.onDidUpdateNote,
onDidDeleteNote: next.onDidDeleteNote,
};
};

View File

@@ -2,11 +2,12 @@ import * as fs from 'fs';
import path from 'path';
import { Node } from 'unist';
import { isNotNull } from '../utils';
import { Middleware } from '../note-graph';
import { Note } from '../types';
import { Middleware } from '../model/note-graph';
import { Note } from '../model/note';
import unified from 'unified';
import { FoamConfig } from '../config';
import { Logger } from '../utils/log';
import { URI } from '../common/uri';
export interface FoamPlugin {
name: string;
@@ -38,15 +39,16 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
if (!isFeatureEnabled) {
return [];
}
const pluginDirs: string[] =
pluginConfig.pluginFolders ?? findPluginDirs(config.workspaceFolders);
const pluginDirs: URI[] =
pluginConfig.pluginFolders?.map(URI.file) ??
findPluginDirs(config.workspaceFolders);
const plugins = await Promise.all(
pluginDirs
.filter(dir => fs.statSync(dir).isDirectory)
.filter(dir => fs.statSync(dir.fsPath).isDirectory)
.map(async dir => {
try {
const pluginFile = path.join(dir, 'index.js');
const pluginFile = path.join(dir.fsPath, 'index.js');
fs.accessSync(pluginFile);
Logger.info(`Found plugin at [${pluginFile}]. Loading..`);
const plugin = validate(await import(pluginFile));
@@ -60,19 +62,22 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
return plugins.filter(isNotNull);
}
function findPluginDirs(workspaceFolders: string[]) {
function findPluginDirs(workspaceFolders: URI[]) {
return workspaceFolders
.map(root => path.join(root, '.foam', 'plugins'))
.map(root => URI.joinPath(root, '.foam', 'plugins'))
.reduce((acc, pluginDir) => {
try {
const content = fs
.readdirSync(pluginDir)
.map(dir => path.join(pluginDir, dir));
return [...acc, ...content.filter(c => fs.statSync(c).isDirectory())];
.readdirSync(pluginDir.fsPath)
.map(dir => URI.joinPath(pluginDir, dir));
return [
...acc,
...content.filter(c => fs.statSync(c.fsPath).isDirectory()),
];
} catch {
return acc;
}
}, [] as string[]);
}, [] as URI[]);
}
function validate(plugin: any): FoamPlugin {

View File

@@ -3,12 +3,31 @@ import { promisify } from 'util';
import micromatch from 'micromatch';
import fs from 'fs';
import { Event, Emitter } from '../common/event';
import { URI } from '../types';
import { URI } from '../common/uri';
import { FoamConfig } from '../config';
import { Logger } from '../utils/log';
import { isSome } from '../utils';
import { IDisposable } from '../common/lifecycle';
const findAllFiles = promisify(glob);
export interface IWatcher {
/**
* An event which fires on file creation.
*/
onDidCreate: Event<URI>;
/**
* An event which fires on file change.
*/
onDidChange: Event<URI>;
/**
* An event which fires on file deletion.
*/
onDidDelete: Event<URI>;
}
/**
* Represents a source of files and content
*/
@@ -29,12 +48,6 @@ export interface IDataStore {
*/
isMatch: (uri: URI) => boolean;
/**
* Filters a list of URIs based on whether they are a match
* in this data store
*/
match: (uris: URI[]) => string[];
/**
* An event which fires on file creation.
*/
@@ -54,26 +67,28 @@ export interface IDataStore {
/**
* File system based data store
*/
export class FileDataStore implements IDataStore {
export class FileDataStore implements IDataStore, IDisposable {
readonly onDidChangeEmitter = new Emitter<URI>();
readonly onDidCreateEmitter = new Emitter<URI>();
readonly onDidDeleteEmitter = new Emitter<URI>();
readonly onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
readonly onDidChange: Event<URI> = this.onDidChangeEmitter.event;
readonly onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
readonly isMatch: (uri: URI) => boolean;
readonly match: (uris: URI[]) => string[];
private _folders: readonly string[];
private _includeGlobs: string[] = [];
private _ignoreGlobs: string[] = [];
private _disposables: IDisposable[] = [];
constructor(config: FoamConfig) {
this._folders = config.workspaceFolders;
constructor(config: FoamConfig, watcher?: IWatcher) {
this._folders = config.workspaceFolders.map(f =>
f.fsPath.replace(/\\/g, '/')
);
Logger.info('Workspace folders: ', this._folders);
let includeGlobs: string[] = [];
let ignoreGlobs: string[] = [];
config.workspaceFolders.forEach(folder => {
this._folders.forEach(folder => {
const withFolder = folderPlusGlob(folder);
includeGlobs.push(
this._includeGlobs.push(
...config.includeGlobs.map(glob => {
if (glob.endsWith('*')) {
glob = `${glob}\\.(md|mdx|markdown)`;
@@ -81,27 +96,59 @@ export class FileDataStore implements IDataStore {
return withFolder(glob);
})
);
ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
this._ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
});
Logger.info('Glob patterns', {
includeGlobs: this._includeGlobs,
ignoreGlobs: this._ignoreGlobs,
});
Logger.debug('Glob patterns', {
includeGlobs,
ignoreGlobs,
});
this.match = (files: URI[]) => {
return micromatch(files, includeGlobs, {
ignore: ignoreGlobs,
if (isSome(watcher)) {
this._disposables.push(
watcher.onDidCreate(uri => {
if (this.isMatch(uri)) {
Logger.info(`Created: ${uri.path}`);
this.onDidCreateEmitter.fire(uri);
}
}),
watcher.onDidChange(uri => {
if (this.isMatch(uri)) {
Logger.info(`Updated: ${uri.path}`);
this.onDidChangeEmitter.fire(uri);
}
}),
watcher.onDidDelete(uri => {
if (this.isMatch(uri)) {
Logger.info(`Deleted: ${uri.path}`);
this.onDidDeleteEmitter.fire(uri);
}
})
);
}
}
match(files: URI[]) {
const matches = micromatch(
files.map(f => f.fsPath),
this._includeGlobs,
{
ignore: this._ignoreGlobs,
nocase: true,
});
};
this.isMatch = uri => this.match([uri]).length > 0;
}
);
return matches.map(URI.file);
}
isMatch(uri: URI) {
return this.match([uri]).length > 0;
}
async listFiles() {
const files = (
await Promise.all(
this._folders.map(folder => {
return findAllFiles(folderPlusGlob(folder)('**/*'));
this._folders.map(async folder => {
const res = await findAllFiles(folderPlusGlob(folder)('**/*'));
return res.map(URI.file);
})
)
).flat();
@@ -109,7 +156,11 @@ export class FileDataStore implements IDataStore {
}
async read(uri: URI) {
return (await fs.promises.readFile(uri)).toString();
return (await fs.promises.readFile(uri.fsPath)).toString();
}
dispose() {
this._disposables.forEach(d => d.dispose());
}
}

View File

@@ -1,28 +1,59 @@
import path from 'path';
import { posix } from 'path';
import GithubSlugger from 'github-slugger';
import { URI, ID } from '../types';
import { hash } from './core';
import { URI } from '../common/uri';
export const uriToSlug = (noteUri: URI): string => {
return GithubSlugger.slug(path.parse(noteUri).name);
return GithubSlugger.slug(posix.parse(noteUri.path).name);
};
export const nameToSlug = (noteName: string): string => {
return GithubSlugger.slug(noteName);
};
export const hashURI = (uri: URI): ID => {
return hash(path.normalize(uri));
export const hashURI = (uri: URI): string => {
return hash(posix.normalize(uri.path));
};
export const computeRelativePath = (source: URI, target: URI): string => {
const relativePath = posix.relative(posix.dirname(source.path), target.path);
return relativePath;
};
export const getBasename = (uri: URI) => posix.parse(uri.path).name;
export const computeRelativeURI = (
reference: URI,
relativeSlug: string
): URI => {
// if no extension is provided, use the same extension as the source file
const slug =
path.extname(relativeSlug) !== ''
posix.extname(relativeSlug) !== ''
? relativeSlug
: `${relativeSlug}${path.extname(reference)}`;
return path.normalize(path.join(path.dirname(reference), slug));
: `${relativeSlug}${posix.extname(reference.path)}`;
return reference.with({
path: posix.join(posix.dirname(reference.path), slug),
});
};
/**
* Parses a URI from value, taking into consideration possible relative paths.
*
* @param reference the URI to use as reference in case value is a relative path
* @param value the value to parse for a URI
* @returns the URI from the given value. In case of a relative path, the URI will take into account
* the reference from which it is computed
*/
export const parseUri = (reference: URI, value: string): URI => {
let uri = URI.parse(value);
if (uri.scheme === 'file' && !value.startsWith('/')) {
const [path, fragment] = value.split('#');
uri = path.length > 0 ? computeRelativeURI(reference, path) : reference;
if (fragment) {
uri = uri.with({
fragment: fragment,
});
}
}
return uri;
};

View File

@@ -1,10 +1,16 @@
import * as path from 'path';
import { createConfigFromFolders } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/common/uri';
Logger.setLevel('error');
const testFolder = URI.joinPath(URI.file(__dirname), 'test-config');
const testFolder = path.join(__dirname, 'test-config');
describe('Foam configuration', () => {
it('can read settings from config.json', () => {
const config = createConfigFromFolders([path.join(testFolder, 'folder1')]);
const config = createConfigFromFolders([
URI.joinPath(testFolder, 'folder1'),
]);
expect(config.get('feature1.setting1.value')).toBeTruthy();
expect(config.get('feature2.value')).toEqual(12);
@@ -14,8 +20,8 @@ describe('Foam configuration', () => {
it('can merge settings from multiple foam folders', () => {
const config = createConfigFromFolders([
path.join(testFolder, 'folder1'),
path.join(testFolder, 'folder2'),
URI.joinPath(testFolder, 'folder1'),
URI.joinPath(testFolder, 'folder2'),
]);
// override value
@@ -31,7 +37,7 @@ describe('Foam configuration', () => {
it('cannot activate local plugins from workspace config', () => {
const config = createConfigFromFolders([
path.join(testFolder, 'enable-plugins'),
URI.joinPath(testFolder, 'enable-plugins'),
]);
expect(config.get('experimental.localPlugins.enabled')).toBeUndefined();
});

View File

@@ -1,6 +1,10 @@
import { NoteGraph, createGraph } from '../src/note-graph';
import { NoteLinkDefinition, Note } from '../src/types';
import { NoteGraph, createGraph } from '../src/model/note-graph';
import { NoteLinkDefinition, Note } from '../src/model/note';
import { uriToSlug } from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const position = {
start: { line: 1, column: 1 },
@@ -11,32 +15,46 @@ const documentStart = position.start;
const documentEnd = position.end;
const eol = '\n';
/**
* Turns a string into a URI
* The goal of this function is to make sure we are consistent in the
* way we generate URIs (and therefore IDs) across the tests
*/
export const strToUri = URI.file;
export const createTestNote = (params: {
uri: string;
title?: string;
definitions?: NoteLinkDefinition[];
links?: { slug: string }[];
links?: Array<{ slug: string } | { to: string }>;
text?: string;
}): Note => {
return {
uri: strToUri(params.uri),
properties: {},
title: params.title ?? null,
slug: uriToSlug(params.uri),
definitions: params.definitions ?? [],
tags: new Set(),
links: params.links
? params.links.map(link => ({
type: 'wikilink',
slug: link.slug,
position: position,
text: 'link text',
}))
? params.links.map(link =>
'slug' in link
? {
type: 'wikilink',
slug: link.slug,
position: position,
text: 'link text',
}
: {
type: 'link',
target: link.to,
label: 'link text',
}
)
: [],
source: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
uri: params.uri,
text: params.text ?? '',
},
};
@@ -52,7 +70,7 @@ describe('Note graph', () => {
expect(
graph
.getNotes()
.map(n => n.slug)
.map(n => uriToSlug(n.uri))
.sort()
).toEqual(['page-a', 'page-b', 'page-c']);
});
@@ -69,7 +87,10 @@ describe('Note graph', () => {
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)!.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
});
@@ -85,14 +106,50 @@ describe('Note graph', () => {
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)!.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
it('Detects backlinks of direct links', () => {
const graph = new NoteGraph();
const noteA = createTestNote({
uri: '/path/to/page-a.md',
});
// connected via absolute path
const noteB = createTestNote({
uri: '/page-b.md',
links: [{ to: noteA.uri.path }],
});
// connected via relative path
const noteC = createTestNote({
uri: '/path/docs/page-c.md',
links: [{ to: '../to/page-a.md' }],
});
// not connected - wrong path
const noteD = createTestNote({
uri: '/path/docs/page-d.md',
links: [{ to: '../to/another/page-a.md' }],
});
graph.setNote(noteA);
graph.setNote(noteB);
graph.setNote(noteC);
graph.setNote(noteD);
expect(
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b', 'page-c']);
});
it('Returns null when accessing non-existing node', () => {
const graph = new NoteGraph();
graph.setNote(createTestNote({ uri: 'page-a' }));
expect(graph.getNote('non-existing')).toBeNull();
expect(graph.getNote(strToUri('non-existing'))).toBeNull();
});
it('Allows adding edges to non-existing documents', () => {
@@ -104,7 +161,7 @@ describe('Note graph', () => {
})
);
expect(graph.getNote('non-existing')).toBeNull();
expect(graph.getNote(strToUri('non-existing'))).toBeNull();
});
it('Updates links when modifying note', () => {
@@ -119,13 +176,22 @@ describe('Note graph', () => {
const noteC = graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
expect(
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual([]);
graph.setNote(
@@ -136,19 +202,31 @@ describe('Note graph', () => {
);
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-c']);
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual([]);
expect(
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
// Tests #393: page-a should not lose its links when updated
graph.setNote(createTestNote({ title: 'Test-C', uri: '/page-c.md' }));
expect(
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
@@ -172,17 +250,22 @@ describe('Note graph', () => {
})
);
graph.deleteNote(noteA.id);
graph.deleteNote(noteA.uri);
expect(
graph.getForwardLinks(noteB.id).map(link => link?.link?.slug)
graph.getForwardLinks(noteB.uri).map(link => (link as any)?.link?.slug)
).toEqual(['page-a']);
expect(graph.getNote(noteA.id)).toBeNull();
expect(graph.getNote(noteA.uri)).toBeNull();
graph.deleteNote(noteC.id);
graph.deleteNote(noteC.uri);
expect(
graph.getForwardLinks(noteC.id).map(link => link?.link?.slug)
graph.getForwardLinks(noteC.uri).map(link => (link as any)?.link?.slug)
).toEqual([]);
expect(graph.getNotes().map(note => note.slug)).toEqual(['page-b']);
expect(
graph
.getNotes()
.map(note => note.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
});
@@ -197,7 +280,7 @@ describe('Graph querying', () => {
it('finds the note by slug', () => {
const graph = new NoteGraph();
const note = graph.setNote(createTestNote({ uri: '/page-a.md' }));
expect(graph.getNotes({ slug: note.slug }).length).toEqual(1);
expect(graph.getNotes({ slug: uriToSlug(note.uri) }).length).toEqual(1);
});
it('finds a note by slug when there is more than one', () => {
@@ -258,7 +341,7 @@ describe('graph events', () => {
const note = graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
graph.deleteNote(note.id);
graph.deleteNote(note.uri);
expect(callback).toHaveBeenCalledTimes(1);
listener.dispose();
});
@@ -266,10 +349,10 @@ describe('graph events', () => {
const graph = new NoteGraph();
const callback = jest.fn();
const listener = graph.onDidDeleteNote(callback);
const note = graph.setNote(
graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
graph.deleteNote('non-existing-note');
graph.deleteNote(strToUri('non-existing-note'));
expect(callback).toHaveBeenCalledTimes(0);
listener.dispose();
});
@@ -305,7 +388,7 @@ describe('graph events', () => {
expect(updateCallback).toHaveBeenCalledTimes(2);
expect(deleteCallback).toHaveBeenCalledTimes(0);
graph.deleteNote(note.id);
graph.deleteNote(note.uri);
expect(addCallback).toHaveBeenCalledTimes(1);
expect(updateCallback).toHaveBeenCalledTimes(2);
expect(deleteCallback).toHaveBeenCalledTimes(1);
@@ -315,7 +398,7 @@ describe('graph events', () => {
});
describe('graph middleware', () => {
it('can intercept calls to the graph', async () => {
it('can intercept calls to the graph', () => {
const graph = createGraph([
next => ({
setNote: note => {

View File

@@ -0,0 +1,72 @@
import { createConfigFromObject } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/common/uri';
import { FileDataStore } from '../src';
Logger.setLevel('error');
const testFolder = URI.joinPath(URI.file(__dirname), 'test-datastore');
function makeConfig(params: { include: string[]; ignore: string[] }) {
return createConfigFromObject(
[testFolder],
params.include,
params.ignore,
{}
);
}
describe('Datastore', () => {
it('defaults to including nothing and exclude nothing', async () => {
const ds = new FileDataStore(
makeConfig({
include: [],
ignore: [],
})
);
expect(await ds.listFiles()).toHaveLength(0);
});
it('returns only markdown files', async () => {
const ds = new FileDataStore(
makeConfig({
include: ['**/*'],
ignore: [],
})
);
const res = toStringSet(await ds.listFiles());
expect(res).toEqual(
makeAbsolute([
'/file-a.md',
'/info/file-b.md',
'/docs/file-in-nm.md',
'/info/docs/file-in-sub-nm.md',
])
);
});
it('supports excludes', async () => {
const ds = new FileDataStore(
makeConfig({
include: ['**/*'],
ignore: ['**/docs/**'],
})
);
const res = toStringSet(await ds.listFiles());
expect(res).toEqual(makeAbsolute(['/file-a.md', '/info/file-b.md']));
});
});
function toStringSet(uris: URI[]) {
return new Set(uris.map(uri => uri.path.toLocaleLowerCase()));
}
function makeAbsolute(files: string[]) {
return new Set(
files.map(f =>
URI.joinPath(testFolder, f)
.path.toLocaleLowerCase()
.replace(/\\/g, '/')
)
);
}

View File

@@ -1,4 +1,7 @@
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
import { Logger } from '../../src/utils/log';
Logger.setLevel('error');
describe('applyTextEdit', () => {
it('should return text with applied TextEdit in the end of the string', () => {

View File

@@ -1,16 +1,20 @@
import * as path from 'path';
import { NoteGraphAPI } from '../../src/note-graph';
import { NoteGraphAPI } from '../../src/model/note-graph';
import { generateHeading } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Services } from '../../src';
import { URI } from '../../src/common/uri';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
Logger.setLevel('error');
describe('generateHeadings', () => {
let _graph: NoteGraphAPI;
beforeAll(async () => {
const config = createConfigFromFolders([
path.join(__dirname, '../__scaffold__'),
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const services: Services = {
dataStore: new FileDataStore(config),

View File

@@ -1,17 +1,20 @@
import * as path from 'path';
import { NoteGraphAPI } from '../../src/note-graph';
import { generateLinkReferences } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Services } from '../../src';
import { Services, Note, NoteGraphAPI } from '../../src';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { URI } from '../../src/common/uri';
Logger.setLevel('error');
describe('generateLinkReferences', () => {
let _graph: NoteGraphAPI;
beforeAll(async () => {
const config = createConfigFromFolders([
path.join(__dirname, '../__scaffold__'),
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const services: Services = {
dataStore: new FileDataStore(config),
@@ -26,23 +29,26 @@ describe('generateLinkReferences', () => {
it('should add link references to a file that does not have them', () => {
const note = _graph.getNotes({ slug: 'index' })[0];
const expected = {
newText: `
newText: textForNote(
note,
`
[//begin]: # "Autogenerated link references for markdown compatibility"
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"
[file-without-title]: file-without-title "file-without-title"
[//end]: # "Autogenerated link references"`,
[//end]: # "Autogenerated link references"`
),
range: {
start: {
start: pointForNote(note, {
line: 10,
column: 1,
offset: 140,
},
end: {
}),
end: pointForNote(note, {
line: 10,
column: 1,
offset: 140,
},
}),
},
};
@@ -59,16 +65,16 @@ describe('generateLinkReferences', () => {
const expected = {
newText: '',
range: {
start: {
start: pointForNote(note, {
line: 7,
column: 1,
offset: 105,
},
end: {
}),
end: pointForNote(note, {
line: 9,
column: 43,
offset: 269,
},
}),
},
};
@@ -83,20 +89,23 @@ describe('generateLinkReferences', () => {
const note = _graph.getNotes({ slug: 'first-document' })[0];
const expected = {
newText: `[//begin]: # "Autogenerated link references for markdown compatibility"
newText: textForNote(
note,
`[//begin]: # "Autogenerated link references for markdown compatibility"
[file-without-title]: file-without-title "file-without-title"
[//end]: # "Autogenerated link references"`,
[//end]: # "Autogenerated link references"`
),
range: {
start: {
start: pointForNote(note, {
line: 9,
column: 1,
offset: 145,
},
end: {
}),
end: pointForNote(note, {
line: 11,
column: 43,
offset: 312,
},
}),
},
};
@@ -117,3 +126,34 @@ describe('generateLinkReferences', () => {
expect(actual).toEqual(expected);
});
});
/**
* Will adjust a text line separator to match
* what is used by the note
* Necessary when running tests on windows
*
* @param note the note we are adjusting for
* @param text starting text, using a \n line separator
*/
function textForNote(note: Note, text: string): string {
return text.split('\n').join(note.source.eol);
}
/**
* Will adjust a point to take into account the EOL length
* of the note
* Necessary when running tests on windows
*
* @param note the note we are adjusting for
* @param pos starting position
*/
function pointForNote(
note: Note,
pos: { line: number; column: number; offset: number }
) {
const rows = pos.line - 1;
return {
...pos,
offset: pos.offset - rows + rows * note.source.eol.length,
};
}

View File

@@ -2,8 +2,14 @@ import {
createMarkdownParser,
createMarkdownReferences,
} from '../src/markdown-provider';
import { NoteGraph } from '../src/note-graph';
import { DirectLink } from '../src/model/note';
import { NoteGraph } from '../src/model/note-graph';
import { ParserPlugin } from '../src/plugins';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
import { uriToSlug } from '../src/utils';
Logger.setLevel('error');
const pageA = `
# Page A
@@ -32,7 +38,8 @@ const pageE = `
# Page E
`;
const createNoteFromMarkdown = createMarkdownParser([]).parse;
const createNoteFromMarkdown = (path: string, content: string) =>
createMarkdownParser([]).parse(URI.file(path), content);
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
@@ -46,11 +53,56 @@ describe('Markdown loader', () => {
expect(
graph
.getNotes()
.map(n => n.slug)
.map(n => n.uri)
.map(uriToSlug)
.sort()
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
});
it('Ingores external links', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
`
this is a [link to google](https://www.google.com)
`
);
expect(note.links.length).toEqual(0);
});
it('Ignores references to sections in the same file', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
`
this is a [link to intro](#introduction)
`
);
expect(note.links.length).toEqual(0);
});
it('Parses internal links correctly', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
'this is a [link to page b](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0] as DirectLink;
expect(link.type).toEqual('link');
expect(link.label).toEqual('link to page b');
expect(link.target).toEqual('../doc/page-b.md');
});
it('Parses links that have formatting in label', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
'this is [**link** with __formatting__](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0] as DirectLink;
expect(link.type).toEqual('link');
expect(link.label).toEqual('link with formatting');
expect(link.target).toEqual('../doc/page-b.md');
});
it('Parses wikilinks correctly', () => {
const graph = new NoteGraph();
const noteA = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
@@ -60,10 +112,16 @@ describe('Markdown loader', () => {
graph.setNote(createNoteFromMarkdown('/page e.md', pageE));
expect(
graph.getBacklinks(noteB.id).map(link => graph.getNote(link.from)!.slug)
graph
.getBacklinks(noteB.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
expect(
graph.getForwardLinks(noteA.id).map(link => graph.getNote(link.to)!.slug)
graph
.getForwardLinks(noteA.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-b', 'page-c', 'page-d', 'page-e']);
});
});
@@ -73,7 +131,7 @@ describe('Note Title', () => {
const graph = new NoteGraph();
const note = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
const pageANoteTitle = graph.getNote(note.id)!.title;
const pageANoteTitle = graph.getNote(note.uri)!.title;
expect(pageANoteTitle).toBe('Page A');
});
@@ -88,7 +146,7 @@ This file has no heading.
)
);
const pageANoteTitle = graph.getNote(note.id)!.title;
const pageANoteTitle = graph.getNote(note.uri)!.title;
expect(pageANoteTitle).toEqual('page-d');
});
@@ -108,7 +166,7 @@ date: 20-12-12
)
);
const pageENoteTitle = graph.getNote(note.id)!.title;
const pageENoteTitle = graph.getNote(note.uri)!.title;
expect(pageENoteTitle).toBe('Note Title');
});
@@ -146,7 +204,7 @@ date: 20-12-12
date: '20-12-12',
};
const actual: any = graph.getNote(note.id)!.properties;
const actual: any = graph.getNote(note.uri)!.properties;
expect(actual.title).toBe(expected.title);
expect(actual.date).toBe(expected.date);
@@ -168,7 +226,7 @@ date: 20-12-12
const expected = {};
const actual = graph.getNote(note.id)!.properties;
const actual = graph.getNote(note.uri)!.properties;
expect(actual).toEqual(expected);
});
@@ -191,7 +249,7 @@ title: - one
const expected = {};
const actual = graph.getNote(note.id)!.properties;
const actual = graph.getNote(note.uri)!.properties;
expect(actual).toEqual(expected);
});
@@ -206,7 +264,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
const noExtRefs = createMarkdownReferences(graph, noteA.id, false);
const noExtRefs = createMarkdownReferences(graph, noteA.uri, false);
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
});
@@ -218,7 +276,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
const extRefs = createMarkdownReferences(graph, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
});
@@ -230,7 +288,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
const extRefs = createMarkdownReferences(graph, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
@@ -293,9 +351,9 @@ describe('parser plugins', () => {
};
const parser = createMarkdownParser([testPlugin]);
it('can augment the parsing of the file', async () => {
it('can augment the parsing of the file', () => {
const note1 = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
This is a test note without headings.
But with some content.
@@ -304,7 +362,7 @@ But with some content.
expect(note1.properties.hasHeading).toBeUndefined();
const note2 = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
# This is a note with header
and some content`

View File

@@ -1,9 +1,13 @@
import path from 'path';
import { loadPlugins } from '../src/plugins';
import { createMarkdownParser } from '../src/markdown-provider';
import { createGraph } from '../src/note-graph';
import { createGraph } from '../src/model/note-graph';
import { createTestNote } from './core.test';
import { FoamConfig, createConfigFromObject } from '../src/config';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const config: FoamConfig = createConfigFromObject([], [], [], {
experimental: {
@@ -60,7 +64,7 @@ describe('Foam plugins', () => {
const parser = createMarkdownParser([parserPlugin!]);
const note = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
# This is a note with header
and some content`

View File

@@ -4,15 +4,24 @@ import {
hashURI,
computeRelativeURI,
extractHashtags,
parseUri,
} from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
describe('URI utils', () => {
it('supports various cases', () => {
expect(uriToSlug('/this/is/a/path.md')).toEqual('path');
expect(uriToSlug('../a/relative/path.md')).toEqual('path');
expect(uriToSlug('another/relative/path.md')).toEqual('path');
expect(uriToSlug('no-directory.markdown')).toEqual('no-directory');
expect(uriToSlug('many.dots.name.markdown')).toEqual('manydotsname');
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
'no-directory'
);
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
'manydotsname'
);
});
it('converts a name to a slug', () => {
@@ -23,22 +32,45 @@ describe('URI utils', () => {
});
it('normalizes URI before hashing', () => {
expect(hashURI('/this/is/a/path.md')).toEqual(
hashURI('/this/has/../is/a/path.md')
expect(hashURI(URI.file('/this/is/a/path.md'))).toEqual(
hashURI(URI.file('/this/has/../is/a/path.md'))
);
expect(hashURI('this/is/a/path.md')).toEqual(
hashURI('this/has/../is/a/path.md')
expect(hashURI(URI.file('this/is/a/path.md'))).toEqual(
hashURI(URI.file('this/has/../is/a/path.md'))
);
});
it('computes a relative uri using a slug', () => {
expect(computeRelativeURI('/my/file.md', '../hello.md')).toEqual(
'/hello.md'
expect(computeRelativeURI(URI.file('/my/file.md'), '../hello.md')).toEqual(
URI.file('/hello.md')
);
expect(computeRelativeURI('/my/file.md', '../hello')).toEqual('/hello.md');
expect(computeRelativeURI('/my/file.markdown', '../hello')).toEqual(
'/hello.markdown'
expect(computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
URI.file('/hello.md')
);
expect(
computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
).toEqual(URI.file('/hello.markdown'));
});
describe('URI parsing', () => {
const base = URI.file('/path/to/file.md');
test.each([
['https://www.google.com', URI.parse('https://www.google.com')],
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
['../relative/file.md', URI.file('/path/relative/file.md')],
['#section', base.with({ fragment: 'section' })],
[
'../relative/file.md#section',
URI.parse('file:/path/relative/file.md#section'),
],
])('URI Parsing (%s) - %s', (input, exp) => {
const result = parseUri(base, input);
expect(result.scheme).toEqual(exp.scheme);
expect(result.authority).toEqual(exp.authority);
expect(result.path).toEqual(exp.path);
expect(result.query).toEqual(exp.query);
expect(result.fragment).toEqual(exp.fragment);
});
});
});

View File

@@ -1,3 +0,0 @@
out
.vscode-test/
*.vsix

View File

@@ -1,3 +0,0 @@
{
"tabWidth": 2
}

View File

@@ -4,7 +4,44 @@ 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.7.2] - 2020-12-13
## [0.9.0] - 2021-01-27
Features:
- Panel: Added orphan panel (#457 - thanks @leonhfr)
## [0.8.0] - 2021-01-15
Features:
- Model: Now direct links are included in the Foam model (#433)
- Commaands: Added `Open random note` command (#440 - thanks @MCluck90)
- Dataviz: Added graph style override from VsCode theme (#438 - thanks @jmg-duarte)
- Dataviz: Added graph style customization based on note type (#449)
Fixes and Improvements:
- Various improvements and fixes in documentation (thanks @anglinb, @themaxdavitt, @elswork)
## [0.7.7] - 2020-12-31
Fixes and Improvements:
- Fixed word-based-suggestions (#415 #417 - thanks @bpugh!)
- Date snippets use standard wikilink syntax (#416 - thanks @MCluck90!)
## [0.7.6] - 2020-12-20
Fixes and Improvements:
- Fixed "Janitor" command issue in Windows (#410)
## [0.7.5] - 2020-12-17
Fixes and Improvements:
- Fixed "Open Daily Note" command issue in Windows (#407)
## [0.7.4] - 2020-12-16
Fixes and Improvements:
- Fixed a bug that was causing Foam to not work correctly in Windows (#391)
## [0.7.3] - 2020-12-13
Fixes and Improvements:
- Foam model: fix to link references on node update/deletion (#393 - thanks @AndrewNatoli)

View File

@@ -6,15 +6,17 @@
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
This is the VS Code extension for [Foam](https://foambubble.github.io/foam).
> the extension doesn't do much on it's own. To learn how to use it, read [Foam documentation](https://foambubble.github.io/foam) and the [Getting started](https://foambubble.github.io/foam/#getting-started) guide.
> ⚠️ This is an early stage software. Use at your own peril.
> You can join the Foam Community on the [Foam Discord](https://discord.gg/EKFyMrVNZ9)
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
## Quick getting started
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VsCode, which means you can pair it with your favorite extensions for a great editing experience.
Foam is open source, and allows you to create a local first, markdown based, personal knowledge base. You can also use it to publish your notes.
Foam is also meant to be extensible, so you can integrate with its internals to customize your knowledge base.
## Getting started
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
@@ -24,13 +26,18 @@ You really, _really_, **really** should read [Foam documentation](https://foambu
This will also install `Foam for VSCode`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
## Features
- Connect your notes using `[[wiki-links]]`
- Create markdown references for `[[wiki-links]]` with the `Foam: Update Markdown Reference List` command, to use your notes in a non-foam workspace
- See how your notes are connected via a graph with the `Foam: Show Graph` command
- Tag your notes and navigate them with the Tag Explorer
- Connect your notes using [`[[wiki-links]]`](https://foambubble.github.io/foam/features/backlinking)
- Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wiki-links]]`, to use your notes in a non-foam workspace
- See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command
- Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags)
- Make your notes navigable both in GitHub UI as well as GitHub Pages
- Use [custom templates](https://foambubble.github.io/foam/features/note-templates) for your notes
- Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes)
- Explore your knowledge base with the `Foam: Open Random Note` command
## Requirements

View File

@@ -2,13 +2,12 @@
"name": "foam-vscode",
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
"description": "Generate markdown reference lists from wikilinks in a workspace",
"author": "Jani Eväkallio",
"repository": {
"url": "https://github.com/foambubble/foam",
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.7.3",
"version": "0.9.0",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -23,6 +22,7 @@
"onView:foam-vscode.tags-explorer",
"onCommand:foam-vscode.update-wikilinks",
"onCommand:foam-vscode.open-daily-note",
"onCommand:foam-vscode.open-random-note",
"onCommand:foam-vscode.janitor",
"onCommand:foam-vscode.copy-without-brackets",
"onCommand:foam-vscode.show-graph"
@@ -36,6 +36,46 @@
"name": "Tag Explorer",
"icon": "media/dep.svg",
"contextualTitle": "Tags Explorer"
},
{
"id": "foam-vscode.orphans",
"name": "Orphans",
"icon": "media/dep.svg",
"contextualTitle": "Orphans"
}
]
},
"viewsWelcome": [
{
"view": "foam-vscode.tags-explorer",
"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.orphans",
"contents": "No orphans found. Notes that have no backlinks nor links will show up here."
}
],
"menus": {
"view/title": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == false",
"group": "navigation"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "false"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "false"
}
]
},
@@ -56,6 +96,10 @@
"command": "foam-vscode.open-daily-note",
"title": "Foam: Open Daily Note"
},
{
"command": "foam-vscode.open-random-note",
"title": "Foam: Open Random Note"
},
{
"command": "foam-vscode.janitor",
"title": "Foam: Run Janitor (Experimental)"
@@ -67,6 +111,16 @@
{
"command": "foam-vscode.create-note-from-template",
"title": "Foam: Create New Note From Template"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"title": "Foam: Group Orphans By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-orphans-off",
"title": "Foam: Don't Group Orphans",
"icon": "$(list-flat)"
}
],
"configuration": {
@@ -137,6 +191,30 @@
"default": null,
"description": "The directory into which daily notes should be created. Defaults to the workspace root."
},
"foam.orphans.exclude": {
"type": [
"array"
],
"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>/**/*`",
"scope": "resource"
},
"foam.orphans.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group orphans report entries by.",
"scope": "resource"
},
"foam.dateSnippets.afterCompletion": {
"type": "string",
"default": "createNote",
@@ -156,6 +234,11 @@
"type": "number",
"default": 24,
"description": "The maximum title length before being abbreviated. Set to 0 or less to disable."
},
"foam.graph.style": {
"type": "object",
"description": "Custom graph styling settings. An example is present in the documentation.",
"default": {}
}
}
},
@@ -168,9 +251,10 @@
},
"scripts": {
"build": "tsc -p ./",
"test": "jest",
"lint": "eslint src --ext ts",
"clean": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --clean",
"pretest": "yarn build",
"test": "node ./out/test/run-tests.js",
"lint": "tsdx lint",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
"vscode:prepublish": "yarn npm-install && yarn run build",
@@ -196,12 +280,16 @@
"eslint": "^6.8.0",
"glob": "^7.1.6",
"jest": "^26.2.2",
"jest-environment-vscode": "^1.0.0",
"jest-extended": "^0.11.5",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"typescript": "^3.8.3",
"vscode-test": "^1.3.0"
},
"dependencies": {
"dateformat": "^3.0.3",
"foam-core": "^0.7.3"
"foam-core": "^0.9.0",
"micromatch": "^4.0.2"
}
}

View File

@@ -1,17 +1,11 @@
import {
Selection,
Uri,
window,
workspace,
WorkspaceConfiguration
} from "vscode";
import { dirname, join } from "path";
import dateFormat from "dateformat";
import * as fs from "fs";
import { docConfig, focusNote, pathExists } from "./utils";
import { workspace, WorkspaceConfiguration } from 'vscode';
import { dirname, join } from 'path';
import dateFormat from 'dateformat';
import * as fs from 'fs';
import { docConfig, focusNote, pathExists } from './utils';
async function openDailyNoteFor(date?: Date) {
const foamConfiguration = workspace.getConfiguration("foam");
const foamConfiguration = workspace.getConfiguration('foam');
const currentDate = date !== undefined ? date : new Date();
const dailyNotePath = getDailyNotePath(foamConfiguration, currentDate);
@@ -26,7 +20,7 @@ async function openDailyNoteFor(date?: Date) {
function getDailyNotePath(configuration: WorkspaceConfiguration, date: Date) {
const rootDirectory = workspace.workspaceFolders[0].uri.fsPath;
const dailyNoteDirectory: string =
configuration.get("openDailyNote.directory") ?? ".";
configuration.get('openDailyNote.directory') ?? '.';
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
return join(rootDirectory, dailyNoteDirectory, dailyNoteFilename);
@@ -37,10 +31,10 @@ function getDailyNoteFileName(
date: Date
): string {
const filenameFormat: string = configuration.get(
"openDailyNote.filenameFormat"
'openDailyNote.filenameFormat'
);
const fileExtension: string = configuration.get(
"openDailyNote.fileExtension"
'openDailyNote.fileExtension'
);
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
@@ -58,8 +52,8 @@ async function createDailyNoteIfNotExists(
await createDailyNoteDirectoryIfNotExists(dailyNotePath);
const titleFormat: string =
configuration.get("openDailyNote.titleFormat") ??
configuration.get("openDailyNote.filenameFormat");
configuration.get('openDailyNote.titleFormat') ??
configuration.get('openDailyNote.filenameFormat');
await fs.promises.writeFile(
dailyNotePath,
@@ -83,5 +77,5 @@ export {
openDailyNoteFor,
getDailyNoteFileName,
createDailyNoteIfNotExists,
getDailyNotePath
getDailyNotePath,
};

View File

@@ -1,22 +1,16 @@
"use strict";
import { workspace, ExtensionContext, window } from "vscode";
import { workspace, ExtensionContext, window } from 'vscode';
import {
bootstrap,
FoamConfig,
Foam,
FileDataStore,
Services,
isDisposable,
Logger
} from "foam-core";
Logger,
FileDataStore,
} from 'foam-core';
import { features } from "./features";
import { getConfigFromVscode } from "./services/config";
import { VsCodeOutputLogger, exposeLogger } from "./services/logging";
let foam: Foam | null = null;
import { features } from './features';
import { getConfigFromVscode } from './services/config';
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -24,30 +18,14 @@ export async function activate(context: ExtensionContext) {
exposeLogger(context, logger);
try {
Logger.info("Starting Foam");
Logger.info('Starting Foam');
const config: FoamConfig = getConfigFromVscode();
const dataStore = new FileDataStore(config);
const watcher = workspace.createFileSystemWatcher("**/*");
watcher.onDidCreate(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidCreateEmitter.fire(uri.fsPath);
}
});
watcher.onDidChange(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidChangeEmitter.fire(uri.fsPath);
}
});
watcher.onDidDelete(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidDeleteEmitter.fire(uri.fsPath);
}
});
const watcher = workspace.createFileSystemWatcher('**/*');
const dataStore = new FileDataStore(config, watcher);
const services: Services = {
dataStore: dataStore
dataStore: dataStore,
};
const foamPromise: Promise<Foam> = bootstrap(config, services);
@@ -55,18 +33,14 @@ export async function activate(context: ExtensionContext) {
f.activate(context, foamPromise);
});
foam = await foamPromise;
const foam = await foamPromise;
Logger.info(`Loaded ${foam.notes.getNotes().length} notes`);
context.subscriptions.push(dataStore, foam, watcher);
} catch (e) {
Logger.error("An error occurred while bootstrapping Foam", e);
Logger.error('An error occurred while bootstrapping Foam', e);
window.showErrorMessage(
`An error occurred while bootstrapping Foam. ${e.stack}`
);
}
}
export function deactivate() {
if (isDisposable(foam)) {
foam?.dispose();
}
}

View File

@@ -0,0 +1,18 @@
import { env, window, Uri, Position, Selection, commands } from 'vscode';
import * as vscode from 'vscode';
describe('copyWithoutBrackets', () => {
it('should get the input from the active editor selection', async () => {
const doc = await vscode.workspace.openTextDocument(
Uri.parse('untitled:/hello.md')
);
const editor = await window.showTextDocument(doc);
editor.edit(builder => {
builder.insert(new Position(0, 0), 'This is my [[test-content]].');
});
editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
await commands.executeCommand('foam-vscode.copy-without-brackets');
const value = await env.clipboard.readText();
expect(value).toEqual('This is my Test Content.');
});
});

View File

@@ -1,21 +1,19 @@
import {
window,
env,
ExtensionContext,
commands,
} from "vscode";
import { FoamFeature } from "../types";
import { removeBrackets } from "../utils";
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)
commands.registerCommand(
'foam-vscode.copy-without-brackets',
copyWithoutBrackets
)
);
},
};
async function copyWithoutBrackets () {
async function copyWithoutBrackets() {
// Get the active text editor
const editor = window.activeTextEditor;

View File

@@ -4,44 +4,44 @@ import {
ExtensionContext,
workspace,
Uri,
SnippetString
} from "vscode";
import * as path from "path";
import { FoamFeature } from "../types";
import { TextEncoder } from "util";
import { focusNote } from "../utils";
SnippetString,
} from 'vscode';
import * as path from 'path';
import { FoamFeature } from '../types';
import { TextEncoder } from 'util';
import { focusNote } from '../utils';
const templatesDir = `${workspace.workspaceFolders[0].uri.fsPath}/.foam/templates`;
const templatesDir = `${workspace.workspaceFolders[0].uri.path}/.foam/templates`;
async function getTemplates(): Promise<string[]> {
const templates = await workspace.findFiles(".foam/templates/**.md");
const templates = await workspace.findFiles('.foam/templates/**.md');
// parse title, not whole file!
return templates.map(template => path.basename(template.fsPath));
return templates.map(template => path.basename(template.path));
}
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
"foam-vscode.create-note-from-template",
'foam-vscode.create-note-from-template',
async () => {
const templates = await getTemplates();
const activeFile = window.activeTextEditor?.document?.fileName;
const currentDir =
activeFile !== undefined
? path.dirname(activeFile)
: workspace.workspaceFolders[0].uri.fsPath;
: workspace.workspaceFolders[0].uri.path;
const selectedTemplate = await window.showQuickPick(templates);
const folder = await window.showInputBox({
prompt: `Where should the template be created?`,
value: currentDir
value: currentDir,
});
let filename = await window.showInputBox({
prompt: `Enter the filename for the new note`,
value: ``,
validateInput: value =>
value.length ? undefined : "Please enter a value!"
value.length ? undefined : 'Please enter a value!',
});
filename = path.extname(filename).length
? filename
@@ -54,14 +54,14 @@ const feature: FoamFeature = {
const snippet = new SnippetString(templateText.toString());
await workspace.fs.writeFile(
Uri.file(targetFile),
new TextEncoder().encode("")
new TextEncoder().encode('')
);
await focusNote(targetFile, true);
await window.activeTextEditor.insertSnippet(snippet);
}
)
);
}
},
};
export default feature;

View File

@@ -1,128 +1,156 @@
import * as vscode from "vscode";
import * as path from "path";
import { FoamFeature } from "../types";
import { Foam, Logger } from "foam-core";
import { TextDecoder } from "util";
import { getTitleMaxLength } from "../settings";
import { isSome } from "../utils";
import * as vscode from 'vscode';
import * as path from 'path';
import { FoamFeature } from '../types';
import { Foam, Logger } from 'foam-core';
import { TextDecoder } from 'util';
import { getGraphStyle, getTitleMaxLength } from '../settings';
import { isSome } from '../utils';
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
vscode.commands.registerCommand("foam-vscode.show-graph", async () => {
const foam = await foamPromise;
const panel = await createGraphPanel(foam, context);
const onFoamChanged = _ => {
updateGraph(panel, foam);
};
const noteAddedListener = foam.notes.onDidAddNote(onFoamChanged);
const noteUpdatedListener = foam.notes.onDidUpdateNote(onFoamChanged);
const noteDeletedListener = foam.notes.onDidDeleteNote(onFoamChanged);
panel.onDidDispose(() => {
noteAddedListener.dispose();
noteUpdatedListener.dispose();
noteDeletedListener.dispose();
});
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === "file") {
const note = foam.notes.getNoteByURI(e.document.uri.fsPath);
if (isSome(note)) {
panel.webview.postMessage({
type: "didSelectNote",
payload: note.id
});
}
}
});
let panel: vscode.WebviewPanel | undefined = undefined;
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('foam.graph.style')) {
const style = getGraphStyle();
panel.webview.postMessage({
type: 'didUpdateStyle',
payload: style,
});
}
});
}
vscode.commands.registerCommand('foam-vscode.show-graph', async () => {
if (panel) {
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
panel.reveal(columnToShowIn);
} else {
const foam = await foamPromise;
panel = await createGraphPanel(foam, context);
const onFoamChanged = _ => {
updateGraph(panel, foam);
};
const noteAddedListener = foam.notes.onDidAddNote(onFoamChanged);
const noteUpdatedListener = foam.notes.onDidUpdateNote(onFoamChanged);
const noteDeletedListener = foam.notes.onDidDeleteNote(onFoamChanged);
panel.onDidDispose(() => {
noteAddedListener.dispose();
noteUpdatedListener.dispose();
noteDeletedListener.dispose();
panel = undefined;
});
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === 'file') {
const note = foam.notes.getNote(e.document.uri);
if (isSome(note)) {
panel.webview.postMessage({
type: 'didSelectNote',
payload: note.uri.path,
});
}
}
});
}
});
},
};
function updateGraph(panel: vscode.WebviewPanel, foam: Foam) {
const graph = generateGraphData(foam);
panel.webview.postMessage({
type: "didUpdateGraphData",
payload: graph
type: 'didUpdateGraphData',
payload: graph,
});
}
function generateGraphData(foam: Foam) {
const graph = {
nodes: {},
edges: new Set()
edges: new Set(),
};
foam.notes.getNotes().forEach(n => {
const links = foam.notes.getForwardLinks(n.id);
graph.nodes[n.id] = {
id: n.id,
type: "note",
uri: n.source.uri,
title: cutTitle(n.title)
const links = foam.notes.getForwardLinks(n.uri);
graph.nodes[n.uri.path] = {
id: n.uri.path,
type: n.properties.type ?? 'note',
uri: n.uri,
title: cutTitle(n.title),
};
links.forEach(link => {
if (!(link.to in graph.nodes)) {
graph.nodes[link.to] = {
if (!(link.to.path in graph.nodes)) {
graph.nodes[link.to.path] = {
id: link.to,
type: "nonExistingNote",
type: 'placeholder',
uri: `virtual:${link.to}`,
title: cutTitle(link.link.slug)
title:
'slug' in link.link
? cutTitle(link.link.slug)
: cutTitle(link.link.label),
};
}
graph.edges.add({
source: link.from,
target: link.to
source: link.from.path,
target: link.to.path,
});
});
});
return {
nodes: graph.nodes,
links: Array.from(graph.edges)
links: Array.from(graph.edges),
};
}
function cutTitle(title: string): string {
const maxLen = getTitleMaxLength();
if (maxLen > 0 && title.length > maxLen) {
return title.substring(0, maxLen).concat("...");
return title.substring(0, maxLen).concat('...');
}
return title;
}
async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
"foam-graph",
"Foam Graph",
'foam-graph',
'Foam Graph',
vscode.ViewColumn.Two,
{
enableScripts: true,
retainContextWhenHidden: true
retainContextWhenHidden: true,
}
);
panel.webview.html = await getWebviewContent(context, panel);
panel.webview.onDidReceiveMessage(
message => {
async message => {
switch (message.type) {
case "webviewDidLoad":
case 'webviewDidLoad':
const styles = getGraphStyle();
panel.webview.postMessage({
type: 'didUpdateStyle',
payload: styles,
});
updateGraph(panel, foam);
break;
case "webviewDidSelectNode":
const noteId = message.payload;
const noteUri = foam.notes.getNote(noteId).source.uri;
const openPath = vscode.Uri.file(noteUri);
case 'webviewDidSelectNode':
const noteUri = vscode.Uri.parse(message.payload);
const selectedNote = foam.notes.getNote(noteUri);
vscode.workspace.openTextDocument(openPath).then(doc => {
if (isSome(selectedNote)) {
const doc = await vscode.workspace.openTextDocument(
selectedNote.uri.path // vscode doesn't recognize the URI directly
);
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
});
}
break;
case "error":
Logger.error("An error occurred in the graph view", message.payload);
case 'error':
Logger.error('An error occurred in the graph view', message.payload);
break;
}
},
@@ -138,27 +166,27 @@ async function getWebviewContent(
panel: vscode.WebviewPanel
) {
const webviewPath = vscode.Uri.file(
path.join(context.extensionPath, "static", "dataviz.html")
path.join(context.extensionPath, 'static', 'dataviz.html')
);
const file = await vscode.workspace.fs.readFile(webviewPath);
const text = new TextDecoder("utf-8").decode(file);
const text = new TextDecoder('utf-8').decode(file);
const webviewUri = (fileName: string) =>
panel.webview
.asWebviewUri(
vscode.Uri.file(path.join(context.extensionPath, "static", fileName))
vscode.Uri.file(path.join(context.extensionPath, 'static', fileName))
)
.toString();
const graphDirectory = path.join("graphs", "default");
const graphDirectory = path.join('graphs', 'default');
const textWithVariables = text
.replace(
"${graphPath}",
"{{" + path.join(graphDirectory, "graph.js") + "}}"
'${graphPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.js') + '}}'
)
.replace(
"${graphStylesPath}",
"{{" + path.join(graphDirectory, "graph.css") + "}}"
'${graphStylesPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.css') + '}}'
);
// Basic templating. Will replace the script paths with the

View File

@@ -1,20 +1,24 @@
import createReferences from "./wikilink-reference-generation";
import openDailyNote from "./open-daily-note";
import janitor from "./janitor";
import dataviz from "./dataviz";
import copyWithoutBrackets from "./copy-without-brackets";
import openDatedNote from "./open-dated-note";
import tagsExplorer from "./tags-tree-view";
import createFromTemplate from "./create-from-template";
import { FoamFeature } from "../types";
import createReferences from './wikilink-reference-generation';
import openDailyNote from './open-daily-note';
import janitor from './janitor';
import dataviz from './dataviz';
import copyWithoutBrackets from './copy-without-brackets';
import openDatedNote from './open-dated-note';
import tagsExplorer from './tags-tree-view';
import createFromTemplate from './create-from-template';
import openRandomNote from './open-random-note';
import orphans from './orphans';
import { FoamFeature } from '../types';
export const features: FoamFeature[] = [
tagsExplorer,
createReferences,
openDailyNote,
openRandomNote,
janitor,
dataviz,
copyWithoutBrackets,
openDatedNote,
createFromTemplate
createFromTemplate,
orphans,
];

View File

@@ -4,31 +4,31 @@ import {
ExtensionContext,
commands,
Range,
ProgressLocation
} from "vscode";
import * as fs from "fs";
import { FoamFeature } from "../types";
ProgressLocation,
} from 'vscode';
import * as fs from 'fs';
import { FoamFeature } from '../types';
import {
applyTextEdit,
generateLinkReferences,
generateHeading,
Foam
} from "foam-core";
Foam,
} from 'foam-core';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting
} from "../settings";
import { astPositionToVsCodePosition } from "../utils";
LinkReferenceDefinitionsSetting,
} from '../settings';
import { astPositionToVsCodePosition } from '../utils';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.janitor", async () =>
commands.registerCommand('foam-vscode.janitor', async () =>
janitor(await foamPromise)
)
);
}
},
};
async function janitor(foam: Foam) {
@@ -44,7 +44,7 @@ async function janitor(foam: Foam) {
const outcome = await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Running Foam Janitor across ${noOfFiles} files!`
title: `Running Foam Janitor across ${noOfFiles} files!`,
},
() => runJanitor(foam)
);
@@ -75,21 +75,21 @@ async function runJanitor(foam: Foam) {
const dirtyTextDocuments = workspace.textDocuments.filter(
textDocument =>
(textDocument.languageId === "markdown" ||
textDocument.languageId === "mdx") &&
(textDocument.languageId === 'markdown' ||
textDocument.languageId === 'mdx') &&
textDocument.isDirty
);
const dirtyEditorsFileName = dirtyTextDocuments.map(
dirtyTextDocument => dirtyTextDocument.fileName
dirtyTextDocument => dirtyTextDocument.uri.fsPath
);
const dirtyNotes = notes.filter(note =>
dirtyEditorsFileName.includes(note.source.uri)
dirtyEditorsFileName.includes(note.uri.fsPath)
);
const nonDirtyNotes = notes.filter(
note => !dirtyEditorsFileName.includes(note.source.uri)
note => !dirtyEditorsFileName.includes(note.uri.fsPath)
);
const wikilinkSetting = getWikilinkDefinitionSetting();
@@ -125,7 +125,7 @@ async function runJanitor(foam: Foam) {
text = definitions ? applyTextEdit(text, definitions) : text;
text = heading ? applyTextEdit(text, heading) : text;
return fs.promises.writeFile(note.source.uri, text);
return fs.promises.writeFile(note.uri.fsPath, text);
});
await Promise.all(fileWritePromises);
@@ -135,7 +135,7 @@ async function runJanitor(foam: Foam) {
for (const doc of dirtyTextDocuments) {
const editor = await window.showTextDocument(doc);
const note = dirtyNotes.find(
n => n.source.uri === editor.document.fileName
n => n.uri.fsPath === editor.document.uri.fsPath
)!;
// Get edits
@@ -151,6 +151,7 @@ async function runJanitor(foam: Foam) {
if (heading || definitions) {
// Apply Edits
/* eslint-disable */
await editor.edit(editBuilder => {
// Note: The ordering matters. Definitions need to be inserted
// before heading, since inserting a heading changes line numbers below
@@ -169,13 +170,14 @@ async function runJanitor(foam: Foam) {
editBuilder.replace(start, heading.newText);
}
});
/* eslint-enable */
}
}
return {
updatedHeadingCount,
updatedDefinitionListCount,
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount,
};
}

View File

@@ -1,13 +1,13 @@
import { ExtensionContext, commands } from "vscode";
import { FoamFeature } from "../types";
import { openDailyNoteFor } from "../dated-notes";
import { ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../types';
import { openDailyNoteFor } from '../dated-notes';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.open-daily-note", openDailyNoteFor)
commands.registerCommand('foam-vscode.open-daily-note', openDailyNoteFor)
);
}
},
};
export default feature;

View File

@@ -6,16 +6,16 @@ import {
CompletionItemProvider,
CompletionItem,
CompletionItemKind,
CompletionList
} from "vscode";
CompletionList,
CompletionTriggerKind,
} from 'vscode';
import {
createDailyNoteIfNotExists,
getDailyNoteFileName,
openDailyNoteFor,
getDailyNotePath
} from "../dated-notes";
import { LinkReferenceDefinitionsSetting } from "../settings";
import { FoamFeature } from "../types";
getDailyNotePath,
} from '../dated-notes';
import { FoamFeature } from '../types';
interface DateSnippet {
snippet: string;
@@ -24,22 +24,18 @@ interface DateSnippet {
}
const daysOfWeek = [
{ day: "sunday", index: 0 },
{ day: "monday", index: 1 },
{ day: "tuesday", index: 2 },
{ day: "wednesday", index: 3 },
{ day: "thursday", index: 4 },
{ day: "friday", index: 5 },
{ day: "saturday", index: 6 }
{ day: 'sunday', index: 0 },
{ day: 'monday', index: 1 },
{ day: 'tuesday', index: 2 },
{ day: 'wednesday', index: 3 },
{ day: 'thursday', index: 4 },
{ day: 'friday', index: 5 },
{ day: 'saturday', index: 6 },
];
type AfterCompletionOptions = "noop" | "createNote" | "navigateToNote";
const foamConfig = workspace.getConfiguration("foam");
const foamExtension = foamConfig.get("openDailyNote.fileExtension");
const foamLinkReferenceDefinitions = foamConfig.get(
"edit.linkReferenceDefinitions"
);
type AfterCompletionOptions = 'noop' | 'createNote' | 'navigateToNote';
const foamConfig = workspace.getConfiguration('foam');
const foamNavigateOnSelect: AfterCompletionOptions = foamConfig.get(
"dateSnippets.afterCompletion"
'dateSnippets.afterCompletion'
);
const generateDayOfWeekSnippets = (): DateSnippet[] => {
@@ -55,7 +51,7 @@ const generateDayOfWeekSnippets = (): DateSnippet[] => {
return {
date: target,
detail: `Get a daily note link for ${day}`,
snippet: `/${day}`
snippet: `/${day}`,
};
});
return snippets;
@@ -68,54 +64,57 @@ const createCompletionItem = ({ snippet, date, detail }: DateSnippet) => {
);
completionItem.insertText = getDailyNoteLink(date);
completionItem.detail = `${completionItem.insertText} - ${detail}`;
if (foamNavigateOnSelect !== "noop") {
if (foamNavigateOnSelect !== 'noop') {
completionItem.command = {
command: "foam-vscode.open-dated-note",
title: "Open a note for the given date",
arguments: [date]
command: 'foam-vscode.open-dated-note',
title: 'Open a note for the given date',
arguments: [date],
};
}
return completionItem;
};
const getDailyNoteLink = (date: Date) => {
let name = getDailyNoteFileName(foamConfig, date);
if (
foamLinkReferenceDefinitions ===
LinkReferenceDefinitionsSetting.withoutExtensions
) {
name = name.replace(`.${foamExtension}`, "");
}
return `[[${name}]]`;
const foamExtension = foamConfig.get('openDailyNote.fileExtension');
const name = getDailyNoteFileName(foamConfig, date);
return `[[${name.replace(`.${foamExtension}`, '')}]]`;
};
const snippets: (() => DateSnippet)[] = [
() => ({
detail: "Insert a link to today's daily note",
snippet: "/day",
date: new Date()
snippet: '/day',
date: new Date(),
}),
() => ({
detail: "Insert a link to today's daily note",
snippet: "/today",
date: new Date()
snippet: '/today',
date: new Date(),
}),
() => {
const today = new Date();
return {
detail: "Insert a link to tomorrow's daily note",
snippet: "/tomorrow",
date: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
snippet: '/tomorrow',
date: new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 1
),
};
},
() => {
const today = new Date();
return {
detail: "Insert a link to yesterday's daily note",
snippet: "/yesterday",
date: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
snippet: '/yesterday',
date: new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() - 1
),
};
}
},
];
const computedSnippets: ((number: number) => DateSnippet)[] = [
@@ -128,7 +127,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth(),
today.getDate() + days
)
),
};
},
(weeks: number) => {
@@ -140,7 +139,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth(),
today.getDate() + 7 * weeks
)
),
};
},
(months: number) => {
@@ -152,7 +151,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth() + months,
today.getDate()
)
),
};
},
(years: number) => {
@@ -164,27 +163,39 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear() + years,
today.getMonth(),
today.getDate()
)
),
};
}
},
];
const completions: CompletionItemProvider = {
provideCompletionItems: (_document, _position, _token, _context) => {
if (_context.triggerKind === CompletionTriggerKind.Invoke) {
// if completion was triggered without trigger character then we return [] to fallback
// to vscode word-based suggestions (see https://github.com/foambubble/foam/pull/417)
return [];
}
const completionItems = [
...snippets.map(item => createCompletionItem(item())),
...generateDayOfWeekSnippets().map(item => createCompletionItem(item))
...generateDayOfWeekSnippets().map(item => createCompletionItem(item)),
];
return completionItems;
}
},
};
const computedCompletions: CompletionItemProvider = {
provideCompletionItems: (document, position, _token, _context) => {
if (_context.triggerKind === CompletionTriggerKind.Invoke) {
// if completion was triggered without trigger character then we return [] to fallback
// to vscode word-based suggestions (see https://github.com/foambubble/foam/pull/417)
return [];
}
const range = document.getWordRangeAtPosition(position, /\S+/);
const snippetString = document.getText(range);
const matches = snippetString.match(/(\d+)/);
const number: string = matches ? matches[0] : "1";
const number: string = matches ? matches[0] : '1';
const completionItems = computedSnippets.map(item => {
const completionItem = createCompletionItem(item(parseInt(number)));
completionItem.range = range;
@@ -192,14 +203,14 @@ const computedCompletions: CompletionItemProvider = {
});
// We still want the list to be treated as "incomplete", because the user may add another number
return new CompletionList(completionItems, true);
}
},
};
const datedNoteCommand = (date: Date) => {
if (foamNavigateOnSelect === "navigateToNote") {
if (foamNavigateOnSelect === 'navigateToNote') {
return openDailyNoteFor(date);
}
if (foamNavigateOnSelect === "createNote") {
if (foamNavigateOnSelect === 'createNote') {
return createDailyNoteIfNotExists(
foamConfig,
getDailyNotePath(foamConfig, date),
@@ -211,18 +222,18 @@ const datedNoteCommand = (date: Date) => {
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.open-dated-note", date =>
commands.registerCommand('foam-vscode.open-dated-note', date =>
datedNoteCommand(date)
)
);
languages.registerCompletionItemProvider("markdown", completions, "/");
languages.registerCompletionItemProvider('markdown', completions, '/');
languages.registerCompletionItemProvider(
"markdown",
'markdown',
computedCompletions,
"/",
"+"
'/',
'+'
);
}
},
};
export default feature;

View File

@@ -0,0 +1,31 @@
import { Foam } from 'foam-core';
import { ExtensionContext, commands, window } from 'vscode';
import { FoamFeature } from '../types';
import { focusNote } from '../utils';
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.notes.getNotes();
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;
}
focusNote(notes[randomNoteIndex].uri.path, false);
})
);
},
};
export default feature;

View File

@@ -0,0 +1,123 @@
import { OrphansProvider, Directory, OrphansProviderConfig } from './orphans';
import { OrphansConfigGroupBy } from '../settings';
describe('orphans', () => {
// Rough mocks of NoteGraphAPI
const orphanA = {
uri: { fsPath: '/path/orphan-a.md', path: '/path/orphan-a.md' },
title: 'Orphan A',
links: [],
};
const orphanB = {
uri: { fsPath: '/path-bis/orphan-b.md', path: '/path-bis/orphan-b.md' },
title: 'Orphan B',
links: [],
};
const orphanC = {
uri: {
fsPath: '/path-exclude/orphan-c.md',
path: '/path-exclude/orphan-c.md',
},
title: 'Orphan C',
links: [],
};
const notOrphanNote = {
uri: { fsPath: '/path/not-orphan.md', path: '/path/not-orphan.md' },
title: 'Not-Orphan',
links: [{ from: '', to: '' }],
};
const notes = [orphanA, orphanB, orphanC, notOrphanNote];
const foam = {
notes: {
getNotes: () => notes,
getAllLinks: (uri: { path: string }) => {
switch (uri.path) {
case orphanA.uri.fsPath:
return orphanA.links;
case orphanB.uri.fsPath:
return orphanB.links;
case orphanC.uri.fsPath:
return orphanC.links;
default:
return notOrphanNote.links;
}
},
},
} as any;
// Mock config
const config: OrphansProviderConfig = {
exclude: ['path-exclude/**/*'],
groupBy: OrphansConfigGroupBy.Folder,
workspacesFsPaths: [''],
};
it('should return the orphans as a folder tree', async () => {
const provider = new OrphansProvider(foam, config);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 1,
label: '/path',
description: '1 orphan',
notes: [{ title: 'Orphan A' }],
},
{
collapsibleState: 1,
label: '/path-bis',
description: '1 orphan',
notes: [{ title: 'Orphan B' }],
},
]);
});
it('should return the orphans in a directory', async () => {
const provider = new OrphansProvider(foam, config);
const directory = new Directory('/path', [orphanA as any]);
const result = await provider.getChildren(directory);
expect(result).toMatchObject([
{
collapsibleState: 0,
label: 'Orphan A',
description: '/path/orphan-a.md',
command: { command: 'vscode.open' },
},
]);
});
it('should return the flattened orphans', async () => {
const mockConfig = { ...config, groupBy: OrphansConfigGroupBy.Off };
const provider = new OrphansProvider(foam, mockConfig);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 0,
label: 'Orphan A',
description: '/path/orphan-a.md',
command: { command: 'vscode.open' },
},
{
collapsibleState: 0,
label: 'Orphan B',
description: '/path-bis/orphan-b.md',
command: { command: 'vscode.open' },
},
]);
});
it('should return the orphans without exclusion', async () => {
const mockConfig = { ...config, exclude: [] };
const provider = new OrphansProvider(foam, mockConfig);
const result = await provider.getChildren();
expect(result).toMatchObject([
expect.anything(),
expect.anything(),
{
collapsibleState: 1,
label: '/path-exclude',
description: '1 orphan',
notes: [{ title: 'Orphan C' }],
},
]);
});
});

View File

@@ -0,0 +1,184 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { Foam, Note, URI } from 'foam-core';
import micromatch from 'micromatch';
import {
getOrphansConfig,
OrphansConfig,
OrphansConfigGroupBy,
} from '../settings';
import { FoamFeature } from '../types';
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesFsPaths = vscode.workspace.workspaceFolders.map(
dir => dir.uri.fsPath
);
const provider = new OrphansProvider(foam, {
...getOrphansConfig(),
workspacesFsPaths,
});
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
vscode.commands.registerCommand(
'foam-vscode.group-orphans-by-folder',
() => provider.setGroupBy(OrphansConfigGroupBy.Folder)
),
vscode.commands.registerCommand('foam-vscode.group-orphans-off', () =>
provider.setGroupBy(OrphansConfigGroupBy.Off)
),
foam.notes.onDidUpdateNote(() => provider.refresh())
);
},
};
export default feature;
export class OrphansProvider
implements vscode.TreeDataProvider<OrphanTreeItem> {
// prettier-ignore
private _onDidChangeTreeData: vscode.EventEmitter<OrphanTreeItem | undefined | void> = new vscode.EventEmitter<OrphanTreeItem | undefined | void>();
// prettier-ignore
readonly onDidChangeTreeData: vscode.Event<OrphanTreeItem | undefined | void> = this._onDidChangeTreeData.event;
private groupBy: OrphansConfigGroupBy = OrphansConfigGroupBy.Folder;
private exclude: string[] = [];
private orphans: Note[] = [];
private root = vscode.workspace.workspaceFolders[0].uri.fsPath;
constructor(private foam: Foam, config: OrphansProviderConfig) {
this.groupBy = config.groupBy;
this.exclude = this.getGlobs(config.workspacesFsPaths, config.exclude);
this.setContext();
this.computeOrphans();
}
setGroupBy(groupBy: OrphansConfigGroupBy): void {
this.groupBy = groupBy;
this.setContext();
this.refresh();
}
private setContext(): void {
vscode.commands.executeCommand(
'setContext',
'foam-vscode.orphans-grouped-by-folder',
this.groupBy === OrphansConfigGroupBy.Folder
);
}
refresh(): void {
this.computeOrphans();
this._onDidChangeTreeData.fire();
}
getTreeItem(item: OrphanTreeItem): vscode.TreeItem {
return item;
}
getChildren(directory?: Directory): Thenable<OrphanTreeItem[]> {
if (!directory && this.groupBy === OrphansConfigGroupBy.Folder) {
const directories = Object.entries(this.getOrphansByDirectory())
.sort(([a], [b]) => a.localeCompare(b))
.map(([dir, orphans]) => new Directory(dir, orphans));
return Promise.resolve(directories);
}
if (directory) {
const orphans = directory.notes.map(o => new Orphan(o));
return Promise.resolve(orphans);
}
const orphans = this.orphans.map(o => new Orphan(o));
return Promise.resolve(orphans);
}
private computeOrphans(): void {
this.orphans = this.foam.notes
.getNotes()
.filter(note => !this.foam.notes.getAllLinks(note.uri).length)
.filter(note => !this.isMatch(note.uri))
.sort((a, b) => a.title.localeCompare(b.title));
}
private isMatch(uri: URI) {
return micromatch.isMatch(uri.fsPath, this.exclude);
}
private getGlobs(fsPaths: string[], globs: string[]): string[] {
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
const exclude: string[] = [];
for (const fsPath of fsPaths) {
let folder = fsPath.replace(/\\/g, '/');
if (folder.substr(-1) === '/') {
folder = folder.slice(0, -1);
}
exclude.push(...globs.map(g => `${folder}/${g}`));
}
return exclude;
}
private getOrphansByDirectory(): OrphansByDirectory {
const orphans: OrphansByDirectory = {};
for (const orphan of this.orphans) {
const p = orphan.uri.fsPath.replace(this.root, '');
const { dir } = path.parse(p);
if (orphans[dir]) {
orphans[dir].push(orphan);
} else {
orphans[dir] = [orphan];
}
}
for (const k in orphans) {
orphans[k].sort((a, b) => a.title.localeCompare(b.title));
}
return orphans;
}
}
export interface OrphansProviderConfig extends OrphansConfig {
workspacesFsPaths: string[];
}
type OrphansByDirectory = { [key: string]: Note[] };
type OrphanTreeItem = Orphan | Directory;
class Orphan extends vscode.TreeItem {
constructor(public readonly note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.description = note.uri.path;
this.tooltip = this.description;
this.command = {
command: 'vscode.open',
title: 'Open File',
arguments: [note.uri],
};
}
iconPath = new vscode.ThemeIcon('note');
contextValue = 'orphan';
}
export class Directory extends vscode.TreeItem {
constructor(public readonly dir: string, public readonly notes: Note[]) {
super(dir, vscode.TreeItemCollapsibleState.Collapsed);
const s = this.notes.length > 1 ? 's' : '';
this.description = `${this.notes.length} orphan${s}`;
this.tooltip = this.description;
}
iconPath = new vscode.ThemeIcon('folder');
contextValue = 'directory';
}

View File

@@ -1,6 +1,6 @@
import * as vscode from "vscode";
import { FoamFeature } from "../../types";
import { Foam, Note } from "foam-core";
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { Foam, Note } from 'foam-core';
const feature: FoamFeature = {
activate: async (
@@ -11,12 +11,12 @@ const feature: FoamFeature = {
const provider = new TagsProvider(foam);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
"foam-vscode.tags-explorer",
'foam-vscode.tags-explorer',
provider
)
);
foam.notes.onDidUpdateNote(() => provider.refresh());
}
},
};
export default feature;
@@ -29,7 +29,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private tags: {
tag: string;
noteIds: string[];
noteUris: vscode.Uri[];
}[];
constructor(private foam: Foam) {
@@ -43,16 +43,16 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private computeTags() {
const rawTags: {
[key: string]: string[];
[key: string]: vscode.Uri[];
} = this.foam.notes.getNotes().reduce((acc, note) => {
note.tags.forEach(tag => {
acc[tag] = acc[tag] ?? [];
acc[tag].push(note.id);
acc[tag].push(note.uri);
});
return acc;
}, {});
this.tags = Object.entries(rawTags)
.map(([tag, noteIds]) => ({ tag, noteIds }))
.map(([tag, noteUris]) => ({ tag, noteUris }))
.sort((a, b) => a.tag.localeCompare(b.tag));
}
@@ -62,18 +62,18 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
if (element) {
const references: TagReference[] = element.noteIds.map(id => {
const references: TagReference[] = element.noteUris.map(id => {
const note = this.foam.notes.getNote(id);
return new TagReference(element.tag, note);
});
return Promise.resolve([
new TagSearch(element.tag),
...references.sort((a, b) => a.title.localeCompare(b.title))
...references.sort((a, b) => a.title.localeCompare(b.title)),
]);
}
if (!element) {
const tags: Tag[] = this.tags.map(
({ tag, noteIds }) => new Tag(tag, noteIds)
({ tag, noteUris }) => new Tag(tag, noteUris)
);
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
}
@@ -83,16 +83,19 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
type TagTreeItem = Tag | TagReference | TagSearch;
export class Tag extends vscode.TreeItem {
constructor(public readonly tag: string, public readonly noteIds: string[]) {
constructor(
public readonly tag: string,
public readonly noteUris: vscode.Uri[]
) {
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
this.description = `${this.noteIds.length} reference${
this.noteIds.length !== 1 ? "s" : ""
this.description = `${this.noteUris.length} reference${
this.noteUris.length !== 1 ? 's' : ''
}`;
this.tooltip = this.description;
}
iconPath = new vscode.ThemeIcon("symbol-number");
contextValue = "tag";
iconPath = new vscode.ThemeIcon('symbol-number');
contextValue = 'tag';
}
export class TagSearch extends vscode.TreeItem {
@@ -101,21 +104,21 @@ export class TagSearch extends vscode.TreeItem {
const searchString = `#${tag}`;
this.tooltip = `Search ${searchString} in workspace`;
this.command = {
command: "workbench.action.findInFiles",
command: 'workbench.action.findInFiles',
arguments: [
{
query: searchString,
triggerSearch: true,
matchWholeWord: true,
isCaseSensitive: true
}
isCaseSensitive: true,
},
],
title: "Search"
title: 'Search',
};
}
iconPath = new vscode.ThemeIcon("search");
contextValue = "tag-search";
iconPath = new vscode.ThemeIcon('search');
contextValue = 'tag-search';
}
export class TagReference extends vscode.TreeItem {
@@ -123,9 +126,9 @@ export class TagReference extends vscode.TreeItem {
constructor(tag: string, note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.title = note.title;
this.description = note.source.uri;
this.description = note.uri.path;
this.tooltip = this.description;
const resourceUri = vscode.Uri.file(note.source.uri);
const resourceUri = note.uri;
let selection: vscode.Range | null = null;
// TODO move search fn to core
const lines = note.source.text.split(/\r?\n/);
@@ -139,18 +142,18 @@ export class TagReference extends vscode.TreeItem {
// TODO I like about this showing the git state of the note, but I don't like the md icon
this.resourceUri = resourceUri;
this.command = {
command: "vscode.open",
command: 'vscode.open',
arguments: [
resourceUri,
{
preview: true,
selection: selection
}
selection: selection,
},
],
title: "Open File"
title: 'Open File',
};
}
iconPath = new vscode.ThemeIcon("note");
contextValue = "reference";
iconPath = new vscode.ThemeIcon('note');
contextValue = 'reference';
}

View File

@@ -1,4 +1,4 @@
import { uniq } from "lodash";
import { uniq } from 'lodash';
import {
CancellationToken,
CodeLens,
@@ -10,8 +10,8 @@ import {
TextDocument,
window,
workspace,
Position
} from "vscode";
Position,
} from 'vscode';
import {
createMarkdownReferences,
@@ -19,33 +19,33 @@ import {
NoteGraphAPI,
Foam,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER
} from "foam-core";
LINK_REFERENCE_DEFINITION_FOOTER,
} from 'foam-core';
import {
hasEmptyTrailing,
docConfig,
loadDocConfig,
isMdEditor,
mdDocSelector,
getText
} from "../utils";
import { FoamFeature } from "../types";
getText,
} from '../utils';
import { FoamFeature } from '../types';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting
} from "../settings";
LinkReferenceDefinitionsSetting,
} from '../settings';
const feature: FoamFeature = {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
const foam = await foamPromise;
context.subscriptions.push(
commands.registerCommand("foam-vscode.update-wikilinks", () =>
commands.registerCommand('foam-vscode.update-wikilinks', () =>
updateReferenceList(foam.notes)
),
workspace.onWillSaveTextDocument(e => {
if (e.document.languageId === "markdown") {
if (e.document.languageId === 'markdown') {
updateDocumentInNoteGraph(foam, e.document);
e.waitUntil(updateReferenceList(foam.notes));
}
@@ -67,12 +67,12 @@ const feature: FoamFeature = {
updateDocumentInNoteGraph(foam, editor.document);
updateReferenceList(foam.notes);
});
}
},
};
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
foam.notes.setNote(
foam.parse(document.fileName, document.getText(), docConfig.eol)
foam.parse(document.uri, document.getText(), docConfig.eol)
);
}
@@ -119,7 +119,7 @@ async function updateReferenceList(foam: NoteGraphAPI) {
// references must always be preceded by an empty line
const spacing = doc.lineAt(range.start.line - 1).isEmptyOrWhitespace
? ""
? ''
: docConfig.eol;
await editor.edit(editBuilder => {
@@ -138,15 +138,13 @@ function generateReferenceList(
return [];
}
const filePath = doc.fileName;
const note = foam.getNoteByURI(filePath);
const note = foam.getNote(doc.uri);
// 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 ${filePath} before attempting to generate its markdown reference list`
`Can't find note for URI ${doc.uri.path} before attempting to generate its markdown reference list`
);
return [];
}
@@ -154,7 +152,7 @@ function generateReferenceList(
const references = uniq(
createMarkdownReferences(
foam,
note.id,
note.uri,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
).map(stringifyMarkdownLinkReferenceDefinition)
);
@@ -163,7 +161,7 @@ function generateReferenceList(
return [
LINK_REFERENCE_DEFINITION_HEADER,
...references,
LINK_REFERENCE_DEFINITION_FOOTER
LINK_REFERENCE_DEFINITION_FOOTER,
];
}
@@ -222,14 +220,14 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
const newRefs = refs.join(docConfig.eol);
let status = oldRefs === newRefs ? "up to date" : "out of date";
let status = oldRefs === newRefs ? 'up to date' : 'out of date';
return [
new CodeLens(range, {
arguments: [],
title: `Link references (${status})`,
command: ""
})
command: '',
}),
];
}
}

View File

@@ -1,17 +1,15 @@
import { workspace } from "vscode";
import { FoamConfig, createConfigFromFolders } from "foam-core";
import { getIgnoredFilesSetting } from "../settings";
import { workspace } from 'vscode';
import { FoamConfig, createConfigFromFolders } from 'foam-core';
import { getIgnoredFilesSetting } from '../settings';
// TODO this is still to be improved - foam config should
// not be dependent on vscode but at the moment it's convenient
// to leverage it
export const getConfigFromVscode = (): FoamConfig => {
const workspaceFolders = workspace.workspaceFolders.map(
dir => dir.uri.fsPath
);
const excludeGlobs: string[] = getIgnoredFilesSetting();
const workspaceFolders = workspace.workspaceFolders.map(dir => dir.uri);
const excludeGlobs = getIgnoredFilesSetting();
return createConfigFromFolders(workspaceFolders, {
ignore: excludeGlobs
ignore: excludeGlobs.map(g => g.toString()),
});
};

View File

@@ -0,0 +1,68 @@
import {
IDataStore,
Event,
URI,
FoamConfig,
IDisposable,
Logger,
} from 'foam-core';
import { workspace, FileSystemWatcher, EventEmitter } from 'vscode';
import { TextDecoder } from 'util';
import { isSome } from '../utils';
export class VsCodeDataStore implements IDataStore, IDisposable {
onDidCreateEmitter = new EventEmitter<URI>();
onDidChangeEmitter = new EventEmitter<URI>();
onDidDeleteEmitter = new EventEmitter<URI>();
onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
onDidChange: Event<URI> = this.onDidChangeEmitter.event;
onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
watcher: FileSystemWatcher;
files: URI[];
constructor(private config: FoamConfig) {
this.watcher = workspace.createFileSystemWatcher('**/*');
this.watcher.onDidCreate(async uri => {
await this.listFiles();
if (this.isMatch(uri)) {
Logger.info('Created: ', uri);
this.onDidCreateEmitter.fire(uri);
}
});
this.watcher.onDidChange(uri => {
if (this.isMatch(uri)) {
Logger.info('Updated: ', uri);
this.onDidChangeEmitter.fire(uri);
}
});
this.watcher.onDidDelete(uri => {
if (this.isMatch(uri)) {
Logger.info('Deleted: ', uri);
this.files = this.files.filter(f => f.path !== uri.path);
this.onDidDeleteEmitter.fire(uri);
}
});
}
async listFiles(): Promise<URI[]> {
this.files = await workspace.findFiles(
`{${this.config.includeGlobs.join(',')}}`,
`{${this.config.ignoreGlobs.join(',')}}`
);
return this.files;
}
isMatch(uri: URI): boolean {
return isSome(this.files.find(f => f.path === uri.path));
}
async read(uri: URI): Promise<string> {
return new TextDecoder().decode(await workspace.fs.readFile(uri));
}
dispose(): void {
this.watcher.dispose();
}
}

View File

@@ -1,17 +1,17 @@
import { window, commands, ExtensionContext } from "vscode";
import { ILogger, IDisposable, LogLevel, BaseLogger } from "foam-core";
import { getFoamLoggerLevel } from "../settings";
import { window, commands, ExtensionContext } from 'vscode';
import { ILogger, IDisposable, LogLevel, BaseLogger } from 'foam-core';
import { getFoamLoggerLevel } from '../settings';
export interface VsCodeLogger extends ILogger, IDisposable {
show();
}
export class VsCodeOutputLogger extends BaseLogger implements VsCodeLogger {
private channel = window.createOutputChannel("Foam");
private channel = window.createOutputChannel('Foam');
constructor() {
super(getFoamLoggerLevel());
this.channel.appendLine("Foam Logging: " + getFoamLoggerLevel());
this.channel.appendLine('Foam Logging: ' + getFoamLoggerLevel());
}
log(lvl: LogLevel, msg?: any, ...extra: any[]): void {
@@ -42,12 +42,12 @@ export const exposeLogger = (
logger: VsCodeLogger
): void => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.set-log-level", async () => {
const items: LogLevel[] = ["debug", "info", "warn", "error"];
commands.registerCommand('foam-vscode.set-log-level', async () => {
const items: LogLevel[] = ['debug', 'info', 'warn', 'error'];
const level = await window.showQuickPick(
items.map(item => ({
label: item,
description: item === logger.getLevel() && "Current"
description: item === logger.getLevel() && 'Current',
}))
);
logger.setLevel(level.label);

View File

@@ -1,31 +1,57 @@
import { workspace } from "vscode";
import { LogLevel } from "foam-core";
import { workspace, GlobPattern } from 'vscode';
import { LogLevel } from 'foam-core';
export enum LinkReferenceDefinitionsSetting {
withExtensions = "withExtensions",
withoutExtensions = "withoutExtensions",
off = "off"
withExtensions = 'withExtensions',
withoutExtensions = 'withoutExtensions',
off = 'off',
}
export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting {
return workspace
.getConfiguration("foam.edit")
.getConfiguration('foam.edit')
.get<LinkReferenceDefinitionsSetting>(
"linkReferenceDefinitions",
'linkReferenceDefinitions',
LinkReferenceDefinitionsSetting.withoutExtensions
);
}
/** Retrieve the list of file ignoring globs. */
export function getIgnoredFilesSetting(): string[] {
return workspace.getConfiguration("foam.files").get("ignore");
export function getIgnoredFilesSetting(): GlobPattern[] {
return [
...workspace.getConfiguration().get('foam.files.ignore', []),
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
];
}
/** Retrieves the maximum length for a Graph node title. */
export function getTitleMaxLength(): number {
return workspace.getConfiguration("foam.graph").get("titleMaxLength");
return workspace.getConfiguration('foam.graph').get('titleMaxLength');
}
/** Retrieve the graph's style object */
export function getGraphStyle(): object {
return workspace.getConfiguration('foam.graph').get('style');
}
export function getFoamLoggerLevel(): LogLevel {
return workspace.getConfiguration("foam.logging").get("level") ?? "info";
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
}
/** Retrieve the orphans configuration */
export function getOrphansConfig(): OrphansConfig {
const orphansConfig = workspace.getConfiguration('foam.orphans');
const exclude: string[] = orphansConfig.get('exclude');
const groupBy: OrphansConfigGroupBy = orphansConfig.get('groupBy');
return { exclude, groupBy };
}
export interface OrphansConfig {
exclude: string[];
groupBy: OrphansConfigGroupBy;
}
export enum OrphansConfigGroupBy {
Folder = 'folder',
Off = 'off',
}

View File

@@ -0,0 +1,30 @@
import * as path from 'path';
import fs from 'fs';
import os from 'os';
import { runTests } from 'vscode-test';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite');
const tmpWorkspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foam-'));
// Download VS Code, unzip it and run the integration test
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [tmpWorkspaceDir, '--disable-extensions'],
});
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,66 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/testRunner.ts
/**
* We use the following convention in Foam:
* - *.test.ts are unit tests
* they might still rely on vscode API and hence will be run in this environment, but
* are fundamentally about testing functions in isolations
* - *.spec.ts are integration tests
* they will make direct use of the vscode API to be invoked as commands, create editors,
* and so on..
*/
import path from 'path';
import { runCLI } from '@jest/core';
const rootDir = path.resolve(__dirname, '../..');
export function run(): Promise<void> {
process.stdout.write = (buffer: string) => {
console.log(buffer);
return true;
};
process.stderr.write = (buffer: string) => {
console.error(buffer);
return true;
};
process.env.FORCE_COLOR = '1';
process.env.NODE_ENV = 'test';
return new Promise(async (resolve, reject) => {
try {
const { results } = await runCLI(
{
rootDir,
roots: ['<rootDir>/src'],
transform: JSON.stringify({ '^.+\\.ts$': 'ts-jest' }),
runInBand: true,
testRegex: '\\.(test|spec)\\.ts$',
testEnvironment:
'<rootDir>/src/test/support/extended-vscode-environment.js',
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
setupFilesAfterEnv: ['jest-extended'],
globals: JSON.stringify({
'ts-jest': {
tsconfig: path.resolve(rootDir, './tsconfig.json'),
},
}),
testTimeout: 20000,
verbose: true,
colors: true,
} as any,
[rootDir]
);
const failures = results.testResults.reduce(
(acc, res) => (res.failureMessage ? acc + 1 : acc),
0
);
return failures === 0
? resolve()
: reject(`${failures} tests have failed!`);
} catch (error) {
return reject(error);
}
});
}

View File

@@ -0,0 +1,15 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/env/ExtendedVscodeEnvironment.js
const VscodeEnvironment = require('jest-environment-vscode');
class ExtendedVscodeEnvironment extends VscodeEnvironment {
async setup() {
await super.setup();
// Expose RegExp otherwise document.getWordRangeAtPosition won't work as supposed.
// Implementation of getWordRangeAtPosition uses "instanceof RegExp" which returns false
// due to Jest running tests in the different vm context.
// See https://github.com/nodejs/node-v0.x-archive/issues/1277.
this.global.RegExp = RegExp;
}
}
module.exports = ExtendedVscodeEnvironment;

View File

@@ -0,0 +1,2 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/config/jestSetup.ts
jest.mock('vscode', () => (global as any).vscode, { virtual: true });

View File

@@ -1,6 +1,6 @@
import { ExtensionContext } from "vscode";
import { Foam } from "foam-core";
import { ExtensionContext } from 'vscode';
import { Foam } from 'foam-core';
export interface FoamFeature {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void;
}

View File

@@ -0,0 +1,65 @@
import { dropExtension, removeBrackets, toTitleCase } from './utils';
describe('dropExtension', () => {
test('returns file name without extension', () => {
expect(dropExtension('file.md')).toEqual('file');
});
});
describe('removeBrackets', () => {
it('removes the brackets', () => {
const input = 'hello world [[this-is-it]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the md file extension', () => {
const input = 'hello world [[this-is-it.md]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the mdx file extension', () => {
const input = 'hello world [[this-is-it.mdx]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the markdown file extension', () => {
const input = 'hello world [[this-is-it.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets even with numbers', () => {
const input = 'hello world [[2020-07-21.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world 2020 07 21';
expect(actual).toEqual(expected);
});
it('removes brackets for more than one word', () => {
const input =
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
const actual = removeBrackets(input);
const expected =
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
expect(actual).toEqual(expected);
});
});
describe('toTitleCase', () => {
it('title cases a word', () => {
const input =
'look at this really long sentence but I am calling it a word';
const actual = toTitleCase(input);
const expected =
'Look At This Really Long Sentence But I Am Calling It A Word';
expect(actual).toEqual(expected);
});
it('works on one word', () => {
const input = 'word';
const actual = toTitleCase(input);
const expected = 'Word';
expect(actual).toEqual(expected);
});
});

View File

@@ -7,10 +7,10 @@ import {
TextEditor,
workspace,
Uri,
Selection
} from "vscode";
import * as fs from "fs";
import { Logger } from "foam-core";
Selection,
} from 'vscode';
import * as fs from 'fs';
import { Logger } from 'foam-core';
interface Point {
line: number;
@@ -18,34 +18,34 @@ interface Point {
offset?: number;
}
export const docConfig = { tab: " ", eol: "\r\n" };
export const docConfig = { tab: ' ', eol: '\r\n' };
export const mdDocSelector = [
{ language: "markdown", scheme: "file" },
{ language: "markdown", scheme: "untitled" }
{ language: 'markdown', scheme: 'file' },
{ language: 'markdown', scheme: 'untitled' },
];
export function loadDocConfig() {
// Load workspace config
let activeEditor = window.activeTextEditor;
if (!activeEditor) {
Logger.debug("Failed to load config, no active editor");
Logger.debug('Failed to load config, no active editor');
return;
}
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? "\r\n" : "\n";
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? '\r\n' : '\n';
let tabSize = Number(activeEditor.options.tabSize);
let insertSpaces = activeEditor.options.insertSpaces;
if (insertSpaces) {
docConfig.tab = " ".repeat(tabSize);
docConfig.tab = ' '.repeat(tabSize);
} else {
docConfig.tab = "\t";
docConfig.tab = '\t';
}
}
export function isMdEditor(editor: TextEditor) {
return editor && editor.document && editor.document.languageId === "markdown";
return editor && editor.document && editor.document.languageId === 'markdown';
}
export function detectGeneratedCode(
@@ -61,7 +61,7 @@ export function detectGeneratedCode(
if (headerLine < 0 || headerLine >= footerLine) {
return {
range: null,
lines: []
lines: [],
};
}
@@ -70,7 +70,7 @@ export function detectGeneratedCode(
new Position(headerLine, 0),
new Position(footerLine, lines[footerLine].length + 1)
),
lines: lines.slice(headerLine + 1, footerLine + 1)
lines: lines.slice(headerLine + 1, footerLine + 1),
};
}
@@ -83,9 +83,9 @@ export function getText(range: Range): string {
}
export function dropExtension(path: string): string {
const parts = path.split(".");
const parts = path.split('.');
parts.pop();
return parts.join(".");
return parts.join('.');
}
/**
@@ -103,17 +103,17 @@ export const astPositionToVsCodePosition = (point: Point): Position => {
*/
export function removeBrackets(s: string): string {
// take in the string, split on space
const stringSplitBySpace = s.split(" ");
const stringSplitBySpace = s.split(' ');
// loop through words
const modifiedWords = stringSplitBySpace.map(currentWord => {
if (currentWord.includes("[[")) {
if (currentWord.includes('[[')) {
// all of these transformations will turn this "[[you-are-awesome]]"
// to this "you are awesome"
let word = currentWord.replace(/(\[\[)/g, "");
word = word.replace(/(\]\])/g, "");
word = word.replace(/(.mdx|.md|.markdown)/g, "");
word = word.replace(/[-]/g, " ");
let word = currentWord.replace(/(\[\[)/g, '');
word = word.replace(/(\]\])/g, '');
word = word.replace(/(.mdx|.md|.markdown)/g, '');
word = word.replace(/[-]/g, ' ');
// then we titlecase the word so "you are awesome"
// becomes "You Are Awesome"
@@ -125,7 +125,7 @@ export function removeBrackets(s: string): string {
return currentWord;
});
return modifiedWords.join(" ");
return modifiedWords.join(' ');
}
/**
@@ -135,9 +135,9 @@ export function removeBrackets(s: string): string {
*/
export function toTitleCase(word: string): string {
return word
.split(" ")
.split(' ')
.map(word => word[0].toUpperCase() + word.substring(1))
.join(" ");
.join(' ');
}
/**

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