mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
46 Commits
chore/cons
...
v0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4457e83e38 | ||
|
|
fb15672e6a | ||
|
|
2ef2a217ee | ||
|
|
8a73cba1f0 | ||
|
|
b0ea08b84f | ||
|
|
ea0edc5149 | ||
|
|
42dabfbf9d | ||
|
|
85d3aef2ff | ||
|
|
8bd3109325 | ||
|
|
6ca800b500 | ||
|
|
3a798b520f | ||
|
|
8db4d2897f | ||
|
|
e0bcb6bd92 | ||
|
|
cd92468311 | ||
|
|
5a44fbc26f | ||
|
|
4412f860dd | ||
|
|
3e3d36b954 | ||
|
|
eee6b7bd3a | ||
|
|
792e66b061 | ||
|
|
e5589f0555 | ||
|
|
32f40fa0de | ||
|
|
97e3b20112 | ||
|
|
0060ea2a3a | ||
|
|
a11dee89ba | ||
|
|
62e46e87b0 | ||
|
|
ef7ea8d23b | ||
|
|
36a632f218 | ||
|
|
f9331ad327 | ||
|
|
a944d993fc | ||
|
|
adf2dfa779 | ||
|
|
c04bc347ed | ||
|
|
5f8a064af2 | ||
|
|
eaa522fe1b | ||
|
|
3868cc5a17 | ||
|
|
76d70d40f8 | ||
|
|
2e373c4624 | ||
|
|
71948062b1 | ||
|
|
94711cf11b | ||
|
|
7d0858246d | ||
|
|
d998c8d482 | ||
|
|
85e0784c54 | ||
|
|
035fb7b634 | ||
|
|
50075a384b | ||
|
|
71e3581409 | ||
|
|
5249edab38 | ||
|
|
6baeec4db6 |
@@ -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,
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules
|
||||
*.tsbuildinfo
|
||||
*.vsix
|
||||
*.log
|
||||
out
|
||||
dist
|
||||
docs/_site
|
||||
docs/.sass-cache
|
||||
|
||||
42
.vscode/settings.json
vendored
42
.vscode/settings.json
vendored
@@ -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"]
|
||||
}
|
||||
|
||||
BIN
docs/assets/images/graph-style.gif
Normal file
BIN
docs/assets/images/graph-style.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 MiB |
BIN
docs/assets/images/style-node-by-type.png
Normal file
BIN
docs/assets/images/style-node-by-type.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -15,4 +15,4 @@
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
|
||||
- Use the `Markdown Links: Show Graph` command to see the graph
|
||||

|
||||
|
||||
|
||||
@@ -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
10
docs/features/orphans.md
Normal 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.
|
||||
129
docs/index.md
129
docs/index.md
@@ -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"
|
||||
|
||||
@@ -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.)
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.7.3"
|
||||
"version": "0.9.0"
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
436
packages/foam-core/src/common/charCode.ts
Normal file
436
packages/foam-core/src/common/charCode.ts
Normal 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,
|
||||
}
|
||||
197
packages/foam-core/src/common/platform.ts
Normal file
197
packages/foam-core/src/common/platform.ts
Normal 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;
|
||||
}
|
||||
748
packages/foam-core/src/common/uri.ts
Normal file
748
packages/foam-core/src/common/uri.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
171
packages/foam-core/src/model/note-graph.ts
Normal file
171
packages/foam-core/src/model/note-graph.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
72
packages/foam-core/test/datastore.test.ts
Normal file
72
packages/foam-core/test/datastore.test.ts
Normal 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, '/')
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
0
packages/foam-core/test/test-datastore/file-a.md
Normal file
0
packages/foam-core/test/test-datastore/file-a.md
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
3
packages/foam-vscode/.gitignore
vendored
3
packages/foam-vscode/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
out
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -6,15 +6,17 @@
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
packages/foam-vscode/src/features/open-random-note.ts
Normal file
31
packages/foam-vscode/src/features/open-random-note.ts
Normal 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;
|
||||
123
packages/foam-vscode/src/features/orphans.test.ts
Normal file
123
packages/foam-vscode/src/features/orphans.test.ts
Normal 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' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
184
packages/foam-vscode/src/features/orphans.ts
Normal file
184
packages/foam-vscode/src/features/orphans.ts
Normal 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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
};
|
||||
|
||||
68
packages/foam-vscode/src/services/datastore.ts
Normal file
68
packages/foam-vscode/src/services/datastore.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
30
packages/foam-vscode/src/test/run-tests.ts
Normal file
30
packages/foam-vscode/src/test/run-tests.ts
Normal 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();
|
||||
66
packages/foam-vscode/src/test/suite.ts
Normal file
66
packages/foam-vscode/src/test/suite.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
2
packages/foam-vscode/src/test/support/jest-setup.ts
Normal file
2
packages/foam-vscode/src/test/support/jest-setup.ts
Normal 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 });
|
||||
6
packages/foam-vscode/src/types.d.ts
vendored
6
packages/foam-vscode/src/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
65
packages/foam-vscode/src/utils.test.ts
Normal file
65
packages/foam-vscode/src/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user