mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af0c2bbaa3 | ||
|
|
700bfc1b26 | ||
|
|
a6d5c04453 | ||
|
|
b0c42cead2 | ||
|
|
6c643adb9d | ||
|
|
ca7fdefaae | ||
|
|
149d5f5a7c | ||
|
|
be80857fd1 | ||
|
|
611fa7359d | ||
|
|
08b7e7a231 | ||
|
|
0a259168c7 | ||
|
|
f3d0569c76 | ||
|
|
502129d5ac | ||
|
|
d29bf16db1 | ||
|
|
d228c7cb18 | ||
|
|
78078cf338 | ||
|
|
fe65883bc5 | ||
|
|
20b5261c5c | ||
|
|
f91cfe5d0d | ||
|
|
1ab9cc5f4a | ||
|
|
02ff681700 | ||
|
|
2d9c3be0e6 | ||
|
|
78cf602347 | ||
|
|
898c7b4387 | ||
|
|
7412d518d7 | ||
|
|
e6030ac562 | ||
|
|
d6d958bc92 | ||
|
|
fd7a24c5fc | ||
|
|
41b3c6fbfb | ||
|
|
84b2ab6e42 | ||
|
|
6cf184ba23 | ||
|
|
6ad8211f56 | ||
|
|
ac247867d9 | ||
|
|
46f0bf2830 | ||
|
|
f0d712d1ce | ||
|
|
b72bca661b | ||
|
|
ac5cd832f6 | ||
|
|
71e8f00e80 | ||
|
|
b371f0fa7d | ||
|
|
b11a206b4a | ||
|
|
c678375712 | ||
|
|
b1bdf766b1 | ||
|
|
531bdab250 | ||
|
|
5fa04c7384 | ||
|
|
1f95d0559c |
@@ -643,6 +643,51 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RobinKing",
|
||||
"name": "Robin King",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1583193?v=4",
|
||||
"profile": "http://robincn.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dheepakg",
|
||||
"name": "Dheepak ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4730170?v=4",
|
||||
"profile": "http://twitter.com/deegovee",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "daniel-vera-g",
|
||||
"name": "Daniel VG",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28257108?v=4",
|
||||
"profile": "https://github.com/daniel-vera-g",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Barabazs",
|
||||
"name": "Barabas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31799121?v=4",
|
||||
"profile": "https://github.com/Barabazs",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EngincanV",
|
||||
"name": "Engincan VESKE",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43685404?v=4",
|
||||
"profile": "http://enginveske@gmail.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -16,16 +16,16 @@
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
"**/node_modules/**/*",
|
||||
"packages/**/*"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": [
|
||||
"fail",
|
||||
"unknown",
|
||||
"pass"
|
||||
],
|
||||
"gitdoc.enabled": false
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"gitdoc.enabled": false,
|
||||
"jest.autoEnable": false,
|
||||
"jest.runAllTestsFirst": false,
|
||||
"search.mode": "reuseEditor"
|
||||
}
|
||||
|
||||
17
LICENSE
17
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT Licence (MIT)
|
||||
|
||||
Copyright 2020 Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -17,4 +17,17 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Where noted, some code uses the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
Well, that shouldn't have happened!
|
||||
|
||||
If you got here via a link from another document, please file an [issue](https://github.com/foambubble/foam/issues) on our GitHub repo including:
|
||||
|
||||
- the page you came from
|
||||
- the link you followed
|
||||
|
||||
Thanks!
|
||||
|
||||
-The Foam Team
|
||||
-The Foam Team
|
||||
|
||||
@@ -35,4 +35,3 @@ gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
|
||||
# kramdown v2 ships without the gfm parser by default. If you're using
|
||||
# kramdown v1, comment out this line.
|
||||
gem "kramdown-parser-gfm"
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ GEM
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.4)
|
||||
rexml (3.2.5)
|
||||
rouge (3.23.0)
|
||||
ruby-enum (0.8.0)
|
||||
i18n
|
||||
|
||||
33
docs/LICENSE.txt
Normal file
33
docs/LICENSE.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
The MIT Licence (MIT)
|
||||
|
||||
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Where noted, some code uses the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
@@ -12,7 +12,6 @@
|
||||
- What use cases are we working towards?
|
||||
-[[todo]] User round table
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -118,7 +118,7 @@ the community.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
@@ -126,7 +126,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
tags: todo, good-first-task
|
||||
---
|
||||
# Contribution Guide
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
|
||||
|
||||
## Getting Up To Speed
|
||||
|
||||
Before you start contributing we recommend that you read the following links:
|
||||
|
||||
- [[principles]] - This document describes the guiding principles behind Foam.
|
||||
- [[code-of-conduct]] - Rules we hope every contributor aims to follow, allowing everyone to participate in our community!
|
||||
|
||||
## Diving In
|
||||
|
||||
We understand that diving in an unfamiliar codebase may seem scary,
|
||||
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!
|
||||
@@ -41,6 +45,7 @@ You should now be ready to start working!
|
||||
|
||||
Code needs to come with tests.
|
||||
We use the following convention in Foam:
|
||||
|
||||
- *.test.ts are unit tests
|
||||
- *.spec.ts are integration tests
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ tags: architecture
|
||||
This document aims to provide a quick overview of the Foam architecture!
|
||||
|
||||
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
|
||||
|
||||
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VSCode plugin.
|
||||
|
||||
Exceptions to the monorepo are:
|
||||
|
||||
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
|
||||
- All other [[recommended-extensions]] live in their respective GitHub repos.
|
||||
- [foam-cli](https://github.com/foambubble/foam-cli) - The Foam CLI tool.
|
||||
|
||||
@@ -10,5 +10,3 @@ But there's also a bunch of roadmap items that are hard to implement this way, a
|
||||
Overall, we should strive to build big things from small things. Focused, interoperable modules are better, because they allow users to pick and mix which features work for them. A good example of why this matters is the Markdown All In One extension we rely on: While it provides many of the things we need, a few of its features are incompatible with how I would like to work, and therefore it becomes a limiter of how well I can improve my own workflow.
|
||||
|
||||
However, there becomes a point where we may benefit from implementing a centralised solution, e.g. a syntax, an extension or perhaps a VSCode language server. As much as possible, we should allow users to operate in a decentralised manner.
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ The potential solution:
|
||||
- This would be a GitHub action (or a local script, ran via foam-cli) that outputs publish-friendly markdown format for static site generators and other publishing tools
|
||||
- This build step should be pluggable, so that other transformations could be ran during it
|
||||
- Have publish targets defined in settings, that support both turning the link reference definitions on/off and defining their format (.md or not). Example draft (including also edit-time aspect):
|
||||
|
||||
```typescript
|
||||
// settings json
|
||||
// see enumerations below for explanations on values
|
||||
@@ -120,6 +121,7 @@ The potential solution:
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- With Foam repo, just use edit-time link reference definitions with '.md' extension - this makes the links work in the Github UI
|
||||
- Have publish target defined for Github pages, that doesn't use '.md' extension, but still has the link reference definitions. Generate the output into gh-pages branch (or separate repo) with automation.
|
||||
- This naturally requires first removing the existing link reference definitions during the build
|
||||
|
||||
@@ -11,8 +11,7 @@ The idea would be to automatically generate lists of backlinks (and optionally,
|
||||
- Make every link two-way navigable in published sites
|
||||
- Make Foam notes more portable to different apps and long-term storage
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
- Write out a new `[[wiki-link]]` and `Cmd` + `Click` to create a new file and enter it.
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `New Note` from [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) and enter a **Title Case Name** to create `title-case-name.md`
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create New Note` and enter a **Title Case Name** to create `Title Case Name.md`
|
||||
- Add a keyboard binding to make creating new notes easier.
|
||||
- The [[note-templates]] used by this command can be customized.
|
||||
- You shouldn't worry too much about categorizing your notes. You can always [[search-for-notes]], and explore them using the [[graph-visualisation]].
|
||||
|
||||
[//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"
|
||||
[note-templates]: note-templates "Note Templates"
|
||||
[search-for-notes]: ../recipes/search-for-notes "Search for Notes"
|
||||
[graph-visualisation]: graph-visualisation "Graph Visualisation"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -28,11 +28,11 @@ To enable the feature:
|
||||
|
||||
```
|
||||
{
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# Foam logging in VsCode
|
||||
|
||||
## Find the Foam log
|
||||
|
||||
The Foam log can be found in the `Output` tab.
|
||||
|
||||
1. To show the tab, click on `View > Output`.
|
||||
2. In the dropdown on the right of the tab, select `Foam`.
|
||||
|
||||

|
||||
|
||||
## Change the default logging level
|
||||
|
||||
1. Open workspace settings (`cmd+,`, or execute the `Preferences: Open Workspace Settings` command)
|
||||
2. Look for the entry `Foam > Logging: Level`
|
||||
|
||||
Set to debug when reporting an issue
|
||||
|
||||
## Change the log level for the session
|
||||
|
||||
Execute the command `Foam: Set log level`.
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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
|
||||
- allow you to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
|
||||
@@ -32,6 +33,7 @@ A sample configuration object is provided below:
|
||||
```
|
||||
|
||||
### 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:
|
||||
@@ -46,6 +48,7 @@ type: feature
|
||||
```
|
||||
|
||||
And the following `settings.json`:
|
||||
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"node": {
|
||||
@@ -57,6 +60,3 @@ And the following `settings.json`:
|
||||
Will result in the following graph:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@ When you use `[[wiki-links]]`, the [foam-vscode](https://github.com/foambubble/f
|
||||
## Example
|
||||
|
||||
The following example:
|
||||
|
||||
```md
|
||||
- [[wiki-links]]
|
||||
- [[github-pages]]
|
||||
```
|
||||
|
||||
...generates the following link reference definitions to the bottom of the file:
|
||||
|
||||
```md
|
||||
[wiki-links]: wiki-links "Wiki Links"
|
||||
[github-pages]: github-pages "Github Pages"
|
||||
```
|
||||
|
||||
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
|
||||
@@ -67,7 +71,6 @@ After changing the setting in your workspace, you can run the [[workspace-janito
|
||||
|
||||
See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[workspace-janitor]: workspace-janitor.md "Janitor"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
# Note Templates
|
||||
|
||||
Foam supports note templates.
|
||||
Foam supports note templates. Templates are a way to customize the starting content for your notes (instead of always starting from an empty note).
|
||||
|
||||
Note templates live in `.foam/templates`. Run the `Foam: Create New Template` command from the command palette or create a regular `.md` file there to add a template.
|
||||
Note templates are files located in the special `.foam/templates` directory.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Create a template:
|
||||
|
||||
* Run the `Foam: Create New Template` command from the command palette
|
||||
* OR manually create a regular `.md` file in the `.foam/templates` directory
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
To create a note from a template, execute the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new template if none exist.
|
||||
To create a note from a template:
|
||||
|
||||
* Run the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new template if none exist.
|
||||
* OR run the `Foam: Create New Note` command, which uses the special default template (`.foam/templates/new-note.md`, if it exists)
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
## Default template
|
||||
|
||||
The `.foam/templates/new-note.md` template is special in that it is the template that will be used by the `Foam: Create New Note` command.
|
||||
Customize this template to contain content that you want included every time you create a note.
|
||||
|
||||
## Variables
|
||||
|
||||
Templates can use all the variables available in [VS Code Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
|
||||
|
||||
In addition, you can also use variables provided by Foam:
|
||||
|
||||
| Name | Description |
|
||||
| ------------ | ----------------------------------------------------------------------------------- |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
|
||||
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables.
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
Foam supports tags.
|
||||
|
||||
## Creating a tag
|
||||
|
||||
There are two ways of creating a tag:
|
||||
|
||||
- adding a `#tag` anywhere in the text of the note
|
||||
- using the `tags: tag1, tag2` property in frontmatter
|
||||
|
||||
@@ -12,6 +14,9 @@ There are two ways of creating a tag:
|
||||
It's possible to navigate tags via the Tag Explorer panel.
|
||||
In the future it will be possible to explore tags via the graph as well.
|
||||
|
||||
## Styling tags
|
||||
Inline tags can be styled using custom CSS with the selector `.foam-tag`.
|
||||
|
||||
## An alternative to tags
|
||||
|
||||
Given the power of backlinks, some people prefer to use them also as tags.
|
||||
|
||||
@@ -5,10 +5,12 @@ To store your personal knowledge graph in markdown files instead of a database,
|
||||
**Foam Janitor** (inspired by Andy Matuschak's [note-link-janitor](https://github.com/andymatuschak/note-link-janitor)) helps you migrate existing notes to Foam, and maintain your Foam's health over time.
|
||||
|
||||
Currently, Foam's Janitor helps you to:
|
||||
|
||||
- Ensure your [[link-reference-definitions]] are up to date
|
||||
- Ensure every document has a well-formatted title (required for Markdown Links, Markdown Notes, and Foam Gatsby Template compatibility)
|
||||
|
||||
In the future, Janitor can help you with
|
||||
|
||||
- Updating [[materialized-backlinks]]
|
||||
- Lint, format and structure notes
|
||||
- Rename and move notes around while keeping their references up to date.
|
||||
|
||||
@@ -5,7 +5,7 @@ Uncategorised thoughts, to be added
|
||||
- Release notes
|
||||
- Markdown Preview
|
||||
- It's possible to customise the markdown preview styling. **Maybe make it use local foam workspace styles for live preview of the site??**
|
||||
- See: https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles
|
||||
- See: <https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles>
|
||||
- Use VS Code [CodeTour](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for onboarding
|
||||
- Investigate other similar extensions:
|
||||
- [Unotes](https://marketplace.visualstudio.com/items?itemName=ryanmcalister.Unotes)
|
||||
@@ -16,7 +16,7 @@ Uncategorised thoughts, to be added
|
||||
- Every Foam could have a different theme even in the editor, so you'll see it like they see it
|
||||
- UI and layout design of your workspace can become a thing
|
||||
- VS Code Notebooks API
|
||||
- https://code.visualstudio.com/api/extension-guides/notebook
|
||||
- <https://code.visualstudio.com/api/extension-guides/notebook>
|
||||
- Future architecture
|
||||
- Could we do publish-related settings as a pre-push git hook, e.g. generating footnote labels
|
||||
- Running them on Github Actions to edit stuff as it comes in
|
||||
@@ -32,5 +32,3 @@ Uncategorised thoughts, to be added
|
||||
- Maps have persistent topologies. As the graph grows, you should be able to visualise where an idea belongs. Maybe a literal map? And island? A DeckGL visualisation?
|
||||
|
||||
Testing: This file is served from the /docs directory.
|
||||
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ These instructions assume you have a GitHub account, and you have Visual Studio
|
||||
|
||||
2. [Clone the repository locally](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) and open it in VS Code.
|
||||
|
||||
*Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces).*
|
||||
*Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces).*
|
||||
|
||||
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one)
|
||||
|
||||
@@ -194,6 +194,13 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -208,7 +215,7 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
|
||||
## License
|
||||
|
||||
Foam is licensed under the [MIT license](license).
|
||||
Foam is licensed under the [MIT license](LICENSE.txt).
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[graph-visualisation]: features/graph-visualisation.md "Graph Visualisation"
|
||||
|
||||
@@ -13,6 +13,7 @@ Present: @jevakallio, @riccardoferretti
|
||||
|
||||
- Land work to master
|
||||
- Create a foam-core package
|
||||
|
||||
-
|
||||
|
||||
### Open questions
|
||||
|
||||
@@ -48,7 +48,6 @@ While Foam uses tools popular among computer programmers, Foam should be inclusi
|
||||
- **Foam is not just for programmers.** If you're a programmer, feel free to write scripts and extensions to support your own workflow, and publish them for others to use, but the out of the box Foam experience should not require you to know how to do so. You should, however, be curious and open to adopting new tools that are unfamiliar to you, and evaluate whether they could work for you.
|
||||
- **Foam is for everyone** As a foam user, you support everyone's quest for knowledge and self-improvement, not only your own, or folks' who look like you. All participants in Foam repositories, discussion forums, physical and virtual meeting spaces etc are expected to respect each other as described in our [[code-of-conduct]]. **Foam is not for toxic tech bros.**
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: recipes/recipes.md "Recipes"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
|
||||
@@ -42,6 +42,7 @@ The current capabilities of templates is limited in some important ways. This do
|
||||
Creating of new notes in Foam is too cumbersome and slow. Despite their power, Foam templates can currently only be used in very limited scenarios.
|
||||
|
||||
This proposal aims to address these issues by streamlining note creation and by allowing templates to be used everywhere.
|
||||
|
||||
## Limitations of current templating
|
||||
|
||||
### Too much friction to create a new note
|
||||
@@ -162,14 +163,16 @@ This would open use the template found at `.foam/templates/new-note.md` to creat
|
||||
#### Case 1: `.foam/templates/new-note.md` doesn't exist
|
||||
|
||||
If `.foam/templates/new-note.md` doesn't exist, it behaves the same as `Markdown Notes: New Note`:
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
**Note:** this would use an implicit default template, making use of the `${title}` variable.
|
||||
|
||||
#### Case 2: `.foam/templates/new-note.md` exists
|
||||
|
||||
If `.foam/templates/new-note.md` exists:
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
**Progress:** At this point, we have a faster way to create new notes from templates.
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
You can use [foam-gatsby-template](https://github.com/mathieudutour/foam-gatsby-template) to generate a static site to host it online on Github or [Vercel](https://vercel.com).
|
||||
|
||||
### Publishing your foam to GitHub pages
|
||||
|
||||
It comes configured with Github actions to auto deploy to Github pages when changes are pushed to your main branch.
|
||||
|
||||
### Publishing your foam to Vercel
|
||||
|
||||
When you're ready to publish, run a local build.
|
||||
|
||||
```bash
|
||||
cd _layouts
|
||||
npm run build
|
||||
|
||||
@@ -11,14 +11,15 @@ The following recipe is written with the assumption that you already have an [Az
|
||||
1. Generate a Foam workspace using the [foam-template project](https://github.com/foambubble/foam-template).
|
||||
2. Change the remote to a git repository in Azure DevOps (Repos -> Import a Repository -> Add Clone URL with Authentication), or copy all the files into a new Azure DevOps git repository.
|
||||
3. Define which document will be the wiki home page. To do that, create a file called `.order` in the Foam workspace root folder, with first line being the document filename without `.md` extension. For a project created from the Foam template, the file would look like this:
|
||||
|
||||
```
|
||||
readme
|
||||
```
|
||||
|
||||
4. Push the repository to remote in Azure DevOps.
|
||||
|
||||
## Publish repository to a wiki
|
||||
|
||||
|
||||
1. Navigate to your Azure DevOps project in a web browser.
|
||||
2. Choose **Overview** > **Wiki**. If you don't have wikis for your project, choose **Publish code as a wiki** on welcome page.
|
||||
3. Choose repository with your Foam workspace, branch (usually `master` or `main`), folder (for workspace created from foam-template it is `/`), and wiki name, and press **Publish**.
|
||||
@@ -40,6 +41,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
|
||||
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
|
||||
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
|
||||
|
||||
```bash
|
||||
[core]
|
||||
...
|
||||
@@ -58,6 +60,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
url = git@github.com:username/repo.git
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
```
|
||||
|
||||
6. You can then push to both repositories by: `git push origin master` or a single one using: `git push github master` or `git push azure master`
|
||||
|
||||
For more information, read the [Azure DevOps documentation](https://docs.microsoft.com/en-us/azure/devops/project/wiki/publish-repo-to-wiki).
|
||||
|
||||
@@ -9,36 +9,41 @@
|
||||
## How to publish locally
|
||||
|
||||
If you want to test your published foam, follow the instructions:
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll
|
||||
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll>
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll>
|
||||
|
||||
Assuming you have installed ruby/jekyll and the rest:
|
||||
|
||||
- `touch Gemfile`
|
||||
- open the file and paste the following:
|
||||
|
||||
```
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem "github-pages", "VERSION"
|
||||
```
|
||||
replacing `VERSION` with the latest from https://rubygems.org/gems/github-pages (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
replacing `VERSION` with the latest from <https://rubygems.org/gems/github-pages> (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
- `bundle`
|
||||
- `bundle exec jekyll 3.9.0 new .`
|
||||
- edit the `Gemfile` according to the instructions at [Creating Your Site](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll#creating-your-site) Point n.8
|
||||
- `bundle exec jekyll serve`
|
||||
|
||||
|
||||
## Other templates
|
||||
|
||||
There are many other templates which also support publish your foam workspace to github pages
|
||||
|
||||
* gatsby-digital-garden
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* foam-mkdocs-template
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ The standard [foam-template](https://github.com/foambubble/foam-template) is rea
|
||||
## Enable navigation in GitHub
|
||||
|
||||
To allow navigation from within the GitHub repo, make sure to generate the link references, by setting
|
||||
|
||||
- `Foam › Edit: Link Reference Definitions` -> `withExtensions`
|
||||
|
||||
See [[link-reference-definitions]] for more information.
|
||||
|
||||
## Customising the style
|
||||
|
||||
You can edit `assets/css/style.scss` to change how published pages look.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -11,6 +11,7 @@ Generate a solution using the [Foam template].
|
||||
Change the remote to GitLab, or copy all the files into a new GitLab repo.
|
||||
|
||||
### Add a _config.yaml
|
||||
|
||||
Add another file to the root directory (the one with `readme.md` in it) called `_config.yaml` (no extension)
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
You can use [foam-eleventy-template](https://github.com/juanfrank77/foam-eleventy-template) to generate a static site with [Eleventy](https://www.11ty.dev/), and host it online on [Netlify](https://www.netlify.com/).
|
||||
|
||||
With this template you can
|
||||
|
||||
- Have control over what to publish and what to keep private
|
||||
- Customize the styling of the site to your own liking
|
||||
|
||||
@@ -12,8 +13,6 @@ When you're ready to publish, import the GitHub repository you created with **fo
|
||||
|
||||
Once that's done, all you have to do is make changes to your workspace in VS Code and push them to the main branch on GitHub. Netlify will recognize the changes, deploy them automatically and give you a link where your Foam is published.
|
||||
|
||||
|
||||
That's it!
|
||||
|
||||
You can now see it online and use that link to share it with your friends, so that they can see it too.
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
- [VSCode Extensions Packs](https://code.visualstudio.com/blogs/2017/03/07/extension-pack-roundup) [[todo]] Evaluate for deployment
|
||||
- [Dark mode](https://css-tricks.com/dark-modes-with-css/)
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -5,7 +5,7 @@ You can also easily manipulate the git history to reduce clutter.
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -18,9 +18,6 @@ __For Foam specific needs, you can add a comment here by following the [[contrib
|
||||
- Feedback and issues with the extension should be reported to the authors themselves
|
||||
- Feedback and issues with the integration of the extension in Foam can be reported in our [issue tracker](https://github.com/foambubble/foam/issues)
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[contribution-guide]: ../contribution-guide.md "Contribution Guide"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,7 @@ With this #recipe you can convert a link to a fully-formed Markdown link, using
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
|
||||
Markdown Link Expander will scrape your URL's `<title>` tag to create a nice Markdown-style link.
|
||||
|
||||
@@ -22,4 +22,3 @@ Tip: If you paste a lot of links, give the action a custom [key binding](https:/
|
||||
## Feedback and issues
|
||||
|
||||
Have an idea for the extension? [Feel free to share! 🎉](https://github.com/Skn0tt/markdown-link-expander/issues)
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ With this #recipe you can create notes on your iOS device, which will automatica
|
||||
|
||||
## Instructions
|
||||
|
||||
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
|
||||
```
|
||||
name: Manually triggered workflow
|
||||
@@ -26,15 +25,17 @@ on:
|
||||
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:
|
||||
with:
|
||||
{% raw %}
|
||||
capture: ${{ github.event.inputs.data }}
|
||||
{% endraw %}
|
||||
- run: |
|
||||
git config --local user.email "example@gmail.com"
|
||||
git config --local user.name "Your name"
|
||||
@@ -44,14 +45,16 @@ jobs:
|
||||
|
||||
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)
|
||||
- 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.)
|
||||
|
||||
@@ -7,7 +7,6 @@ We have two alternative #recipe for displaying diagrams in markdown:
|
||||
- [Draw.io](#drawio)
|
||||
- [Using Draw.io](#using-drawio)
|
||||
|
||||
|
||||
## Mermaid
|
||||
|
||||
You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) plugin to draw and preview diagrams in your content.
|
||||
@@ -27,7 +26,6 @@ You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierne
|
||||
3. Start drawing your diagram. Once you done, save it.
|
||||
4. Embed the diagram file as you embedding the image file, for example: ``
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This is an example of how to structure a Recipe. The first paragraph or two should explain the purpose of the recipe succinctly, including why it's useful, if that's not obvious.
|
||||
|
||||
Recipes are intended to document:
|
||||
|
||||
- How to use Foam's basic features
|
||||
- Power user pro-tips
|
||||
- Useful customisations of the default Foam environment
|
||||
@@ -10,8 +11,8 @@ Recipes are intended to document:
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
|
||||
The first section should be a bulleted list of extensions required to use this recipe. At a minimum, this section should list all additional, non-standard extensions.
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@ In the future we'll want to improve this feature by
|
||||
- Make back links editable using [VS Code Search Editors](https://code.visualstudio.com/updates/v1_43#_search-editors)
|
||||
- [Suggested by @Jash on Discord](https://discordapp.com/channels/729975036148056075/729978910363746315/730999992419876956)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[materialized-backlinks]: ../dev/materialized-backlinks.md "Materialized Backlinks (stub)"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
If you're interested in working on it, please start a conversation in [GitHub issues](https://github.com/foambubble/foam/issues).
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
[roadmap]: ../dev/roadmap.md "Roadmap"
|
||||
|
||||
@@ -56,6 +56,3 @@ in `keybindings.json` (Code|File > Preferences > Keyboard Shortcuts) add binding
|
||||
If you have any issues or questions please look at the [README.md](https://github.com/kneely/note-macros#note-macros) on the [note-macros](https://github.com/kneely/note-macros) GitHub.
|
||||
|
||||
If you run into any issues that are not fixed by referring to the README or feature requests please open an [issue](https://github.com/kneely/note-macros/issues).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Real-time Collaboration
|
||||
|
||||
This #recipe is here to just tell you that VS Code Live Share will allow you to collaborate live on your notes.
|
||||
|
||||
|
||||
@@ -26,14 +26,17 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Clip webpages with [[web-clipper]]
|
||||
|
||||
## Discover
|
||||
|
||||
- Explore your notes using [[graph-visualisation]]
|
||||
- Discover relationships with [[backlinking]]
|
||||
- Simulating [[unlinked-references]]
|
||||
|
||||
## Organise
|
||||
|
||||
- Using [[backlinking]] for reference lists.
|
||||
|
||||
## Write
|
||||
|
||||
- Link documents with [[wiki-links]].
|
||||
- Use shortcuts for [[creating-new-notes]]
|
||||
- Instantly create and access your [[daily-notes]]
|
||||
|
||||
@@ -9,6 +9,7 @@ The most promising options are:
|
||||
### [GitJournal](https://gitjournal.io/)
|
||||
|
||||
Pros
|
||||
|
||||
- Open source
|
||||
- Already a usable solution.
|
||||
- Provides functionality to edit, create, and browser markdown files.
|
||||
@@ -19,6 +20,7 @@ Pros
|
||||
- Developer is happy to prioritize Foam compatibility
|
||||
|
||||
Cons
|
||||
|
||||
- Doesn't generate link reference lists (but this is ok, since [[workspace-janitor]] as a GitHub action can solve this)
|
||||
- Not as sleek as Apple/Google notes, some keyboard state glitching on Android, etc.
|
||||
- Lack of control over roadmap. Established product with a paid plan, so may not be open to Foam-supportive changes and additions that don't benefit most users.
|
||||
@@ -28,13 +30,15 @@ Verdict: Good. By far best effort/outcome ratio would be to help improve GitJour
|
||||
### GitHub Codespaces
|
||||
|
||||
Pros
|
||||
|
||||
- Works out of the box just like the desktop app
|
||||
|
||||
Cons
|
||||
|
||||
- not generally available quite yet
|
||||
- [Pricing](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/about-billing-for-codespaces)
|
||||
|
||||
For a quick demo, see https://www.youtube.com/watch?v=KI5m4Uy8_4I.
|
||||
For a quick demo, see <https://www.youtube.com/watch?v=KI5m4Uy8_4I>.
|
||||
|
||||
Verdict: Good. Pricing should be reasonable for taking notes on the fly. Harder to assess for people who would constantly use Foam from mobile phone.
|
||||
|
||||
@@ -54,7 +58,6 @@ If such an app was worth building, it would have to have the following features:
|
||||
|
||||
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"
|
||||
[build-vs-assemble]: ../dev/build-vs-assemble.md "Build vs Assemble"
|
||||
[workspace-janitor]: ../features/workspace-janitor.md "Janitor"
|
||||
|
||||
@@ -9,7 +9,6 @@ This list is subject to change. Especially the Git ones.
|
||||
- [Markdown Links](https://marketplace.visualstudio.com/items?itemName=tchayen.markdown-links)
|
||||
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
|
||||
|
||||
|
||||
## Extensions For Additional Features
|
||||
|
||||
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
|
||||
|
||||
@@ -17,4 +17,3 @@ Also happens to sound quite a lot like Home. Funny, that.
|
||||
## Bubble
|
||||
|
||||
Individual Foam note, written in Markdown.
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.12.1"
|
||||
"version": "0.13.3"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.12.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,56 +1,39 @@
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam, IDataStore } from './index';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { isSome } from './utils';
|
||||
import { Logger } from './utils/log';
|
||||
import { isMarkdownFile } from './utils/uri';
|
||||
import { FoamConfig, Foam, IDataStore, FoamGraph } from './index';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { Matcher } from './services/datastore';
|
||||
import { ResourceProvider } from 'model/provider';
|
||||
|
||||
export const bootstrap = async (config: FoamConfig, dataStore: IDataStore) => {
|
||||
const plugins = await loadPlugins(config);
|
||||
|
||||
const parserPlugins = plugins.map(p => p.parser).filter(isSome);
|
||||
const parser = createMarkdownParser(parserPlugins);
|
||||
|
||||
const workspace = new FoamWorkspace();
|
||||
const files = await dataStore.listFiles();
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + uri);
|
||||
if (isMarkdownFile(uri)) {
|
||||
const content = await dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(parser.parse(uri, content));
|
||||
}
|
||||
}
|
||||
})
|
||||
export const bootstrap = async (
|
||||
config: FoamConfig,
|
||||
dataStore: IDataStore,
|
||||
initialProviders: ResourceProvider[]
|
||||
) => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
workspace.resolveLinks(true);
|
||||
const workspace = new FoamWorkspace();
|
||||
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
|
||||
|
||||
const listeners = [
|
||||
dataStore.onDidChange(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidCreate(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true);
|
||||
|
||||
return {
|
||||
workspace: workspace,
|
||||
config: config,
|
||||
const foam: Foam = {
|
||||
workspace,
|
||||
graph,
|
||||
config,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
matcher,
|
||||
},
|
||||
dispose: () => {
|
||||
listeners.forEach(l => l.dispose());
|
||||
workspace.dispose();
|
||||
graph.dispose();
|
||||
},
|
||||
} as Foam;
|
||||
};
|
||||
|
||||
return foam;
|
||||
};
|
||||
|
||||
@@ -74,7 +74,8 @@ export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
|
||||
// Web environment
|
||||
if (typeof navigator === 'object' && !isElectronRenderer) {
|
||||
_userAgent = navigator.userAgent;
|
||||
_isWindows = _userAgent.indexOf('Windows') >= 0;
|
||||
_isWindows =
|
||||
_userAgent.indexOf('Windows') >= 0 || _userAgent.indexOf('win32') >= 0;
|
||||
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
|
||||
_isIOS =
|
||||
(_userAgent.indexOf('Macintosh') >= 0 ||
|
||||
|
||||
@@ -1,748 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 compatible 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,7 +1,7 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './common/uri';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export interface FoamConfig {
|
||||
workspaceFolders: URI[];
|
||||
@@ -68,8 +68,8 @@ export const createConfigFromFolders = (
|
||||
|
||||
const parseConfig = (path: URI) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path.fsPath, 'utf8'));
|
||||
return JSON.parse(readFileSync(URI.toFsPath(path), 'utf8'));
|
||||
} catch {
|
||||
Logger.debug('Could not read configuration from ' + path);
|
||||
Logger.debug('Could not read configuration from ' + URI.toString(path));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,42 +1,37 @@
|
||||
import {
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
NoteLink,
|
||||
isNote,
|
||||
ResourceLink,
|
||||
NoteLinkDefinition,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
NoteParser,
|
||||
ResourceParser,
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import { URI } from './common/uri';
|
||||
import { FoamConfig } from './config';
|
||||
import { IDataStore, FileDataStore } from './services/datastore';
|
||||
import {
|
||||
IDataStore,
|
||||
FileDataStore,
|
||||
Matcher,
|
||||
IMatcher,
|
||||
} from './services/datastore';
|
||||
import { ILogger } from './utils/log';
|
||||
import { IDisposable, isDisposable } from './common/lifecycle';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import * as uris from './utils/uri';
|
||||
import * as positions from './model/position';
|
||||
import * as ranges from './model/range';
|
||||
import { FoamGraph } from '../src/model/graph';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export { uris, positions, ranges };
|
||||
export { IDataStore, FileDataStore };
|
||||
export { Position } from './model/position';
|
||||
export { Range } from './model/range';
|
||||
export { IDataStore, FileDataStore, Matcher, IMatcher };
|
||||
export { ILogger };
|
||||
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
|
||||
export { Event, Emitter } from './common/event';
|
||||
export { FoamConfig };
|
||||
export { isSameUri, parseUri } from './utils/uri';
|
||||
|
||||
export { ResourceProvider } from './model/provider';
|
||||
export { IDisposable, isDisposable };
|
||||
|
||||
export {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
createMarkdownParser,
|
||||
MarkdownResourceProvider,
|
||||
} from './markdown-provider';
|
||||
|
||||
export {
|
||||
@@ -56,29 +51,23 @@ export { bootstrap } from './bootstrap';
|
||||
|
||||
export {
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
Position,
|
||||
Range,
|
||||
NoteLink,
|
||||
ResourceLink,
|
||||
URI,
|
||||
FoamWorkspace,
|
||||
FoamGraph,
|
||||
NoteLinkDefinition,
|
||||
NoteParser,
|
||||
isNote,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
ResourceParser,
|
||||
};
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: NoteParser;
|
||||
parser: ResourceParser;
|
||||
matcher: IMatcher;
|
||||
}
|
||||
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
graph: FoamGraph;
|
||||
config: FoamConfig;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Note } from '../model/note';
|
||||
import { Range, createFromPosition } from '../model/range';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../markdown-provider';
|
||||
import { getHeadingFromFileName, uriToSlug } from '../utils';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
@@ -19,7 +20,7 @@ export interface TextEdit {
|
||||
}
|
||||
|
||||
export const generateLinkReferences = (
|
||||
note: Note,
|
||||
note: Resource,
|
||||
workspace: FoamWorkspace,
|
||||
includeExtensions: boolean
|
||||
): TextEdit | null => {
|
||||
@@ -53,7 +54,7 @@ export const generateLinkReferences = (
|
||||
: `${note.source.eol}${note.source.eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: createFromPosition(note.source.end, note.source.end),
|
||||
range: Range.createFromPosition(note.source.end, note.source.end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
@@ -69,12 +70,12 @@ export const generateLinkReferences = (
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${newReferences}`,
|
||||
range: createFromPosition(first.range!.start, last.range!.end),
|
||||
range: Range.createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const generateHeading = (note: Note): TextEdit | null => {
|
||||
export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
@@ -108,7 +109,7 @@ export const generateHeading = (note: Note): TextEdit | null => {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
uriToSlug(note.uri)
|
||||
)}${paddingEnd}`,
|
||||
range: createFromPosition(
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
note.source.contentStart
|
||||
),
|
||||
|
||||
@@ -10,13 +10,13 @@ import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import {
|
||||
NoteLinkDefinition,
|
||||
Note,
|
||||
NoteParser,
|
||||
isWikilink,
|
||||
getTitle,
|
||||
Resource,
|
||||
ResourceLink,
|
||||
WikiLink,
|
||||
ResourceParser,
|
||||
} from './model/note';
|
||||
import { Position, create as createPos } from './model/position';
|
||||
import { Range, create as createRange } from './model/range';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
extractHashtags,
|
||||
@@ -24,11 +24,121 @@ import {
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { computeRelativePath, getBasename, parseUri } from './utils/uri';
|
||||
import { ParserPlugin } from './plugins';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './common/uri';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { ResourceProvider } from 'model/provider';
|
||||
import { IDataStore, FileDataStore, IMatcher } from './services/datastore';
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly watcherInit?: (triggers: {
|
||||
onDidChange: (uri: URI) => void;
|
||||
onDidCreate: (uri: URI) => void;
|
||||
onDidDelete: (uri: URI) => void;
|
||||
}) => IDisposable[],
|
||||
private readonly parser: ResourceParser = createMarkdownParser([]),
|
||||
private readonly dataStore: IDataStore = new FileDataStore()
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob => this.dataStore.list(glob))
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + URI.toString(uri));
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables =
|
||||
this.watcherInit?.({
|
||||
onDidChange: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidCreate: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidDelete: uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
},
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return URI.isMarkdownFile(uri);
|
||||
}
|
||||
|
||||
read(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
const content = await this.read(uri);
|
||||
return isSome(content) ? this.parser.parse(uri, content) : null;
|
||||
}
|
||||
|
||||
resolveLink(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
link: ResourceLink
|
||||
) {
|
||||
let targetUri: URI | undefined;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = resource.definitions.find(
|
||||
def => def.label === link.slug
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, resource.uri);
|
||||
targetUri =
|
||||
workspace.find(definedUri, resource.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
targetUri =
|
||||
workspace.find(link.slug, resource.uri)?.uri ??
|
||||
URI.placeholder(link.slug);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(URI.resolve(link.target, resource.uri).path);
|
||||
break;
|
||||
}
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all the children of the given node, extracts
|
||||
@@ -60,7 +170,7 @@ const tagsPlugin: ParserPlugin = {
|
||||
const titlePlugin: ParserPlugin = {
|
||||
name: 'title',
|
||||
visit: (node, note) => {
|
||||
if (note.title == null && node.type === 'heading' && node.depth === 1) {
|
||||
if (note.title === '' && node.type === 'heading' && node.depth === 1) {
|
||||
note.title =
|
||||
((node as Parent)!.children?.[0]?.value as string) || note.title;
|
||||
}
|
||||
@@ -70,8 +180,8 @@ const titlePlugin: ParserPlugin = {
|
||||
note.title = props.title ?? note.title;
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title == null) {
|
||||
note.title = getBasename(note.uri);
|
||||
if (note.title === '') {
|
||||
note.title = URI.getBasename(note.uri);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -89,7 +199,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = parseUri(note.uri, targetUri);
|
||||
const uri = URI.resolve(targetUri, note.uri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +244,9 @@ const handleError = (
|
||||
);
|
||||
};
|
||||
|
||||
export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
export function createMarkdownParser(
|
||||
extraPlugins: ParserPlugin[]
|
||||
): ResourceParser {
|
||||
const parser = unified()
|
||||
.use(markdownParse, { gfm: true })
|
||||
.use(frontmatterPlugin, ['yaml'])
|
||||
@@ -156,8 +268,8 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
}
|
||||
});
|
||||
|
||||
const foamParser: NoteParser = {
|
||||
parse: (uri: URI, markdown: string): Note => {
|
||||
const foamParser: ResourceParser = {
|
||||
parse: (uri: URI, markdown: string): Resource => {
|
||||
Logger.debug('Parsing:', uri);
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
try {
|
||||
@@ -170,11 +282,11 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
const tree = parser.parse(markdown);
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
var note: Note = {
|
||||
var note: Resource = {
|
||||
uri: uri,
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: null,
|
||||
title: '',
|
||||
tags: new Set(),
|
||||
links: [],
|
||||
definitions: [],
|
||||
@@ -204,7 +316,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = note.properties.title ?? note.title;
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = createPos(
|
||||
note.source.contentStart = Position.create(
|
||||
node.position!.end.line! + 2,
|
||||
0
|
||||
);
|
||||
@@ -306,13 +418,13 @@ export function createMarkdownReferences(
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = computeRelativePath(noteUri, target.uri);
|
||||
const relativePath = URI.relativePath(noteUri, target.uri);
|
||||
const pathToNote = includeExtension
|
||||
? relativePath
|
||||
: dropExtension(relativePath);
|
||||
|
||||
// [wiki-link-text]: path/to/file.md "Page title"
|
||||
return { label: link.slug, url: pathToNote, title: getTitle(target) };
|
||||
return { label: link.slug, url: pathToNote, title: target.title };
|
||||
})
|
||||
.filter(isSome)
|
||||
.sort();
|
||||
@@ -324,7 +436,7 @@ export function createMarkdownReferences(
|
||||
* @returns Foam Position (0-indexed)
|
||||
*/
|
||||
const astPointToFoamPosition = (point: Point): Position => {
|
||||
return createPos(point.line - 1, point.column - 1);
|
||||
return Position.create(point.line - 1, point.column - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -333,9 +445,13 @@ const astPointToFoamPosition = (point: Point): Position => {
|
||||
* @returns Foam Range (0-indexed)
|
||||
*/
|
||||
const astPositionToFoamRange = (pos: AstPosition): Range =>
|
||||
createRange(
|
||||
Range.create(
|
||||
pos.start.line - 1,
|
||||
pos.start.column - 1,
|
||||
pos.end.line - 1,
|
||||
pos.end.column - 1
|
||||
);
|
||||
|
||||
const isWikilink = (link: ResourceLink): link is WikiLink => {
|
||||
return link.type === 'wikilink';
|
||||
};
|
||||
|
||||
232
packages/foam-core/src/model/graph.ts
Normal file
232
packages/foam-core/src/model/graph.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { IDisposable } from '../index';
|
||||
import { FoamWorkspace, uriToResourceName } from './workspace';
|
||||
import { Range } from './range';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
link: ResourceLink;
|
||||
};
|
||||
|
||||
const pathToPlaceholderId = (value: string) => value;
|
||||
const uriToPlaceholderId = (uri: URI) => pathToPlaceholderId(uri.path);
|
||||
|
||||
export class FoamGraph implements IDisposable {
|
||||
/**
|
||||
* Placehoders by key / slug / value
|
||||
*/
|
||||
public readonly placeholders: { [key: string]: URI } = {};
|
||||
/**
|
||||
* Maps the connections starting from a URI
|
||||
*/
|
||||
public readonly links: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* Maps the connections arriving to a URI
|
||||
*/
|
||||
public readonly backlinks: { [key: string]: Connection[] } = {};
|
||||
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(private readonly workspace: FoamWorkspace) {}
|
||||
|
||||
public contains(uri: URI): boolean {
|
||||
return this.getConnections(uri).length > 0;
|
||||
}
|
||||
|
||||
public getAllNodes(): URI[] {
|
||||
return [
|
||||
...Object.values(this.placeholders),
|
||||
...this.workspace.list().map(r => r.uri),
|
||||
];
|
||||
}
|
||||
|
||||
public getAllConnections(): Connection[] {
|
||||
return Object.values(this.links).flat();
|
||||
}
|
||||
|
||||
public getConnections(uri: URI): Connection[] {
|
||||
return [
|
||||
...(this.links[uri.path] || []),
|
||||
...(this.backlinks[uri.path] || []),
|
||||
];
|
||||
}
|
||||
|
||||
public getLinks(uri: URI): Connection[] {
|
||||
return this.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public getBacklinks(uri: URI): Connection[] {
|
||||
return this.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all the links in the workspace, connecting notes and
|
||||
* creating placeholders.
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @returns the FoamGraph
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
): FoamGraph {
|
||||
let graph = new FoamGraph(workspace);
|
||||
|
||||
Object.values(workspace.list()).forEach(resource =>
|
||||
graph.resolveResource(resource)
|
||||
);
|
||||
if (keepMonitoring) {
|
||||
graph.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
graph.updateLinksRelatedToAddedResource(resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
graph.updateLinksForResource(change.old, change.new);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
graph.updateLinksRelatedToDeletedResource(resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
if (name in this.placeholders) {
|
||||
const placeholder = this.placeholders[name];
|
||||
delete this.placeholders[name];
|
||||
const resourcesToUpdate = this.backlinks[placeholder.path] ?? [];
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res.source))
|
||||
);
|
||||
}
|
||||
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
}
|
||||
|
||||
private updateLinksForResource(oldResource: Resource, newResource: Resource) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
patch.removed.forEach(link => {
|
||||
const target = this.workspace.resolveLink(oldResource, link);
|
||||
return this.disconnect(oldResource.uri, target, link);
|
||||
}, this);
|
||||
patch.added.forEach(link => {
|
||||
const target = this.workspace.resolveLink(newResource, link);
|
||||
return this.connect(newResource.uri, target, link);
|
||||
}, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToDeletedResource(resource: Resource) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = this.links[uri.path] ?? [];
|
||||
delete this.links[uri.path];
|
||||
resourcesPointedByDeletedNote.forEach(connection =>
|
||||
this.disconnect(uri, connection.target, connection.link)
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = this.backlinks[uri.path] ?? [];
|
||||
delete this.backlinks[uri.path];
|
||||
notesPointingToDeletedResource.forEach(link =>
|
||||
this.resolveResource(this.workspace.get(link.source))
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
private connect(source: URI, target: URI, link: ResourceLink) {
|
||||
const connection = { source, target, link };
|
||||
|
||||
this.links[source.path] = this.links[source.path] ?? [];
|
||||
this.links[source.path].push(connection);
|
||||
this.backlinks[target.path] = this.backlinks[target.path] ?? [];
|
||||
this.backlinks[target.path].push(connection);
|
||||
|
||||
if (URI.isPlaceholder(target)) {
|
||||
this.placeholders[uriToPlaceholderId(target)] = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private disconnect(source: URI, target: URI, link: ResourceLink | true) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links[source.path] =
|
||||
this.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.links[source.path].length === 0) {
|
||||
delete this.links[source.path];
|
||||
}
|
||||
this.backlinks[target.path] =
|
||||
this.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.backlinks[target.path].length === 0) {
|
||||
delete this.backlinks[target.path];
|
||||
if (URI.isPlaceholder(target)) {
|
||||
delete this.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public resolveResource(resource: Resource) {
|
||||
delete this.links[resource.uri.path];
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
URI.isEqual(a.source, b.source) &&
|
||||
URI.isEqual(a.target, b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
a.type === b.type && Range.isEqual(a.range, b.range);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { URI } from '../common/uri';
|
||||
import { getBasename } from '../utils/uri';
|
||||
import { URI } from './uri';
|
||||
import { Position } from './position';
|
||||
import { Range } from './range';
|
||||
|
||||
@@ -24,7 +23,7 @@ export interface DirectLink {
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export type NoteLink = WikiLink | DirectLink;
|
||||
export type ResourceLink = WikiLink | DirectLink;
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
label: string;
|
||||
@@ -33,53 +32,40 @@ export interface NoteLinkDefinition {
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface BaseResource {
|
||||
export interface Resource {
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export interface Attachment extends BaseResource {
|
||||
type: 'attachment';
|
||||
}
|
||||
|
||||
export interface Placeholder extends BaseResource {
|
||||
type: 'placeholder';
|
||||
}
|
||||
|
||||
export interface Note extends BaseResource {
|
||||
type: 'note';
|
||||
title: string | null;
|
||||
type: string;
|
||||
title: string;
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
tags: Set<string>;
|
||||
links: NoteLink[];
|
||||
links: ResourceLink[];
|
||||
|
||||
// TODO to remove
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export type Resource = Note | Attachment | Placeholder;
|
||||
|
||||
export interface NoteParser {
|
||||
parse: (uri: URI, text: string) => Note;
|
||||
export interface ResourceParser {
|
||||
parse: (uri: URI, text: string) => Resource;
|
||||
}
|
||||
|
||||
export const isWikilink = (link: NoteLink): link is WikiLink => {
|
||||
return link.type === 'wikilink';
|
||||
};
|
||||
export abstract class Resource {
|
||||
public static sortByTitle(a: Resource, b: Resource) {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
export const getTitle = (resource: Resource): string => {
|
||||
return resource.type === 'note'
|
||||
? resource.title ?? getBasename(resource.uri)
|
||||
: getBasename(resource.uri);
|
||||
};
|
||||
|
||||
export const isNote = (resource: Resource): resource is Note => {
|
||||
return resource.type === 'note';
|
||||
};
|
||||
|
||||
export const isPlaceholder = (resource: Resource): resource is Placeholder => {
|
||||
return resource.type === 'placeholder';
|
||||
};
|
||||
|
||||
export const isAttachment = (resource: Resource): resource is Attachment => {
|
||||
return resource.type === 'attachment';
|
||||
};
|
||||
public static isResource(thing: any): thing is Resource {
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
URI.isUri((thing as Resource).uri) &&
|
||||
typeof (thing as Resource).title === 'string' &&
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
typeof (thing as Resource).tags === 'object' &&
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,91 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
export interface Position {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
export const create = (line: number, character: number): Position => ({
|
||||
line,
|
||||
character,
|
||||
});
|
||||
|
||||
export const Min = (...positions: Position[]): Position => {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
export abstract class Position {
|
||||
static create(line: number, character: number): Position {
|
||||
return { line, character };
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (isBefore(p, result!)) {
|
||||
result = p;
|
||||
|
||||
static Min(...positions: Position[]): Position {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const Max = (...positions: Position[]): Position => {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (isAfter(p, result!)) {
|
||||
result = p;
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (Position.isBefore(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const isBefore = (p1: Position, p2: Position): boolean => {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
static Max(...positions: Position[]): Position {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (Position.isAfter(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
|
||||
static isBefore(p1: Position, p2: Position): boolean {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character < p2.character;
|
||||
}
|
||||
return p1.character < p2.character;
|
||||
};
|
||||
|
||||
export const isBeforeOrEqual = (p1: Position, p2: Position): boolean => {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
static isBeforeOrEqual(p1: Position, p2: Position): boolean {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character <= p2.character;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
|
||||
static isAfter(p1: Position, p2: Position): boolean {
|
||||
return !Position.isBeforeOrEqual(p1, p2);
|
||||
}
|
||||
return p1.character <= p2.character;
|
||||
};
|
||||
|
||||
export const isAfter = (p1: Position, p2: Position): boolean => {
|
||||
return !isBeforeOrEqual(p1, p2);
|
||||
};
|
||||
static isAfterOrEqual(p1: Position, p2: Position): boolean {
|
||||
return !Position.isBefore(p1, p2);
|
||||
}
|
||||
|
||||
export const isAfterOrEqual = (p1: Position, p2: Position): boolean => {
|
||||
return !isBefore(p1, p2);
|
||||
};
|
||||
static isEqual(p1: Position, p2: Position): boolean {
|
||||
return p1.line === p2.line && p1.character === p2.character;
|
||||
}
|
||||
|
||||
export const isEqual = (p1: Position, p2: Position): boolean => {
|
||||
return p1.line === p2.line && p1.character === p2.character;
|
||||
};
|
||||
|
||||
export const compareTo = (p1: Position, p2: Position): number => {
|
||||
if (p1.line < p2.line) {
|
||||
return -1;
|
||||
} else if (p1.line > p2.line) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line
|
||||
if (p1.character < p2.character) {
|
||||
static compareTo(p1: Position, p2: Position): number {
|
||||
if (p1.line < p2.line) {
|
||||
return -1;
|
||||
} else if (p1.character > p2.character) {
|
||||
} else if (p1.line > p2.line) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line and character
|
||||
return 0;
|
||||
// equal line
|
||||
if (p1.character < p2.character) {
|
||||
return -1;
|
||||
} else if (p1.character > p2.character) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line and character
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
17
packages/foam-core/src/model/provider.ts
Normal file
17
packages/foam-core/src/model/provider.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
import { ResourceLink, URI } from 'index';
|
||||
import { Resource } from './note';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
|
||||
export interface ResourceProvider extends IDisposable {
|
||||
init: (workspace: FoamWorkspace) => Promise<void>;
|
||||
supports: (uri: URI) => boolean;
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
readAsMarkdown: (uri: URI) => Promise<string | null>;
|
||||
fetch: (uri: URI) => Promise<Resource | null>;
|
||||
resolveLink: (
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
link: ResourceLink
|
||||
) => URI;
|
||||
}
|
||||
@@ -1,55 +1,72 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
import { Position } from './position';
|
||||
import * as pos from './position';
|
||||
|
||||
export interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
export const create = (
|
||||
startLine: number,
|
||||
startChar: number,
|
||||
endLine?: number,
|
||||
endChar?: number
|
||||
): Range => {
|
||||
const start: Position = {
|
||||
line: startLine,
|
||||
character: startChar,
|
||||
};
|
||||
const end: Position = {
|
||||
line: endLine ?? startLine,
|
||||
character: endChar ?? startChar,
|
||||
};
|
||||
return createFromPosition(start, end);
|
||||
};
|
||||
|
||||
export const createFromPosition = (start: Position, end?: Position) => {
|
||||
end = end ?? start;
|
||||
let first = start;
|
||||
let second = end;
|
||||
if (pos.isAfter(start, end)) {
|
||||
first = end;
|
||||
second = start;
|
||||
export abstract class Range {
|
||||
static create(
|
||||
startLine: number,
|
||||
startChar: number,
|
||||
endLine?: number,
|
||||
endChar?: number
|
||||
): Range {
|
||||
const start: Position = {
|
||||
line: startLine,
|
||||
character: startChar,
|
||||
};
|
||||
const end: Position = {
|
||||
line: endLine ?? startLine,
|
||||
character: endChar ?? startChar,
|
||||
};
|
||||
return Range.createFromPosition(start, end);
|
||||
}
|
||||
return {
|
||||
start: {
|
||||
line: first.line,
|
||||
character: first.character,
|
||||
},
|
||||
end: {
|
||||
line: second.line,
|
||||
character: second.character,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const containsRange = (range: Range, contained: Range): boolean =>
|
||||
containsPosition(range, contained.start) &&
|
||||
containsPosition(range, contained.end);
|
||||
static createFromPosition(start: Position, end?: Position) {
|
||||
end = end ?? start;
|
||||
let first = start;
|
||||
let second = end;
|
||||
if (Position.isAfter(start, end)) {
|
||||
first = end;
|
||||
second = start;
|
||||
}
|
||||
return {
|
||||
start: {
|
||||
line: first.line,
|
||||
character: first.character,
|
||||
},
|
||||
end: {
|
||||
line: second.line,
|
||||
character: second.character,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const containsPosition = (range: Range, position: Position): boolean =>
|
||||
pos.isAfterOrEqual(position, range.start) &&
|
||||
pos.isBeforeOrEqual(position, range.end);
|
||||
static containsRange(range: Range, contained: Range): boolean {
|
||||
return (
|
||||
Range.containsPosition(range, contained.start) &&
|
||||
Range.containsPosition(range, contained.end)
|
||||
);
|
||||
}
|
||||
|
||||
export const isEqual = (r1: Range, r2: Range): boolean =>
|
||||
pos.isEqual(r1.start, r2.start) && pos.isEqual(r1.end, r2.end);
|
||||
static containsPosition(range: Range, position: Position): boolean {
|
||||
return (
|
||||
Position.isAfterOrEqual(position, range.start) &&
|
||||
Position.isBeforeOrEqual(position, range.end)
|
||||
);
|
||||
}
|
||||
|
||||
static isEqual(r1: Range, r2: Range): boolean {
|
||||
return (
|
||||
Position.isEqual(r1.start, r2.start) && Position.isEqual(r1.end, r2.end)
|
||||
);
|
||||
}
|
||||
|
||||
static isBefore(a: Range, b: Range): number {
|
||||
return a.start.line - b.start.line || a.start.character - b.start.character;
|
||||
}
|
||||
}
|
||||
|
||||
461
packages/foam-core/src/model/uri.ts
Normal file
461
packages/foam-core/src/model/uri.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { isWindows } from '../common/platform';
|
||||
|
||||
/**
|
||||
* 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 interface URI {
|
||||
scheme: string;
|
||||
authority: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
const { posix } = paths;
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
export abstract class URI {
|
||||
static create(from: Partial<URI>): URI {
|
||||
return {
|
||||
scheme: from.scheme ?? _empty,
|
||||
authority: from.authority ?? _empty,
|
||||
path: from.path ?? _empty,
|
||||
query: from.query ?? _empty,
|
||||
fragment: from.fragment ?? _empty,
|
||||
};
|
||||
}
|
||||
|
||||
static parse(value: string): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return URI.create({});
|
||||
}
|
||||
return URI.create({
|
||||
scheme: match[2] || 'file',
|
||||
authority: percentDecode(match[4] ?? _empty),
|
||||
path: percentDecode(match[5] ?? _empty),
|
||||
query: percentDecode(match[7] ?? _empty),
|
||||
fragment: percentDecode(match[9] ?? _empty),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
static resolve(value: string, reference: URI): URI {
|
||||
let uri = URI.parse(value);
|
||||
if (uri.scheme === 'file' && !value.startsWith('/')) {
|
||||
const [path, fragment] = value.split('#');
|
||||
uri =
|
||||
path.length > 0 ? URI.computeRelativeURI(reference, path) : reference;
|
||||
if (fragment) {
|
||||
uri = URI.create({
|
||||
...uri,
|
||||
fragment: fragment,
|
||||
});
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
static computeRelativeURI(reference: URI, relativeSlug: string): URI {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${posix.extname(reference.path)}`;
|
||||
return URI.create({
|
||||
...reference,
|
||||
path: posix.join(posix.dirname(reference.path), slug),
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
if (path.startsWith(_slash)) {
|
||||
path = `${path.replace(/\\/g, _slash)}`;
|
||||
} else {
|
||||
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 URI.create({ scheme: 'file', authority, path });
|
||||
}
|
||||
|
||||
static placeholder(key: string): URI {
|
||||
return URI.create({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
}
|
||||
|
||||
static relativePath(source: URI, target: URI): string {
|
||||
const relativePath = posix.relative(
|
||||
posix.dirname(source.path),
|
||||
target.path
|
||||
);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
static getBasename(uri: URI) {
|
||||
return posix.parse(uri.path).name;
|
||||
}
|
||||
|
||||
static getDir(uri: URI) {
|
||||
return URI.file(posix.dirname(uri.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
static createResourceUriFromPlaceholder(
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(URI.toFsPath(uri), ...pathFragment))
|
||||
.path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return URI.create({ ...uri, path: newPath });
|
||||
}
|
||||
|
||||
static toFsPath(uri: URI, keepDriveLetterCasing = true): 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;
|
||||
}
|
||||
|
||||
static toString(uri: URI): string {
|
||||
return encode(uri, false);
|
||||
}
|
||||
|
||||
// --- utility
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
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'
|
||||
);
|
||||
}
|
||||
|
||||
static isPlaceholder(uri: URI): boolean {
|
||||
return uri.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
static isEqual(a: URI, b: URI): boolean {
|
||||
return (
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path &&
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query
|
||||
);
|
||||
}
|
||||
static isMarkdownFile(uri: URI): boolean {
|
||||
return uri.path.endsWith('.md');
|
||||
}
|
||||
}
|
||||
|
||||
// --- encode / 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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the external version of a uri
|
||||
*/
|
||||
function encode(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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -1,26 +1,10 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { URI } from '../common/uri';
|
||||
import { Resource, NoteLink, Note } from './note';
|
||||
import * as ranges from './range';
|
||||
import {
|
||||
computeRelativeURI,
|
||||
isSome,
|
||||
isNone,
|
||||
parseUri,
|
||||
placeholderUri,
|
||||
isPlaceholder,
|
||||
isSameUri,
|
||||
} from '../utils';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { isSome, isNone } from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { IDisposable } from '../index';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
link: NoteLink;
|
||||
};
|
||||
import { ResourceProvider } from './provider';
|
||||
|
||||
export function getReferenceType(
|
||||
reference: URI | string
|
||||
@@ -43,10 +27,7 @@ const pathToResourceId = (pathValue: string) => {
|
||||
const uriToResourceId = (uri: URI) => pathToResourceId(uri.path);
|
||||
|
||||
const pathToResourceName = (pathValue: string) => path.parse(pathValue).name;
|
||||
const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
const pathToPlaceholderId = (value: string) => value;
|
||||
const uriToPlaceholderId = (uri: URI) => pathToPlaceholderId(uri.path);
|
||||
export const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -56,6 +37,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
onDidUpdate = this.onDidUpdateEmitter.event;
|
||||
onDidDelete = this.onDidDeleteEmitter.event;
|
||||
|
||||
private providers: ResourceProvider[] = [];
|
||||
|
||||
/**
|
||||
* Resources by key / slug
|
||||
*/
|
||||
@@ -64,207 +47,53 @@ export class FoamWorkspace implements IDisposable {
|
||||
* Resources by URI
|
||||
*/
|
||||
private resources: { [key: string]: Resource } = {};
|
||||
/**
|
||||
* Placehoders by key / slug / value
|
||||
*/
|
||||
private placeholders: { [key: string]: Resource } = {};
|
||||
|
||||
/**
|
||||
* Maps the connections starting from a URI
|
||||
*/
|
||||
private links: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* Maps the connections arriving to a URI
|
||||
*/
|
||||
private backlinks: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
disposables: IDisposable[] = [];
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
return provider.init(this);
|
||||
}
|
||||
|
||||
exists(uri: URI) {
|
||||
return FoamWorkspace.exists(this, uri);
|
||||
}
|
||||
list() {
|
||||
return FoamWorkspace.list(this);
|
||||
}
|
||||
get(uri: URI) {
|
||||
return FoamWorkspace.get(this, uri);
|
||||
}
|
||||
find(uri: URI | string) {
|
||||
return FoamWorkspace.find(this, uri);
|
||||
}
|
||||
set(resource: Resource) {
|
||||
return FoamWorkspace.set(this, resource);
|
||||
}
|
||||
delete(uri: URI) {
|
||||
return FoamWorkspace.delete(this, uri);
|
||||
}
|
||||
|
||||
resolveLink(note: Note, link: NoteLink) {
|
||||
return FoamWorkspace.resolveLink(this, note, link);
|
||||
}
|
||||
resolveLinks(keepMonitoring: boolean = false) {
|
||||
return FoamWorkspace.resolveLinks(this, keepMonitoring);
|
||||
}
|
||||
getAllConnections() {
|
||||
return FoamWorkspace.getAllConnections(this);
|
||||
}
|
||||
getConnections(uri: URI) {
|
||||
return FoamWorkspace.getConnections(this, uri);
|
||||
}
|
||||
getLinks(uri: URI) {
|
||||
return FoamWorkspace.getLinks(this, uri);
|
||||
}
|
||||
getBacklinks(uri: URI) {
|
||||
return FoamWorkspace.getBacklinks(this, uri);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.onDidAddEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
public static resolveLink(
|
||||
workspace: FoamWorkspace,
|
||||
note: Note,
|
||||
link: NoteLink
|
||||
): URI {
|
||||
let targetUri: URI | undefined;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = note.definitions.find(
|
||||
def => def.label === link.slug
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = parseUri(note.uri, definitionUri);
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, definedUri, note.uri)?.uri ??
|
||||
placeholderUri(definedUri.path);
|
||||
} else {
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.slug, note.uri)?.uri ??
|
||||
placeholderUri(link.slug);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.target, note.uri)?.uri ??
|
||||
placeholderUri(parseUri(note.uri, link.target).path);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isPlaceholder(targetUri)) {
|
||||
// we can only add placeholders when links are being resolved
|
||||
workspace = FoamWorkspace.set(workspace, {
|
||||
type: 'placeholder',
|
||||
uri: targetUri,
|
||||
});
|
||||
}
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all the links in the workspace, connecting notes and
|
||||
* creating placeholders.
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @returns the resolved workspace
|
||||
*/
|
||||
public static resolveLinks(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
): FoamWorkspace {
|
||||
workspace.links = {};
|
||||
workspace.backlinks = {};
|
||||
workspace.placeholders = {};
|
||||
|
||||
workspace = Object.values(workspace.list()).reduce(
|
||||
(w, resource) => FoamWorkspace.resolveResource(w, resource),
|
||||
workspace
|
||||
);
|
||||
if (keepMonitoring) {
|
||||
workspace.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
FoamWorkspace.updateLinksRelatedToAddedResource(workspace, resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
FoamWorkspace.updateLinksForResource(
|
||||
workspace,
|
||||
change.old,
|
||||
change.new
|
||||
);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
FoamWorkspace.updateLinksRelatedToDeletedResource(
|
||||
workspace,
|
||||
resource
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public static getAllConnections(workspace: FoamWorkspace): Connection[] {
|
||||
return Object.values(workspace.links).flat();
|
||||
}
|
||||
|
||||
public static getConnections(
|
||||
workspace: FoamWorkspace,
|
||||
uri: URI
|
||||
): Connection[] {
|
||||
return [
|
||||
...(workspace.links[uri.path] || []),
|
||||
...(workspace.backlinks[uri.path] || []),
|
||||
];
|
||||
}
|
||||
|
||||
public static getLinks(workspace: FoamWorkspace, uri: URI): Connection[] {
|
||||
return workspace.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public static getBacklinks(workspace: FoamWorkspace, uri: URI): Connection[] {
|
||||
return workspace.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public static set(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
): FoamWorkspace {
|
||||
if (resource.type === 'placeholder') {
|
||||
workspace.placeholders[uriToPlaceholderId(resource.uri)] = resource;
|
||||
return workspace;
|
||||
}
|
||||
const id = uriToResourceId(resource.uri);
|
||||
const old = FoamWorkspace.find(workspace, resource.uri);
|
||||
const old = this.find(resource.uri);
|
||||
const name = uriToResourceName(resource.uri);
|
||||
workspace.resources[id] = resource;
|
||||
workspace.resourcesByName[name] = workspace.resourcesByName[name] ?? [];
|
||||
workspace.resourcesByName[name].push(id);
|
||||
this.resources[id] = resource;
|
||||
this.resourcesByName[name] = this.resourcesByName[name] ?? [];
|
||||
this.resourcesByName[name].push(id);
|
||||
isSome(old)
|
||||
? workspace.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: workspace.onDidAddEmitter.fire(resource);
|
||||
return workspace;
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static exists(workspace: FoamWorkspace, uri: URI): boolean {
|
||||
return isSome(workspace.resources[uriToResourceId(uri)]);
|
||||
delete(uri: URI) {
|
||||
const id = uriToResourceId(uri);
|
||||
const deleted = this.resources[id];
|
||||
delete this.resources[id];
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
this.resourcesByName[name] =
|
||||
this.resourcesByName[name]?.filter(resId => resId !== id) ?? [];
|
||||
if (this.resourcesByName[name].length === 0) {
|
||||
delete this.resourcesByName[name];
|
||||
}
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
public static list(workspace: FoamWorkspace): Resource[] {
|
||||
return [
|
||||
...Object.values(workspace.resources),
|
||||
...Object.values(workspace.placeholders),
|
||||
];
|
||||
public exists(uri: URI): boolean {
|
||||
return (
|
||||
!URI.isPlaceholder(uri) && isSome(this.resources[uriToResourceId(uri)])
|
||||
);
|
||||
}
|
||||
|
||||
public static get(workspace: FoamWorkspace, uri: URI): Resource {
|
||||
const note = FoamWorkspace.find(workspace, uri);
|
||||
public list(): Resource[] {
|
||||
return Object.values(this.resources);
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
const note = this.find(uri);
|
||||
if (isSome(note)) {
|
||||
return note;
|
||||
} else {
|
||||
@@ -272,223 +101,64 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public static find(
|
||||
workspace: FoamWorkspace,
|
||||
resourceId: URI | string,
|
||||
reference?: URI
|
||||
): Resource | null {
|
||||
public find(resourceId: URI | string, reference?: URI): Resource | null {
|
||||
const refType = getReferenceType(resourceId);
|
||||
switch (refType) {
|
||||
case 'uri':
|
||||
const uri = resourceId as URI;
|
||||
if (uri.scheme === 'placeholder') {
|
||||
return uri.path in workspace.placeholders
|
||||
? { type: 'placeholder', uri: uri }
|
||||
: null;
|
||||
} else {
|
||||
return FoamWorkspace.exists(workspace, uri)
|
||||
? workspace.resources[uriToResourceId(uri)]
|
||||
: null;
|
||||
}
|
||||
return this.exists(uri) ? this.resources[uriToResourceId(uri)] : null;
|
||||
|
||||
case 'key':
|
||||
const name = pathToResourceName(resourceId as string);
|
||||
const paths = workspace.resourcesByName[name];
|
||||
const paths = this.resourcesByName[name];
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
const placeholderId = pathToPlaceholderId(resourceId as string);
|
||||
return workspace.placeholders[placeholderId] ?? null;
|
||||
return null;
|
||||
}
|
||||
// prettier-ignore
|
||||
const sortedPaths = paths.length === 1
|
||||
? paths
|
||||
: paths.sort((a, b) => a.localeCompare(b));
|
||||
return workspace.resources[sortedPaths[0]];
|
||||
return this.resources[sortedPaths[0]];
|
||||
|
||||
case 'absolute-path':
|
||||
const resourceUri = URI.file(resourceId as string);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(resourceUri)] ??
|
||||
workspace.placeholders[uriToPlaceholderId(resourceUri)]
|
||||
);
|
||||
return this.resources[uriToResourceId(resourceUri)] ?? null;
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
return null;
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = computeRelativeURI(reference, relativePath);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(targetUri)] ??
|
||||
workspace.placeholders[pathToPlaceholderId(resourceId as string)]
|
||||
);
|
||||
const targetUri = URI.computeRelativeURI(reference, relativePath);
|
||||
return this.resources[uriToResourceId(targetUri)] ?? null;
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected reference type: ' + refType);
|
||||
}
|
||||
}
|
||||
|
||||
public static delete(workspace: FoamWorkspace, uri: URI): Resource | null {
|
||||
const id = uriToResourceId(uri);
|
||||
const deleted = workspace.resources[id];
|
||||
delete workspace.resources[id];
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
workspace.resourcesByName[name] =
|
||||
workspace.resourcesByName[name]?.filter(resId => resId !== id) ?? [];
|
||||
if (workspace.resourcesByName[name].length === 0) {
|
||||
delete workspace.resourcesByName[name];
|
||||
}
|
||||
|
||||
isSome(deleted) && workspace.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
public static resolveResource(workspace: FoamWorkspace, resource: Resource) {
|
||||
if (resource.type === 'note') {
|
||||
delete workspace.links[resource.uri.path];
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = FoamWorkspace.resolveLink(workspace, resource, link);
|
||||
workspace = FoamWorkspace.connect(workspace, resource.uri, targetUri, link);
|
||||
});
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static updateLinksForResource(
|
||||
workspace: FoamWorkspace,
|
||||
oldResource: Resource,
|
||||
newResource: Resource
|
||||
) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
workspace = patch.removed.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(oldResource, link);
|
||||
return FoamWorkspace.disconnect(ws, oldResource.uri, target, link);
|
||||
}, workspace);
|
||||
workspace = patch.added.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(newResource, link);
|
||||
return FoamWorkspace.connect(ws, newResource.uri, target, link);
|
||||
}, workspace);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static updateLinksRelatedToAddedResource(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
if (name in workspace.placeholders) {
|
||||
const placeholder = workspace.placeholders[name];
|
||||
delete workspace.placeholders[name];
|
||||
const resourcesToUpdate = workspace.backlinks[placeholder.uri.path] ?? [];
|
||||
workspace = resourcesToUpdate.reduce(
|
||||
(ws, res) => FoamWorkspace.resolveResource(ws, ws.get(res.source)),
|
||||
workspace
|
||||
);
|
||||
}
|
||||
|
||||
// resolve the resource
|
||||
workspace = FoamWorkspace.resolveResource(workspace, resource);
|
||||
}
|
||||
|
||||
private static updateLinksRelatedToDeletedResource(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = workspace.links[uri.path] ?? [];
|
||||
delete workspace.links[uri.path];
|
||||
workspace = resourcesPointedByDeletedNote.reduce(
|
||||
(ws, connection) =>
|
||||
FoamWorkspace.disconnect(ws, uri, connection.target, connection.link),
|
||||
workspace
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
// TODO add tests
|
||||
const provider = this.providers.find(p => p.supports(resource.uri));
|
||||
return (
|
||||
provider?.resolveLink(this, resource, link) ??
|
||||
URI.placeholder(link.target)
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = workspace.backlinks[uri.path] ?? [];
|
||||
delete workspace.backlinks[uri.path];
|
||||
workspace = notesPointingToDeletedResource.reduce(
|
||||
(ws, link) => FoamWorkspace.resolveResource(ws, ws.get(link.source)),
|
||||
workspace
|
||||
);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static connect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI,
|
||||
link: NoteLink
|
||||
) {
|
||||
const connection = { source, target, link };
|
||||
|
||||
workspace.links[source.path] = workspace.links[source.path] ?? [];
|
||||
workspace.links[source.path].push(connection);
|
||||
workspace.backlinks[target.path] = workspace.backlinks[target.path] ?? [];
|
||||
workspace.backlinks[target.path].push(connection);
|
||||
|
||||
return workspace;
|
||||
public read(uri: URI): Promise<string | null> {
|
||||
const provider = this.providers.find(p => p.supports(uri));
|
||||
return provider?.read(uri) ?? Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private static disconnect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI,
|
||||
link: NoteLink | true
|
||||
) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!isSameUri(source, c.source) || !isSameUri(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
public readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
const provider = this.providers.find(p => p.supports(uri));
|
||||
return provider?.readAsMarkdown(uri) ?? Promise.resolve(null);
|
||||
}
|
||||
|
||||
workspace.links[source.path] =
|
||||
workspace.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.links[source.path].length === 0) {
|
||||
delete workspace.links[source.path];
|
||||
}
|
||||
workspace.backlinks[target.path] =
|
||||
workspace.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.backlinks[target.path].length === 0) {
|
||||
delete workspace.backlinks[target.path];
|
||||
if (isPlaceholder(target)) {
|
||||
delete workspace.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return workspace;
|
||||
public dispose(): void {
|
||||
this.onDidAddEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
isSameUri(a.source, b.source) &&
|
||||
isSameUri(a.target, b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: NoteLink, b: NoteLink) =>
|
||||
a.type === b.type && ranges.isEqual(a.range, b.range);
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Node } from 'unist';
|
||||
import { isNotNull } from '../utils';
|
||||
import { Note } from '../model/note';
|
||||
import { Resource } from '../model/note';
|
||||
import unified from 'unified';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../common/uri';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export interface FoamPlugin {
|
||||
name: string;
|
||||
@@ -16,12 +16,12 @@ export interface FoamPlugin {
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
visit?: (node: Node, note: Note) => void;
|
||||
visit?: (node: Node, note: Resource) => void;
|
||||
onDidInitializeParser?: (parser: unified.Processor) => void;
|
||||
onWillParseMarkdown?: (markdown: string) => string;
|
||||
onWillVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidFindProperties?: (properties: any, note: Note) => void;
|
||||
onWillVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidFindProperties?: (properties: any, note: Resource) => void;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
@@ -43,10 +43,10 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
|
||||
const plugins = await Promise.all(
|
||||
pluginDirs
|
||||
.filter(dir => fs.statSync(dir.fsPath).isDirectory)
|
||||
.filter(dir => fs.statSync(URI.toFsPath(dir)).isDirectory)
|
||||
.map(async dir => {
|
||||
try {
|
||||
const pluginFile = path.join(dir.fsPath, 'index.js');
|
||||
const pluginFile = path.join(URI.toFsPath(dir), 'index.js');
|
||||
fs.accessSync(pluginFile);
|
||||
Logger.info(`Found plugin at [${pluginFile}]. Loading..`);
|
||||
const plugin = validate(await import(pluginFile));
|
||||
@@ -66,11 +66,11 @@ function findPluginDirs(workspaceFolders: URI[]) {
|
||||
.reduce((acc, pluginDir) => {
|
||||
try {
|
||||
const content = fs
|
||||
.readdirSync(pluginDir.fsPath)
|
||||
.readdirSync(URI.toFsPath(pluginDir))
|
||||
.map(dir => URI.joinPath(pluginDir, dir));
|
||||
return [
|
||||
...acc,
|
||||
...content.filter(c => fs.statSync(c.fsPath).isDirectory()),
|
||||
...content.filter(c => fs.statSync(URI.toFsPath(c)).isDirectory()),
|
||||
];
|
||||
} catch {
|
||||
return acc;
|
||||
|
||||
@@ -1,139 +1,87 @@
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { Event, Emitter } from '../common/event';
|
||||
import { URI } from '../common/uri';
|
||||
import { FoamConfig } from '../config';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { isSome } from '../utils';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { isWindows } from '../common/platform';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
export interface IWatcher {
|
||||
export interface IMatcher {
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
* Filters the given list of URIs, keepin only the ones that
|
||||
* are matched by this Matcher
|
||||
*
|
||||
* @param files the URIs to check
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
match(files: URI[]): URI[];
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
* Returns whether this URI is matched by this Matcher
|
||||
*
|
||||
* @param uri the URI to check
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
isMatch(uri: URI): boolean;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
* The include globs
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
include: string[];
|
||||
|
||||
/**
|
||||
* The exclude lobs
|
||||
*/
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
* The matcher requires the path to be in unix format, so if we are in windows
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files available in the store
|
||||
*/
|
||||
listFiles: () => Promise<URI[]>;
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => URI.toFsPath(uri).replace(/\\/g, '/')
|
||||
: (uri: URI) => URI.toFsPath(uri);
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*/
|
||||
read: (uri: URI) => Promise<string>;
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
: (path: string): string => path;
|
||||
|
||||
/**
|
||||
* Returns whether the given URI is a match in
|
||||
* this data store
|
||||
*/
|
||||
isMatch: (uri: URI) => boolean;
|
||||
export class Matcher implements IMatcher {
|
||||
public readonly folders: string[];
|
||||
public readonly include: string[] = [];
|
||||
public readonly exclude: string[] = [];
|
||||
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
constructor(
|
||||
baseFolders: URI[],
|
||||
include: string[] = ['**/*'],
|
||||
exclude: string[] = []
|
||||
) {
|
||||
this.folders = baseFolders.map(toMatcherPathFormat);
|
||||
Logger.info('Workspace folders: ', this.folders);
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
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;
|
||||
|
||||
private _folders: readonly string[];
|
||||
private _includeGlobs: string[] = [];
|
||||
private _ignoreGlobs: string[] = [];
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(config: FoamConfig, watcher?: IWatcher) {
|
||||
this._folders = config.workspaceFolders.map(f =>
|
||||
f.fsPath.replace(/\\/g, '/')
|
||||
);
|
||||
Logger.info('Workspace folders: ', this._folders);
|
||||
|
||||
this._folders.forEach(folder => {
|
||||
this.folders.forEach(folder => {
|
||||
const withFolder = folderPlusGlob(folder);
|
||||
this._includeGlobs.push(
|
||||
...config.includeGlobs.map(glob => {
|
||||
if (glob.endsWith('*')) {
|
||||
glob = `${glob}\\.(md|mdx|markdown)`;
|
||||
}
|
||||
this.include.push(
|
||||
...include.map(glob => {
|
||||
return withFolder(glob);
|
||||
})
|
||||
);
|
||||
this._ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
|
||||
this.exclude.push(...exclude.map(withFolder));
|
||||
});
|
||||
Logger.info('Glob patterns', {
|
||||
includeGlobs: this._includeGlobs,
|
||||
ignoreGlobs: this._ignoreGlobs,
|
||||
includeGlobs: this.include,
|
||||
ignoreGlobs: this.exclude,
|
||||
});
|
||||
|
||||
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,
|
||||
files.map(f => URI.toFsPath(f)),
|
||||
this.include,
|
||||
{
|
||||
ignore: this._ignoreGlobs,
|
||||
ignore: this.exclude,
|
||||
nocase: true,
|
||||
format: toFsPath,
|
||||
}
|
||||
);
|
||||
return matches.map(URI.file);
|
||||
@@ -142,34 +90,53 @@ export class FileDataStore implements IDataStore, IDisposable {
|
||||
isMatch(uri: URI) {
|
||||
return this.match([uri]).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
const files = (
|
||||
await Promise.all(
|
||||
this._folders.map(async folder => {
|
||||
const res = await findAllFiles(folderPlusGlob(folder)('**/*'));
|
||||
return res.map(URI.file);
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
return this.match(files);
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files matching the given glob from the
|
||||
* store
|
||||
*/
|
||||
list: (glob: string) => Promise<URI[]>;
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*
|
||||
* Returns `null` in case of errors while reading
|
||||
*/
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
async list(glob: string): Promise<URI[]> {
|
||||
const res = await findAllFiles(glob);
|
||||
return res.map(URI.file);
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
return (await fs.promises.readFile(uri.fsPath)).toString();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
try {
|
||||
return (await fs.promises.readFile(URI.toFsPath(uri))).toString();
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const folderPlusGlob = (folder: string) => (glob: string): string => {
|
||||
export const folderPlusGlob = (folder: string) => (glob: string): string => {
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
if (glob.startsWith('/')) {
|
||||
glob = glob.slice(1);
|
||||
}
|
||||
return `${folder}/${glob}`;
|
||||
return folder.length > 0 ? `${folder}/${glob}` : glob;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(^|[ ])#([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
const WORD_REGEX = /(^|[ ])([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
const HASHTAG_REGEX = /(^|\s)#([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
const WORD_REGEX = /(^|\s)([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
|
||||
export const extractHashtags = (text: string): Set<string> => {
|
||||
return isSome(text)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { titleCase } from 'title-case';
|
||||
export { extractHashtags, extractTagsFromProp } from './hashtags';
|
||||
export * from './uri';
|
||||
export * from './core';
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
|
||||
5
packages/foam-core/src/utils/slug.ts
Normal file
5
packages/foam-core/src/utils/slug.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export const uriToSlug = (uri: URI): string =>
|
||||
GithubSlugger.slug(URI.getBasename(uri));
|
||||
@@ -1,102 +0,0 @@
|
||||
import { posix } from 'path';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { hash } from './core';
|
||||
import { URI } from '../common/uri';
|
||||
import { statSync } from 'fs';
|
||||
|
||||
export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(posix.parse(noteUri.path).name);
|
||||
};
|
||||
|
||||
export const nameToSlug = (noteName: string): string => {
|
||||
return GithubSlugger.slug(noteName);
|
||||
};
|
||||
|
||||
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 getDir = (uri: URI) => URI.file(posix.dirname(uri.path));
|
||||
|
||||
export const computeRelativeURI = (
|
||||
reference: URI,
|
||||
relativeSlug: string
|
||||
): URI => {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${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;
|
||||
};
|
||||
|
||||
export const placeholderUri = (key: string): URI => {
|
||||
return URI.from({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
export const placeholderToResourceUri = (
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI => {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
};
|
||||
|
||||
export const isPlaceholder = (uri: URI): boolean => {
|
||||
return uri.scheme === 'placeholder';
|
||||
};
|
||||
|
||||
export const isSameUri = (a: URI, b: URI) =>
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path && // Note we don't use fsPath for sameness
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query;
|
||||
|
||||
export const isMarkdownFile = (uri: URI): boolean => {
|
||||
return uri.path.endsWith('md') && statSync(uri.fsPath).isFile();
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
# Roam Document
|
||||
|
||||
[[Second Roam Document]]
|
||||
[[Second Roam Document]]
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Second Roam Document
|
||||
# Second Roam Document
|
||||
|
||||
@@ -9,4 +9,4 @@ All the link references are correct in this file.
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
[second-document]: second-document "Second Document"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createConfigFromFolders } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { URI } from '../src/model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import path from 'path';
|
||||
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
|
||||
import * as ranges from '../src/model/range';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { FoamWorkspace } from '../src';
|
||||
import { NoteLinkDefinition, Resource } from '../src/model/note';
|
||||
import { IDataStore, Matcher } from '../src/services/datastore';
|
||||
import { MarkdownResourceProvider } from '../src/markdown-provider';
|
||||
import { Range } from '../src/model/range';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { parseUri } from '../src/utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const position = ranges.create(0, 0, 0, 100);
|
||||
const position = Range.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
@@ -20,11 +22,22 @@ const eol = '\n';
|
||||
*/
|
||||
export const strToUri = URI.file;
|
||||
|
||||
export const createAttachment = (params: { uri: string }): Attachment => {
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
type: 'attachment',
|
||||
};
|
||||
export const noOpDataStore = (): IDataStore => ({
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
});
|
||||
|
||||
export const createTestWorkspace = () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*']);
|
||||
const provider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
undefined,
|
||||
undefined,
|
||||
noOpDataStore()
|
||||
);
|
||||
workspace.registerProvider(provider);
|
||||
return workspace;
|
||||
};
|
||||
|
||||
export const createTestNote = (params: {
|
||||
@@ -34,10 +47,10 @@ export const createTestNote = (params: {
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
text?: string;
|
||||
root?: URI;
|
||||
}): Note => {
|
||||
}): Resource => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: parseUri(root, params.uri),
|
||||
uri: URI.resolve(params.uri, root),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
@@ -45,7 +58,7 @@ export const createTestNote = (params: {
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map((link, index) => {
|
||||
const range = ranges.create(
|
||||
const range = Range.create(
|
||||
position.start.line + index,
|
||||
position.start.character,
|
||||
position.start.line + index,
|
||||
|
||||
@@ -1,72 +1,93 @@
|
||||
import { createConfigFromObject } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { FileDataStore } from '../src';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { FileDataStore, Matcher } from '../src';
|
||||
import { toMatcherPathFormat } from '../src/services/datastore';
|
||||
|
||||
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);
|
||||
describe('Matcher', () => {
|
||||
it('generates globs with the base dir provided', () => {
|
||||
const matcher = new Matcher([testFolder], ['*'], []);
|
||||
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
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('defaults to including everything and excluding nothing', () => {
|
||||
const matcher = new Matcher([testFolder]);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '**', '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
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']));
|
||||
it('supports multiple includes', () => {
|
||||
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g1')),
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a match method to filter strings', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.match(files)).toEqual([
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a isMatch method to see whether a file is matched or not', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(true);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
|
||||
it('happy path', () => {
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*'], ['**/*.pdf']);
|
||||
expect(matcher.isMatch(URI.file('/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/file.pdf'))).toBeFalsy();
|
||||
expect(matcher.isMatch(URI.file('/dir/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/dir/file.pdf'))).toBeFalsy();
|
||||
});
|
||||
|
||||
it('ignores files in the exclude list', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(false);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
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, '/')
|
||||
)
|
||||
);
|
||||
}
|
||||
describe('Datastore', () => {
|
||||
it('uses the matcher to get the file list', async () => {
|
||||
const matcher = new Matcher([testFolder], ['**/*.md'], []);
|
||||
const ds = new FileDataStore();
|
||||
expect((await ds.list(matcher.include[0])).length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
|
||||
import * as ranges from '../../src/model/range';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
@@ -8,7 +8,7 @@ describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit in the end of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `4. this is fourth line`,
|
||||
range: ranges.create(4, 0, 4, 0),
|
||||
range: Range.create(4, 0, 4, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
@@ -31,7 +31,7 @@ describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit at the top of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `1. this is first line\n`,
|
||||
range: ranges.create(1, 0, 1, 0),
|
||||
range: Range.create(1, 0, 1, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
@@ -53,7 +53,7 @@ describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit in the middle of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `2. this is the updated second line`,
|
||||
range: ranges.create(2, 0, 2, 100),
|
||||
range: Range.create(2, 0, 2, 100),
|
||||
};
|
||||
|
||||
const text = `
|
||||
|
||||
@@ -2,27 +2,36 @@ import * as path from 'path';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Note } from '../../src';
|
||||
import { URI } from '../../src/common/uri';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { Resource } from '../../src/model/note';
|
||||
import { FileDataStore, Matcher } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { getBasename } from '../../src/utils/uri';
|
||||
import * as ranges from '../../src/model/range';
|
||||
import { URI } from '../../src/model/uri';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { MarkdownResourceProvider } from '../../src';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateHeadings', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
const findBySlug = (slug: string): Note => {
|
||||
return _workspace.list().find(res => getBasename(res.uri) === slug) as Note;
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
const foam = await bootstrap(config, new FileDataStore(config));
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
@@ -32,7 +41,7 @@ describe('generateHeadings', () => {
|
||||
newText: `# File without Title
|
||||
|
||||
`,
|
||||
range: ranges.create(0, 0, 0, 0),
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
@@ -52,7 +61,7 @@ describe('generateHeadings', () => {
|
||||
|
||||
const expected = {
|
||||
newText: '\n# File with only Frontmatter\n\n',
|
||||
range: ranges.create(3, 0, 3, 0),
|
||||
range: Range.create(3, 0, 3, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
|
||||
@@ -2,28 +2,37 @@ import * as path from 'path';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Note, ranges } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { FileDataStore, Matcher } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { URI } from '../../src/common/uri';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { getBasename } from '../../src/utils/uri';
|
||||
import { URI } from '../../src/model/uri';
|
||||
import { Resource } from '../../src/model/note';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { MarkdownResourceProvider } from '../../src';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
const findBySlug = (slug: string): Note => {
|
||||
return _workspace.list().find(res => getBasename(res.uri) === slug) as Note;
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
_workspace = await bootstrap(config, new FileDataStore(config)).then(
|
||||
foam => foam.workspace
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
@@ -42,7 +51,7 @@ describe('generateLinkReferences', () => {
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: ranges.create(9, 0, 9, 0),
|
||||
range: Range.create(9, 0, 9, 0),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -57,7 +66,7 @@ describe('generateLinkReferences', () => {
|
||||
|
||||
const expected = {
|
||||
newText: '',
|
||||
range: ranges.create(6, 0, 8, 42),
|
||||
range: Range.create(6, 0, 8, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -77,7 +86,7 @@ describe('generateLinkReferences', () => {
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: ranges.create(8, 0, 10, 42),
|
||||
range: Range.create(8, 0, 10, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -106,6 +115,6 @@ describe('generateLinkReferences', () => {
|
||||
* @param note the note we are adjusting for
|
||||
* @param text starting text, using a \n line separator
|
||||
*/
|
||||
function textForNote(note: Note, text: string): string {
|
||||
function textForNote(note: Resource, text: string): string {
|
||||
return text.split('\n').join(note.source.eol);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
} from '../src/markdown-provider';
|
||||
import { DirectLink } from '../src/model/note';
|
||||
import { ParserPlugin } from '../src/plugins';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { uriToSlug } from '../src/utils';
|
||||
import { FoamWorkspace } from '../src/model/workspace';
|
||||
import { uriToSlug } from '../src/utils/slug';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { FoamGraph } from '../src/model/graph';
|
||||
import { createTestWorkspace } from './core.test';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -43,7 +44,7 @@ const createNoteFromMarkdown = (path: string, content: string) =>
|
||||
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = createTestWorkspace();
|
||||
workspace.set(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
workspace.set(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
workspace.set(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
@@ -104,7 +105,7 @@ this is a [link to intro](#introduction)
|
||||
});
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/page-a.md', pageA);
|
||||
const noteB = createNoteFromMarkdown('/page-b.md', pageB);
|
||||
const noteC = createNoteFromMarkdown('/page-c.md', pageC);
|
||||
@@ -116,13 +117,13 @@ this is a [link to intro](#introduction)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD)
|
||||
.set(noteE)
|
||||
.resolveLinks();
|
||||
.set(noteE);
|
||||
const graph = FoamGraph.fromWorkspace(workspace);
|
||||
|
||||
expect(workspace.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(workspace.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
noteB.uri,
|
||||
noteC.uri,
|
||||
noteD.uri,
|
||||
@@ -233,7 +234,7 @@ title: - one
|
||||
|
||||
describe('wikilinks definitions', () => {
|
||||
it('can generate links without file extension when includeExtension = false', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
@@ -245,7 +246,7 @@ describe('wikilinks definitions', () => {
|
||||
});
|
||||
|
||||
it('can generate links with file extension when includeExtension = true', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
@@ -257,7 +258,7 @@ describe('wikilinks definitions', () => {
|
||||
});
|
||||
|
||||
it('use relative paths', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from 'path';
|
||||
import { loadPlugins } from '../src/plugins';
|
||||
import { createMarkdownParser } from '../src/markdown-provider';
|
||||
import { FoamConfig, createConfigFromObject } from '../src/config';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
51
packages/foam-core/test/uri.test.ts
Normal file
51
packages/foam-core/test/uri.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Logger } from '../src';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { uriToSlug } from '../src/utils/slug';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('Foam URIs', () => {
|
||||
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', URI.create({ ...base, fragment: 'section' })],
|
||||
[
|
||||
'../relative/file.md#section',
|
||||
URI.parse('file:/path/relative/file.md#section'),
|
||||
],
|
||||
])('URI Parsing (%s) - %s', (input, exp) => {
|
||||
const result = URI.resolve(input, base);
|
||||
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);
|
||||
});
|
||||
});
|
||||
it('supports various cases', () => {
|
||||
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('computes a relative uri using a slug', () => {
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.md'), '../hello.md')
|
||||
).toEqual(URI.file('/hello.md'));
|
||||
expect(URI.computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
|
||||
).toEqual(URI.file('/hello.markdown'));
|
||||
});
|
||||
});
|
||||
@@ -1,79 +1,8 @@
|
||||
import {
|
||||
uriToSlug,
|
||||
nameToSlug,
|
||||
hashURI,
|
||||
computeRelativeURI,
|
||||
extractHashtags,
|
||||
parseUri,
|
||||
} from '../src/utils';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { extractHashtags } from '../src/utils';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('URI utils', () => {
|
||||
it('supports various cases', () => {
|
||||
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', () => {
|
||||
expect(nameToSlug('this.has.dots')).toEqual('thishasdots');
|
||||
expect(nameToSlug('title')).toEqual('title');
|
||||
expect(nameToSlug('this is a title')).toEqual('this-is-a-title');
|
||||
expect(nameToSlug('this is a title/slug')).toEqual('this-is-a-titleslug');
|
||||
});
|
||||
|
||||
it('normalizes URI before hashing', () => {
|
||||
expect(hashURI(URI.file('/this/is/a/path.md'))).toEqual(
|
||||
hashURI(URI.file('/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(URI.file('/my/file.md'), '../hello.md')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashtag extraction', () => {
|
||||
it('works with simple strings', () => {
|
||||
expect(extractHashtags('hello #world on #this planet')).toEqual(
|
||||
@@ -95,6 +24,16 @@ describe('hashtag extraction', () => {
|
||||
extractHashtags('this #123 tag should be ignore, but not #123four')
|
||||
).toEqual(new Set(['123four']));
|
||||
});
|
||||
it('supports unicode letters like Chinese charaters', () => {
|
||||
expect(
|
||||
extractHashtags(`
|
||||
this #tag_with_unicode_letters_汉字, pure Chinese tag like #纯中文标签 and
|
||||
other mixed tags like #标签1 #123四 should work
|
||||
`)
|
||||
).toEqual(
|
||||
new Set(['tag_with_unicode_letters_汉字', '纯中文标签', '标签1', '123四'])
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores hashes in plain text urls and links', () => {
|
||||
expect(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FoamWorkspace, getReferenceType } from '../src/model/workspace';
|
||||
import { getReferenceType } from '../src/model/workspace';
|
||||
import { FoamGraph } from '../src/model/graph';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { createTestNote, createAttachment } from './core.test';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { placeholderUri } from '../src/utils';
|
||||
import { createTestNote, createTestWorkspace } from './core.test';
|
||||
import { URI } from '../src/model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('Reference types', () => {
|
||||
|
||||
describe('Workspace resources', () => {
|
||||
it('Adds notes to workspace', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(createTestNote({ uri: '/page-a.md' }));
|
||||
ws.set(createTestNote({ uri: '/page-b.md' }));
|
||||
ws.set(createTestNote({ uri: '/page-c.md' }));
|
||||
@@ -39,25 +39,24 @@ describe('Workspace resources', () => {
|
||||
).toEqual(['/page-a.md', '/page-b.md', '/page-c.md']);
|
||||
});
|
||||
|
||||
it('Listing resources includes notes, attachments and placeholders', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
it('Listing resources includes all notes', () => {
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(createTestNote({ uri: '/page-a.md' }));
|
||||
ws.set(createAttachment({ uri: '/file.pdf' }));
|
||||
ws.set({ type: 'placeholder', uri: placeholderUri('place-holder') });
|
||||
ws.set(createTestNote({ uri: '/file.pdf' }));
|
||||
|
||||
expect(
|
||||
ws
|
||||
.list()
|
||||
.map(n => n.uri.path)
|
||||
.sort()
|
||||
).toEqual(['/file.pdf', '/page-a.md', 'place-holder']);
|
||||
).toEqual(['/file.pdf', '/page-a.md']);
|
||||
});
|
||||
|
||||
it('Fails if getting non-existing note', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
|
||||
const uri = URI.file('/path/to/another/page-b.md');
|
||||
@@ -67,7 +66,27 @@ describe('Workspace resources', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace links', () => {
|
||||
describe('Graph', () => {
|
||||
it('contains notes and placeholders', () => {
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(
|
||||
createTestNote({
|
||||
uri: '/page-a.md',
|
||||
links: [{ slug: 'placeholder-link' }],
|
||||
})
|
||||
);
|
||||
ws.set(createTestNote({ uri: '/file.pdf' }));
|
||||
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getAllNodes()
|
||||
.map(uri => uri.path)
|
||||
.sort()
|
||||
).toEqual(['/file.pdf', '/page-a.md', 'placeholder-link']);
|
||||
});
|
||||
|
||||
it('Supports multiple connections between the same resources', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/note-a.md',
|
||||
@@ -76,11 +95,11 @@ describe('Workspace links', () => {
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
expect(ws.getBacklinks(noteA.uri)).toEqual([
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
expect(graph.getBacklinks(noteA.uri)).toEqual([
|
||||
{
|
||||
source: noteB.uri,
|
||||
target: noteA.uri,
|
||||
@@ -101,21 +120,22 @@ describe('Workspace links', () => {
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getBacklinks(noteA.uri).length).toEqual(2);
|
||||
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
|
||||
|
||||
const noteBBis = createTestNote({
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }],
|
||||
});
|
||||
ws.set(noteBBis);
|
||||
expect(ws.getBacklinks(noteA.uri).length).toEqual(1);
|
||||
expect(graph.getBacklinks(noteA.uri).length).toEqual(1);
|
||||
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,16 +156,16 @@ describe('Wikilinks', () => {
|
||||
{ slug: 'placeholder-test' },
|
||||
],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(createTestNote({ uri: '/somewhere/page-b.md' }))
|
||||
.set(createTestNote({ uri: '/path/another/page-c.md' }))
|
||||
.set(createTestNote({ uri: '/absolute/path/page-d.md' }))
|
||||
.set(createTestNote({ uri: '/absolute/path/page-e.md' }))
|
||||
.resolveLinks();
|
||||
.set(createTestNote({ uri: '/absolute/path/page-e.md' }));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(
|
||||
ws
|
||||
graph
|
||||
.getLinks(noteA.uri)
|
||||
.map(link => link.target.path)
|
||||
.sort()
|
||||
@@ -163,8 +183,8 @@ describe('Wikilinks', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'page-b' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(
|
||||
createTestNote({
|
||||
uri: '/somewhere/page-b.md',
|
||||
@@ -182,11 +202,11 @@ describe('Wikilinks', () => {
|
||||
uri: '/absolute/path/page-d.md',
|
||||
links: [{ slug: '../to/page-a.md' }],
|
||||
})
|
||||
)
|
||||
.resolveLinks();
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(
|
||||
ws
|
||||
graph
|
||||
.getBacklinks(noteA.uri)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
@@ -194,7 +214,7 @@ describe('Wikilinks', () => {
|
||||
});
|
||||
|
||||
it('Uses wikilink definitions when available to resolve target', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
const noteA = createTestNote({
|
||||
uri: '/somewhere/from/page-a.md',
|
||||
links: [{ slug: 'page-b' }],
|
||||
@@ -206,11 +226,10 @@ describe('Wikilinks', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: '/somewhere/to/page-b.md',
|
||||
});
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
ws.set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
expect(graph.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: noteB.uri,
|
||||
link: expect.objectContaining({ type: 'wikilink', slug: 'page-b' }),
|
||||
@@ -225,13 +244,13 @@ describe('Wikilinks', () => {
|
||||
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
|
||||
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
|
||||
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB1)
|
||||
.set(noteB2)
|
||||
.resolveLinks();
|
||||
.set(noteB2);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([
|
||||
expect(graph.getLinks(noteA.uri)).toEqual([
|
||||
{
|
||||
source: noteA.uri,
|
||||
target: noteB1.uri,
|
||||
@@ -249,14 +268,14 @@ describe('Wikilinks', () => {
|
||||
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
|
||||
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
|
||||
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB1)
|
||||
.set(noteB2)
|
||||
.set(noteB3)
|
||||
.resolveLinks();
|
||||
.set(noteB3);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
noteB2.uri,
|
||||
noteB3.uri,
|
||||
]);
|
||||
@@ -272,22 +291,22 @@ describe('Wikilinks', () => {
|
||||
{ slug: 'attachment-b' },
|
||||
],
|
||||
});
|
||||
const attachmentA = createAttachment({
|
||||
const attachmentA = createTestNote({
|
||||
uri: '/path/to/more/attachment-a.pdf',
|
||||
});
|
||||
const attachmentB = createAttachment({
|
||||
const attachmentB = createTestNote({
|
||||
uri: '/path/to/more/attachment-b.pdf',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentA)
|
||||
.set(attachmentB)
|
||||
.resolveLinks();
|
||||
.set(attachmentB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
|
||||
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([
|
||||
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
});
|
||||
@@ -297,19 +316,19 @@ describe('Wikilinks', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'attachment-a' }],
|
||||
});
|
||||
const attachmentA = createAttachment({
|
||||
const attachmentA = createTestNote({
|
||||
uri: '/path/to/more/attachment-a.pdf',
|
||||
});
|
||||
const attachmentABis = createAttachment({
|
||||
const attachmentABis = createTestNote({
|
||||
uri: '/path/to/attachment-a.pdf',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentA)
|
||||
.set(attachmentABis)
|
||||
.resolveLinks();
|
||||
.set(attachmentABis);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
attachmentABis.uri,
|
||||
]);
|
||||
});
|
||||
@@ -319,19 +338,19 @@ describe('Wikilinks', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'attachment-a' }],
|
||||
});
|
||||
const attachmentA = createAttachment({
|
||||
const attachmentA = createTestNote({
|
||||
uri: '/path/to/more/attachment-a.pdf',
|
||||
});
|
||||
const attachmentABis = createAttachment({
|
||||
const attachmentABis = createTestNote({
|
||||
uri: '/path/to/attachment-a.pdf',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentABis)
|
||||
.set(attachmentA)
|
||||
.resolveLinks();
|
||||
.set(attachmentA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
attachmentABis.uri,
|
||||
]);
|
||||
});
|
||||
@@ -350,22 +369,24 @@ describe('markdown direct links', () => {
|
||||
const noteC = createTestNote({
|
||||
uri: '/path/to/more/page-c.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.resolveLinks();
|
||||
.set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(
|
||||
ws
|
||||
graph
|
||||
.getLinks(noteA.uri)
|
||||
.map(link => link.target.path)
|
||||
.sort()
|
||||
).toEqual(['/path/to/another/page-b.md', '/path/to/more/page-c.md']);
|
||||
|
||||
expect(ws.getLinks(noteB.uri).map(l => l.target)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteA.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
expect(ws.getConnections(noteA.uri)).toEqual([
|
||||
expect(graph.getLinks(noteB.uri).map(l => l.target)).toEqual([noteA.uri]);
|
||||
expect(graph.getBacklinks(noteA.uri).map(l => l.source)).toEqual([
|
||||
noteB.uri,
|
||||
]);
|
||||
expect(graph.getConnections(noteA.uri)).toEqual([
|
||||
{
|
||||
source: noteA.uri,
|
||||
target: noteB.uri,
|
||||
@@ -387,41 +408,43 @@ describe('markdown direct links', () => {
|
||||
|
||||
describe('Placeholders', () => {
|
||||
it('Treats direct links to non-existing files as placeholders', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
const noteA = createTestNote({
|
||||
uri: '/somewhere/from/page-a.md',
|
||||
links: [{ to: '../page-b.md' }, { to: '/path/to/page-c.md' }],
|
||||
});
|
||||
ws.set(noteA).resolveLinks();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
expect(graph.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/somewhere/page-b.md'),
|
||||
target: URI.placeholder('/somewhere/page-b.md'),
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
});
|
||||
expect(ws.getAllConnections()[1]).toEqual({
|
||||
expect(graph.getAllConnections()[1]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/path/to/page-c.md'),
|
||||
target: URI.placeholder('/path/to/page-c.md'),
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('Treats wikilinks without matching file as placeholders', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
const noteA = createTestNote({
|
||||
uri: '/somewhere/page-a.md',
|
||||
links: [{ slug: 'page-b' }],
|
||||
});
|
||||
ws.set(noteA).resolveLinks();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
expect(graph.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('page-b'),
|
||||
target: URI.placeholder('page-b'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
});
|
||||
it('Treats wikilink with definition to non-existing file as placeholders', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
const noteA = createTestNote({
|
||||
uri: '/somewhere/page-a.md',
|
||||
links: [{ slug: 'page-b' }, { slug: 'page-c' }],
|
||||
@@ -434,18 +457,19 @@ describe('Placeholders', () => {
|
||||
label: 'page-c',
|
||||
url: '/path/to/page-c.md',
|
||||
});
|
||||
ws.set(noteA)
|
||||
.set(createTestNote({ uri: '/different/location/for/note-b.md' }))
|
||||
.resolveLinks();
|
||||
ws.set(noteA).set(
|
||||
createTestNote({ uri: '/different/location/for/note-b.md' })
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
expect(graph.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/somewhere/page-b.md'),
|
||||
target: URI.placeholder('/somewhere/page-b.md'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
expect(ws.getAllConnections()[1]).toEqual({
|
||||
expect(graph.getAllConnections()[1]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/path/to/page-c.md'),
|
||||
target: URI.placeholder('/path/to/page-c.md'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
});
|
||||
@@ -464,15 +488,19 @@ describe('Updating workspace happy path', () => {
|
||||
const noteC = createTestNote({
|
||||
uri: '/path/to/more/page-c.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.resolveLinks();
|
||||
.set(noteC);
|
||||
let graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
|
||||
noteB.uri,
|
||||
]);
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
@@ -481,16 +509,20 @@ describe('Updating workspace happy path', () => {
|
||||
});
|
||||
ws.set(noteABis);
|
||||
// change is not propagated immediately
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
|
||||
noteB.uri,
|
||||
]);
|
||||
|
||||
// recompute the links
|
||||
ws.resolveLinks();
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
graph = FoamGraph.fromWorkspace(ws);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
expect(
|
||||
ws
|
||||
graph
|
||||
.getBacklinks(noteC.uri)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
@@ -505,21 +537,22 @@ describe('Updating workspace happy path', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/another/page-b.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
ws.delete(noteB.uri);
|
||||
ws.resolveLinks();
|
||||
const graph2 = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(() => ws.get(noteB.uri)).toThrow();
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
expect(graph2.contains(URI.placeholder('page-b'))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for wikilinks', () => {
|
||||
@@ -527,13 +560,14 @@ describe('Updating workspace happy path', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'page-b' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('page-b'),
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
URI.placeholder('page-b'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
expect(graph.contains(URI.placeholder('page-b'))).toBeTruthy();
|
||||
|
||||
// add note-b
|
||||
const noteB = createTestNote({
|
||||
@@ -541,9 +575,9 @@ describe('Updating workspace happy path', () => {
|
||||
});
|
||||
|
||||
ws.set(noteB);
|
||||
ws.resolveLinks();
|
||||
FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
});
|
||||
|
||||
@@ -555,23 +589,24 @@ describe('Updating workspace happy path', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/another/page-b.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
ws.delete(noteB.uri);
|
||||
ws.resolveLinks();
|
||||
const graph2 = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(() => ws.get(noteB.uri)).toThrow();
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
expect(
|
||||
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for direct links', () => {
|
||||
@@ -579,15 +614,16 @@ describe('Updating workspace happy path', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: '/path/to/another/page-b.md' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('/path/to/another/page-b.md'),
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
URI.placeholder('/path/to/another/page-b.md'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
expect(() =>
|
||||
ws.get(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toThrow();
|
||||
|
||||
// add note-b
|
||||
const noteB = createTestNote({
|
||||
@@ -595,9 +631,9 @@ describe('Updating workspace happy path', () => {
|
||||
});
|
||||
|
||||
ws.set(noteB);
|
||||
ws.resolveLinks();
|
||||
FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
});
|
||||
|
||||
@@ -606,22 +642,23 @@ describe('Updating workspace happy path', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: '/path/to/another/page-b.md' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks();
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
expect(
|
||||
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeTruthy();
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [],
|
||||
});
|
||||
ws.set(noteABis).resolveLinks();
|
||||
ws.set(noteABis);
|
||||
const graph2 = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(() =>
|
||||
ws.get(placeholderUri('/path/to/another/page-b.md'))
|
||||
).toThrow();
|
||||
expect(
|
||||
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -638,15 +675,19 @@ describe('Monitoring of workspace state', () => {
|
||||
const noteC = createTestNote({
|
||||
uri: '/path/to/more/page-c.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.resolveLinks(true);
|
||||
.set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
|
||||
noteB.uri,
|
||||
]);
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
@@ -655,15 +696,16 @@ describe('Monitoring of workspace state', () => {
|
||||
});
|
||||
ws.set(noteABis);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
expect(
|
||||
ws
|
||||
graph
|
||||
.getBacklinks(noteC.uri)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('Removing target note should produce placeholder for wikilinks', () => {
|
||||
@@ -674,21 +716,22 @@ describe('Monitoring of workspace state', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/another/page-b.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
ws.delete(noteB.uri);
|
||||
|
||||
expect(() => ws.get(noteB.uri)).toThrow();
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for wikilinks', () => {
|
||||
@@ -696,13 +739,13 @@ describe('Monitoring of workspace state', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'page-b' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks(true);
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('page-b'),
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
URI.placeholder('page-b'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
|
||||
// add note-b
|
||||
const noteB = createTestNote({
|
||||
@@ -711,9 +754,10 @@ describe('Monitoring of workspace state', () => {
|
||||
|
||||
ws.set(noteB);
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('Removing target note should produce placeholder for direct links', () => {
|
||||
@@ -724,23 +768,25 @@ describe('Monitoring of workspace state', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/another/page-b.md',
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
ws.delete(noteB.uri);
|
||||
|
||||
expect(() => ws.get(noteB.uri)).toThrow();
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
expect(
|
||||
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeTruthy();
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for direct links', () => {
|
||||
@@ -748,15 +794,16 @@ describe('Monitoring of workspace state', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: '/path/to/another/page-b.md' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks(true);
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('/path/to/another/page-b.md'),
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
URI.placeholder('/path/to/another/page-b.md'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
expect(
|
||||
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeTruthy();
|
||||
|
||||
// add note-b
|
||||
const noteB = createTestNote({
|
||||
@@ -765,9 +812,10 @@ describe('Monitoring of workspace state', () => {
|
||||
|
||||
ws.set(noteB);
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('removing link to placeholder should remove placeholder', () => {
|
||||
@@ -775,11 +823,12 @@ describe('Monitoring of workspace state', () => {
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: '/path/to/another/page-b.md' }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks(true);
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
expect(
|
||||
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeTruthy();
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
@@ -787,9 +836,10 @@ describe('Monitoring of workspace state', () => {
|
||||
links: [],
|
||||
});
|
||||
ws.set(noteABis);
|
||||
expect(() =>
|
||||
ws.get(placeholderUri('/path/to/another/page-b.md'))
|
||||
).toThrow();
|
||||
expect(
|
||||
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
|
||||
).toBeFalsy();
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,46 @@ 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.13.3] - 2021-05-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved Foam template variables resolution: unknown variables are now ignored (#622 - thanks @movermeyer)
|
||||
- Fixed file matching in MarkdownProvider (#617)
|
||||
- Fixed cancelling `Foam: Create New Note` and `Foam: Create New Note From Template` behavior (#623 - thanks @movermeyer)
|
||||
|
||||
## [0.13.2] - 2021-05-06
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed wikilink completion bug (#592 - thanks @RobinKing)
|
||||
- Added support for stylable tags (#598 - thanks @Barabazs)
|
||||
- Added "Create new note" command (#601 - thanks @movermeyer)
|
||||
- Fixed navigation from placeholder and orphan panel (#600)
|
||||
|
||||
Internal:
|
||||
|
||||
- Refactored data model representation of resources: `Resource` (#593)
|
||||
|
||||
## [0.13.1] - 2021-04-21
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- fixed bug in Windows when running `Open Daily Note` command (#591 - Thanks @RobinKing)
|
||||
|
||||
## [0.13.0] - 2021-04-19
|
||||
|
||||
Features:
|
||||
|
||||
- Wikilink completion (#554)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- fixed link navigation on path with spaces (#542)
|
||||
- support for Chinese characters in tags (#567 - thanks @RobinKing)
|
||||
- added support for `FOAM_TITLE` in templates (#549 - thanks @movermeyer)
|
||||
- added configuration to enable/disable link navigation (#584)
|
||||
|
||||
## [0.12.1] - 2021-04-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
|
||||
|
||||
> ⚠️ This is an early stage software. Use at your own peril.
|
||||
|
||||
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
|
||||
|
||||
[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.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.12.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -28,7 +28,8 @@
|
||||
"onCommand:foam-vscode.copy-without-brackets",
|
||||
"onCommand:foam-vscode.show-graph",
|
||||
"onCommand:foam-vscode.create-new-template",
|
||||
"onCommand:foam-vscode.create-note-from-template"
|
||||
"onCommand:foam-vscode.create-note-from-template",
|
||||
"onCommand:foam-vscode.create-note-from-default-template"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
@@ -161,6 +162,10 @@
|
||||
"command": "foam-vscode.create-note-from-template",
|
||||
"title": "Foam: Create New Note From Template"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-note-from-default-template",
|
||||
"title": "Foam: Create New Note"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"title": "Foam: Open Resource"
|
||||
@@ -230,27 +235,28 @@
|
||||
"Disable wikilink definitions generation"
|
||||
]
|
||||
},
|
||||
"foam.links.navigation.enable": {
|
||||
"description": "Enable navigation through links",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"foam.decorations.links.enable": {
|
||||
"description": "Enable decorations for links",
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": false
|
||||
},
|
||||
"foam.openDailyNote.onStartup": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": false
|
||||
},
|
||||
"foam.openDailyNote.fileExtension": {
|
||||
"type": "string",
|
||||
"scope": "resource",
|
||||
"default": "md"
|
||||
},
|
||||
"foam.openDailyNote.filenameFormat": {
|
||||
"type": "string",
|
||||
"default": "isoDate",
|
||||
"markdownDescription": "Specifies how the daily note filename is formatted. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats",
|
||||
"scope": "resource"
|
||||
"markdownDescription": "Specifies how the daily note filename is formatted. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats"
|
||||
},
|
||||
"foam.openDailyNote.titleFormat": {
|
||||
"type": [
|
||||
@@ -258,8 +264,7 @@
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"markdownDescription": "Specifies how the daily note title is formatted. Will default to the filename format if set to null. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats",
|
||||
"scope": "resource"
|
||||
"markdownDescription": "Specifies how the daily note title is formatted. Will default to the filename format if set to null. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats"
|
||||
},
|
||||
"foam.openDailyNote.directory": {
|
||||
"type": [
|
||||
@@ -274,8 +279,7 @@
|
||||
"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"
|
||||
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the orphans report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
|
||||
},
|
||||
"foam.orphans.groupBy": {
|
||||
"type": [
|
||||
@@ -290,16 +294,14 @@
|
||||
"Group by folder"
|
||||
],
|
||||
"default": "folder",
|
||||
"markdownDescription": "Group orphans report entries by.",
|
||||
"scope": "resource"
|
||||
"markdownDescription": "Group orphans report entries by."
|
||||
},
|
||||
"foam.placeholders.exclude": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"default": [],
|
||||
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the placeholders report. To ignore the all the content of a given folder, use `**<folderName>/**/*`",
|
||||
"scope": "resource"
|
||||
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the placeholders report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
|
||||
},
|
||||
"foam.placeholders.groupBy": {
|
||||
"type": [
|
||||
@@ -314,8 +316,7 @@
|
||||
"Group by folder"
|
||||
],
|
||||
"default": "folder",
|
||||
"markdownDescription": "Group blank note report entries by.",
|
||||
"scope": "resource"
|
||||
"markdownDescription": "Group blank note report entries by."
|
||||
},
|
||||
"foam.dateSnippets.afterCompletion": {
|
||||
"type": "string",
|
||||
@@ -394,7 +395,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"foam-core": "^0.12.1",
|
||||
"foam-core": "^0.13.3",
|
||||
"gray-matter": "^4.0.2",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
|
||||
@@ -1,74 +1,49 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { getDailyNotePath } from './dated-notes';
|
||||
import { URI } from 'foam-core';
|
||||
import { isWindows } from './utils';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories (Posix paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
config,
|
||||
`${isoDate}.md`
|
||||
);
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'journal/subdir');
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`journal[\\\\/]subdir[\\\\/]${isoDate}.md$`)
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toEqual(
|
||||
expectedPath.fsPath
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification (Posix paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
test('Uses absolute directories without modification', async () => {
|
||||
const config = isWindows
|
||||
? 'c:\\absolute_path\\journal'
|
||||
: '/absolute_path/journal';
|
||||
const expectedPath = isWindows
|
||||
? `${config}\\${isoDate}.md`
|
||||
: `${config}/${isoDate}.md`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', '/absolute_path/journal');
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`^/absolute_path/journal/${isoDate}.md`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Adds the root directory to relative directories (Windows paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'journal\\subdir');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`journal[\\\\/]subdir[\\\\/]${isoDate}.md$`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification (Windows paths)', async () => {
|
||||
// While technically the test passes on all OS's, it's only because the test is overly loose.
|
||||
// On Posix systems, this test actually does modify the path, since Windows style paths are
|
||||
// considered to be relative paths. So while this test passes on Posix systems, it is not
|
||||
// because it treats it as an absolute path, but rather that the test doesn't check the same thing.
|
||||
// This was considered "good enough" instead of introducing a dependency like `skip-if` to skip the
|
||||
// test on Posix systems.
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'C:\\absolute_path\\journal');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`/C:[\\\\/]absolute_path[\\\\/]journal[\\\\/]${isoDate}.md`)
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toMatch(
|
||||
expectedPath
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import { workspace, WorkspaceConfiguration, Uri } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import * as fs from 'fs';
|
||||
import { isAbsolute } from 'path';
|
||||
import { docConfig, focusNote, getDirname, pathExists } from './utils';
|
||||
import { docConfig, focusNote, pathExists } from './utils';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
async function openDailyNoteFor(date?: Date) {
|
||||
@@ -28,7 +28,7 @@ function getDailyNotePath(
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
|
||||
if (isAbsolute(dailyNoteDirectory)) {
|
||||
return URI.joinPath(URI.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
return URI.joinPath(Uri.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
} else {
|
||||
return URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
@@ -68,7 +68,7 @@ async function createDailyNoteIfNotExists(
|
||||
configuration.get('openDailyNote.filenameFormat');
|
||||
|
||||
await fs.promises.writeFile(
|
||||
dailyNotePath.fsPath,
|
||||
URI.toFsPath(dailyNotePath),
|
||||
`# ${dateFormat(currentDate, titleFormat, false)}${docConfig.eol}${
|
||||
docConfig.eol
|
||||
}`
|
||||
@@ -78,10 +78,12 @@ async function createDailyNoteIfNotExists(
|
||||
}
|
||||
|
||||
async function createDailyNoteDirectoryIfNotExists(dailyNotePath: URI) {
|
||||
const dailyNoteDirectory = getDirname(dailyNotePath);
|
||||
const dailyNoteDirectory = URI.getDir(dailyNotePath);
|
||||
|
||||
if (!(await pathExists(dailyNoteDirectory))) {
|
||||
await fs.promises.mkdir(dailyNoteDirectory.fsPath, { recursive: true });
|
||||
await fs.promises.mkdir(URI.toFsPath(dailyNoteDirectory), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import { bootstrap, FoamConfig, Foam, Logger, FileDataStore } from 'foam-core';
|
||||
import {
|
||||
bootstrap,
|
||||
FoamConfig,
|
||||
Logger,
|
||||
FileDataStore,
|
||||
Matcher,
|
||||
MarkdownResourceProvider,
|
||||
ResourceProvider,
|
||||
} from 'foam-core';
|
||||
|
||||
import { features } from './features';
|
||||
import { getConfigFromVscode } from './services/config';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
|
||||
function createMarkdownProvider(config: FoamConfig): ResourceProvider {
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
const provider = new MarkdownResourceProvider(matcher, triggers => {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
return [
|
||||
watcher.onDidChange(triggers.onDidChange),
|
||||
watcher.onDidCreate(triggers.onDidCreate),
|
||||
watcher.onDidDelete(triggers.onDidDelete),
|
||||
watcher,
|
||||
];
|
||||
});
|
||||
return provider;
|
||||
}
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
Logger.setDefaultLogger(logger);
|
||||
@@ -13,18 +39,18 @@ export async function activate(context: ExtensionContext) {
|
||||
try {
|
||||
Logger.info('Starting Foam');
|
||||
|
||||
// Prepare Foam
|
||||
const config: FoamConfig = getConfigFromVscode();
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
const dataStore = new FileDataStore(config, watcher);
|
||||
|
||||
const foamPromise: Promise<Foam> = bootstrap(config, dataStore);
|
||||
const dataStore = new FileDataStore();
|
||||
const markdownProvider = createMarkdownProvider(config);
|
||||
const foamPromise = bootstrap(config, dataStore, [markdownProvider]);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
|
||||
|
||||
context.subscriptions.push(dataStore, foam, watcher);
|
||||
context.subscriptions.push(foam, markdownProvider);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { workspace, window } from 'vscode';
|
||||
import { URI, FoamWorkspace, IDataStore } from 'foam-core';
|
||||
import { URI, FoamGraph } from 'foam-core';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createNote,
|
||||
createTestNote,
|
||||
createTestWorkspace,
|
||||
} from '../test/test-utils';
|
||||
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
describe('Backlinks panel', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -18,18 +20,13 @@ describe('Backlinks panel', () => {
|
||||
await createNote(noteC);
|
||||
});
|
||||
afterAll(async () => {
|
||||
graph.dispose();
|
||||
ws.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
const rootUri = workspace.workspaceFolders[0].uri;
|
||||
const ws = new FoamWorkspace();
|
||||
const dataStore = {
|
||||
read: uri => {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
isMatch: uri => uri.path.endsWith('.md'),
|
||||
} as IDataStore;
|
||||
const ws = createTestWorkspace();
|
||||
|
||||
const noteA = createTestNote({
|
||||
root: rootUri,
|
||||
@@ -47,10 +44,10 @@ describe('Backlinks panel', () => {
|
||||
});
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.resolveLinks(true);
|
||||
.set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(ws, dataStore);
|
||||
const provider = new BacklinksTreeDataProvider(ws, graph);
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
@@ -64,8 +61,8 @@ describe('Backlinks panel', () => {
|
||||
expect(await provider.getChildren()).toEqual([]);
|
||||
});
|
||||
it.skip('targets active editor', async () => {
|
||||
const docA = await workspace.openTextDocument(noteA.uri);
|
||||
const docB = await workspace.openTextDocument(noteB.uri);
|
||||
const docA = await workspace.openTextDocument(toVsCodeUri(noteA.uri));
|
||||
const docB = await workspace.openTextDocument(toVsCodeUri(noteB.uri));
|
||||
|
||||
await window.showTextDocument(docA);
|
||||
expect(provider.target).toEqual(noteA.uri);
|
||||
@@ -99,7 +96,7 @@ describe('Backlinks panel', () => {
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes[0].command).toMatchObject({
|
||||
command: OPEN_COMMAND.command,
|
||||
arguments: [expect.objectContaining({ resource: noteB.uri })],
|
||||
arguments: [expect.objectContaining({ uri: noteB.uri })],
|
||||
});
|
||||
});
|
||||
it('navigates to document with link selection if clicking on backlink', async () => {
|
||||
|
||||
@@ -3,18 +3,15 @@ import { groupBy } from 'lodash';
|
||||
import {
|
||||
Foam,
|
||||
FoamWorkspace,
|
||||
IDataStore,
|
||||
isNote,
|
||||
NoteLink,
|
||||
FoamGraph,
|
||||
ResourceLink,
|
||||
Resource,
|
||||
isSameUri,
|
||||
URI,
|
||||
Range,
|
||||
} from 'foam-core';
|
||||
import { getNoteTooltip } from '../utils';
|
||||
import { getNoteTooltip, isNone } from '../utils';
|
||||
import { FoamFeature } from '../types';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
import { Position } from 'unist';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -23,10 +20,7 @@ const feature: FoamFeature = {
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(
|
||||
foam.workspace,
|
||||
foam.services.dataStore
|
||||
);
|
||||
const provider = new BacklinksTreeDataProvider(foam.workspace, foam.graph);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
provider.target = vscode.window.activeTextEditor?.document.uri;
|
||||
@@ -43,9 +37,6 @@ const feature: FoamFeature = {
|
||||
};
|
||||
export default feature;
|
||||
|
||||
const isBefore = (a: Range, b: Range) =>
|
||||
a.start.line - b.start.line || a.start.character - b.start.character;
|
||||
|
||||
export class BacklinksTreeDataProvider
|
||||
implements vscode.TreeDataProvider<BacklinkPanelTreeItem> {
|
||||
public target?: URI = undefined;
|
||||
@@ -53,10 +44,7 @@ export class BacklinksTreeDataProvider
|
||||
private _onDidChangeTreeDataEmitter = new vscode.EventEmitter<BacklinkPanelTreeItem | undefined | void>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private dataStore: IDataStore
|
||||
) {}
|
||||
constructor(private workspace: FoamWorkspace, private graph: FoamGraph) {}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeDataEmitter.fire();
|
||||
@@ -70,18 +58,17 @@ export class BacklinksTreeDataProvider
|
||||
const uri = this.target;
|
||||
if (item) {
|
||||
const resource = item.resource;
|
||||
if (!isNote(resource)) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
isSameUri(this.workspace.resolveLink(resource, link), uri)
|
||||
URI.isEqual(this.workspace.resolveLink(resource, link), uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
const lines = (await this.dataStore.read(resource.uri)).split('\n');
|
||||
const lines = (
|
||||
(await this.workspace.read(resource.uri)) ?? ''
|
||||
).split('\n');
|
||||
if (link.range.start.line < lines.length) {
|
||||
const line = lines[link.range.start.line];
|
||||
let start = Math.max(0, link.range.start.character - 15);
|
||||
@@ -100,27 +87,26 @@ export class BacklinksTreeDataProvider
|
||||
return backlinkRefs;
|
||||
}
|
||||
|
||||
if (!uri || !this.dataStore.isMatch(uri)) {
|
||||
if (isNone(uri) || isNone(this.workspace.find(uri))) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const backlinksByResourcePath = groupBy(
|
||||
this.workspace.getConnections(uri).filter(c => isSameUri(c.target, uri)),
|
||||
this.graph.getConnections(uri).filter(c => URI.isEqual(c.target, uri)),
|
||||
b => b.source.path
|
||||
);
|
||||
|
||||
const resources = Object.keys(backlinksByResourcePath)
|
||||
.map(res => backlinksByResourcePath[res][0].source)
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.filter(isNote)
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.sort(Resource.sortByTitle)
|
||||
.map(note => {
|
||||
const connections = backlinksByResourcePath[
|
||||
note.uri.path
|
||||
].sort((a, b) => isBefore(a.link.range, b.link.range));
|
||||
].sort((a, b) => Range.isBefore(a.link.range, b.link.range));
|
||||
const item = new ResourceTreeItem(
|
||||
note,
|
||||
this.dataStore,
|
||||
this.workspace,
|
||||
vscode.TreeItemCollapsibleState.Expanded
|
||||
);
|
||||
item.description = `(${connections.length}) ${item.description}`;
|
||||
@@ -137,7 +123,7 @@ export class BacklinksTreeDataProvider
|
||||
export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
public readonly link: NoteLink
|
||||
public readonly link: ResourceLink
|
||||
) {
|
||||
super(
|
||||
link.type === 'wikilink' ? link.slug : link.label,
|
||||
|
||||
@@ -20,6 +20,33 @@ describe('createFromTemplate', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create-note-from-default-template', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const fileWriteSpy = jest.spyOn(workspace.fs, 'writeFile');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(fileWriteSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create-new-template', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
123
packages/foam-vscode/src/features/create-from-template.test.ts
Normal file
123
packages/foam-vscode/src/features/create-from-template.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { SnippetString, window } from 'vscode';
|
||||
import {
|
||||
resolveFoamVariables,
|
||||
resolveFoamTemplateVariables,
|
||||
substituteFoamVariables,
|
||||
} from './create-from-template';
|
||||
|
||||
describe('substituteFoamVariables', () => {
|
||||
test('Does nothing if no Foam-specific variables are used', () => {
|
||||
const input = `
|
||||
# \${AnotherVariable} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
|
||||
# $AnotherVariable} <-- Unrelated to foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
|
||||
`;
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', 'My note title');
|
||||
expect(substituteFoamVariables(input, givenValues)).toEqual(input);
|
||||
});
|
||||
|
||||
test('Correctly substitutes variables that are substrings of one another', () => {
|
||||
// FOAM_TITLE is a substring of FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
// If we're not careful with how we substitute the values
|
||||
// we can end up putting the FOAM_TITLE in place FOAM_TITLE_NON_EXISTENT_VARIABLE should be.
|
||||
const input = `
|
||||
# \${FOAM_TITLE}
|
||||
# $FOAM_TITLE
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
# My note title
|
||||
# My note title
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', 'My note title');
|
||||
expect(substituteFoamVariables(input, givenValues)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFoamVariables', () => {
|
||||
test('Does nothing for unknown Foam-specific variables', async () => {
|
||||
const variables = ['FOAM_FOO'];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_FOO', 'FOAM_FOO');
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foam_title)));
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', foam_title);
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFoamTemplateVariables', () => {
|
||||
test('Does nothing for template without Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# \${AnotherVariable} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
|
||||
# $AnotherVariable} <-- Unrelated to foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedSnippet = new SnippetString(input);
|
||||
const expected = [expectedMap, expectedSnippet];
|
||||
|
||||
expect(await resolveFoamTemplateVariables(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Does nothing for unknown Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# $FOAM_FOO
|
||||
# \${FOAM_FOO}
|
||||
# \${FOAM_FOO:default_value}
|
||||
# \${FOAM_FOO:default_value/(.*)/\${1:/upcase}/}}
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedSnippet = new SnippetString(input);
|
||||
const expected = [expectedMap, expectedSnippet];
|
||||
|
||||
expect(await resolveFoamTemplateVariables(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -4,26 +4,41 @@ import {
|
||||
ExtensionContext,
|
||||
workspace,
|
||||
SnippetString,
|
||||
Uri,
|
||||
} from 'vscode';
|
||||
import { URI } from 'foam-core';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { TextEncoder } from 'util';
|
||||
import { focusNote } from '../utils';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const templatesDir = URI.joinPath(
|
||||
const templatesDir = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates'
|
||||
);
|
||||
|
||||
export class UserCancelledOperation extends Error {
|
||||
constructor(message?: string) {
|
||||
super('UserCancelledOperation');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const knownFoamVariables = new Set(['FOAM_TITLE']);
|
||||
|
||||
const defaultTemplateDefaultText: string = '# ${FOAM_TITLE}'; // eslint-disable-line no-template-curly-in-string
|
||||
const defaultTemplateUri = Uri.joinPath(templatesDir, 'new-note.md');
|
||||
|
||||
const templateContent = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
Welcome to Foam templates.
|
||||
|
||||
What you see in the heading is a placeholder
|
||||
- it allows you to quickly move through positions of the new note by pressing TAB, e.g. to easily fill fields
|
||||
- a placeholder optionally has a default value, which can be some text or, as in this case, a [variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables)
|
||||
- a placeholder optionally has a default value, which can be some text or, as in this case, a [variable](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables)
|
||||
- when landing on a placeholder, the default value is already selected so you can easily replace it
|
||||
- a placeholder can define a list of values, e.g.: \${2|one,two,three|}
|
||||
- you can use variables even outside of placeholders, here is today's date: \${CURRENT_YEAR}/\${CURRENT_MONTH}/\${CURRENT_DATE}
|
||||
@@ -33,7 +48,7 @@ For a full list of features see [the VS Code snippets page](https://code.visuals
|
||||
## To get started
|
||||
|
||||
1. edit this file to create the shape new notes from this template will look like
|
||||
2. create a note from this template by running the 'Foam: Create new note from template' command
|
||||
2. create a note from this template by running the \`Foam: Create New Note From Template\` command
|
||||
`;
|
||||
|
||||
async function getTemplates(): Promise<string[]> {
|
||||
@@ -52,31 +67,112 @@ async function offerToCreateTemplate(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function createNoteFromTemplate(): Promise<void> {
|
||||
function findFoamVariables(templateText: string): string[] {
|
||||
const regex = /\$(FOAM_[_a-zA-Z0-9]*)|\${(FOAM_[[_a-zA-Z0-9]*)}/g;
|
||||
var matches = [];
|
||||
const output: string[] = [];
|
||||
while ((matches = regex.exec(templateText))) {
|
||||
output.push(matches[1] || matches[2]);
|
||||
}
|
||||
const uniqVariables = [...new Set(output)];
|
||||
const knownVariables = uniqVariables.filter(x => knownFoamVariables.has(x));
|
||||
return knownVariables;
|
||||
}
|
||||
|
||||
async function resolveFoamTitle() {
|
||||
const title = await window.showInputBox({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: value =>
|
||||
value.trim().length === 0 ? 'Please enter a title' : undefined,
|
||||
});
|
||||
if (title === undefined) {
|
||||
throw new UserCancelledOperation();
|
||||
}
|
||||
return title;
|
||||
}
|
||||
class Resolver {
|
||||
promises = new Map<string, Thenable<string>>();
|
||||
|
||||
resolve(name: string, givenValues: Map<string, string>): Thenable<string> {
|
||||
if (givenValues.has(name)) {
|
||||
this.promises.set(name, Promise.resolve(givenValues.get(name)));
|
||||
} else if (!this.promises.has(name)) {
|
||||
switch (name) {
|
||||
case 'FOAM_TITLE':
|
||||
this.promises.set(name, resolveFoamTitle());
|
||||
break;
|
||||
default:
|
||||
this.promises.set(name, Promise.resolve(name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const result = this.promises.get(name);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveFoamVariables(
|
||||
variables: string[],
|
||||
givenValues: Map<string, string>
|
||||
) {
|
||||
const resolver = new Resolver();
|
||||
const promises = variables.map(async variable =>
|
||||
Promise.resolve([variable, await resolver.resolve(variable, givenValues)])
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const valueByName = new Map<string, string>();
|
||||
results.forEach(([variable, value]) => {
|
||||
valueByName.set(variable, value);
|
||||
});
|
||||
|
||||
return valueByName;
|
||||
}
|
||||
|
||||
export function substituteFoamVariables(
|
||||
templateText: string,
|
||||
givenValues: Map<string, string>
|
||||
) {
|
||||
givenValues.forEach((value, variable) => {
|
||||
const regex = new RegExp(
|
||||
// Matches a limited subset of the the TextMate variable syntax:
|
||||
// ${VARIABLE} OR $VARIABLE
|
||||
`\\\${${variable}}|\\$${variable}(\\W|$)`,
|
||||
// The latter is more complicated, since it needs to avoid replacing
|
||||
// longer variable names with the values of variables that are
|
||||
// substrings of the longer ones (e.g. `$FOO` and `$FOOBAR`. If you
|
||||
// replace $FOO first, and aren't careful, you replace the first
|
||||
// characters of `$FOOBAR`)
|
||||
'g' // 'g' => Global replacement (i.e. not just the first instance)
|
||||
);
|
||||
templateText = templateText.replace(regex, `${value}$1`);
|
||||
});
|
||||
|
||||
return templateText;
|
||||
}
|
||||
|
||||
async function askUserForTemplate() {
|
||||
const templates = await getTemplates();
|
||||
if (templates.length === 0) {
|
||||
return offerToCreateTemplate();
|
||||
}
|
||||
const activeFile = window.activeTextEditor?.document?.uri.path;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? URI.parse(path.dirname(activeFile))
|
||||
: workspace.workspaceFolders[0].uri;
|
||||
const selectedTemplate = await window.showQuickPick(templates, {
|
||||
return await window.showQuickPick(templates, {
|
||||
placeHolder: 'Select a template to use.',
|
||||
});
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultFileName = 'new-note.md';
|
||||
const defaultDir = URI.joinPath(currentDir, defaultFileName);
|
||||
const filename = await window.showInputBox({
|
||||
async function askUserForFilepathConfirmation(
|
||||
defaultFilepath: Uri,
|
||||
defaultFilename: string
|
||||
) {
|
||||
return await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: defaultDir.fsPath,
|
||||
value: defaultFilepath.fsPath,
|
||||
valueSelection: [
|
||||
defaultDir.fsPath.length - defaultFileName.length,
|
||||
defaultDir.fsPath.length - 3,
|
||||
defaultFilepath.fsPath.length - defaultFilename.length,
|
||||
defaultFilepath.fsPath.length - 3,
|
||||
],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
@@ -85,33 +181,127 @@ async function createNoteFromTemplate(): Promise<void> {
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
if (filename === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function resolveFoamTemplateVariables(
|
||||
templateText: string
|
||||
): Promise<[Map<string, string>, SnippetString]> {
|
||||
const givenValues = new Map<string, string>();
|
||||
const variables = findFoamVariables(templateText.toString());
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(variables, givenValues);
|
||||
const subbedText = substituteFoamVariables(
|
||||
templateText.toString(),
|
||||
resolvedValues
|
||||
);
|
||||
const snippet = new SnippetString(subbedText);
|
||||
return [resolvedValues, snippet];
|
||||
}
|
||||
|
||||
async function writeTemplate(templateSnippet: SnippetString, filepath: Uri) {
|
||||
await workspace.fs.writeFile(filepath, new TextEncoder().encode(''));
|
||||
await focusNote(filepath, true);
|
||||
await window.activeTextEditor.insertSnippet(templateSnippet);
|
||||
}
|
||||
|
||||
function currentDirectoryFilepath(filename: string) {
|
||||
const activeFile = window.activeTextEditor?.document?.uri.path;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? Uri.parse(path.dirname(activeFile))
|
||||
: workspace.workspaceFolders[0].uri;
|
||||
|
||||
return Uri.joinPath(currentDir, filename);
|
||||
}
|
||||
|
||||
async function createNoteFromDefaultTemplate(): Promise<void> {
|
||||
const templateUri = defaultTemplateUri;
|
||||
const templateText = existsSync(templateUri.fsPath)
|
||||
? await workspace.fs.readFile(templateUri).then(bytes => bytes.toString())
|
||||
: defaultTemplateDefaultText;
|
||||
|
||||
let resolvedValues, templateSnippet;
|
||||
try {
|
||||
[resolvedValues, templateSnippet] = await resolveFoamTemplateVariables(
|
||||
templateText
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const templateText = await workspace.fs.readFile(
|
||||
URI.joinPath(templatesDir, selectedTemplate)
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
const defaultFilename = `${defaultSlug}.md`;
|
||||
const defaultFilepath = currentDirectoryFilepath(defaultFilename);
|
||||
|
||||
let filepath = defaultFilepath;
|
||||
if (existsSync(filepath.fsPath)) {
|
||||
const newFilepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
defaultFilename
|
||||
);
|
||||
|
||||
if (newFilepath === undefined) {
|
||||
return;
|
||||
}
|
||||
filepath = Uri.file(newFilepath);
|
||||
}
|
||||
await writeTemplate(templateSnippet, filepath);
|
||||
}
|
||||
|
||||
async function createNoteFromTemplate(
|
||||
templateFilename?: string
|
||||
): Promise<void> {
|
||||
const selectedTemplate = await askUserForTemplate();
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
templateFilename = selectedTemplate as string;
|
||||
const templateUri = Uri.joinPath(templatesDir, templateFilename);
|
||||
const templateText = await workspace.fs
|
||||
.readFile(templateUri)
|
||||
.then(bytes => bytes.toString());
|
||||
|
||||
let resolvedValues, templateSnippet;
|
||||
try {
|
||||
[resolvedValues, templateSnippet] = await resolveFoamTemplateVariables(
|
||||
templateText
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
const defaultFilename = `${defaultSlug}.md`;
|
||||
const defaultFilepath = currentDirectoryFilepath(defaultFilename);
|
||||
|
||||
const filepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
defaultFilename
|
||||
);
|
||||
const snippet = new SnippetString(templateText.toString());
|
||||
const filenameURI = URI.file(filename);
|
||||
await workspace.fs.writeFile(filenameURI, new TextEncoder().encode(''));
|
||||
await focusNote(filenameURI, true);
|
||||
await window.activeTextEditor.insertSnippet(snippet);
|
||||
|
||||
if (filepath === undefined) {
|
||||
return;
|
||||
}
|
||||
const filepathURI = Uri.file(filepath);
|
||||
await writeTemplate(templateSnippet, filepathURI);
|
||||
}
|
||||
|
||||
async function createNewTemplate(): Promise<void> {
|
||||
const defaultFileName = 'new-template.md';
|
||||
const defaultTemplate = URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
defaultFileName
|
||||
);
|
||||
const defaultFilename = 'new-template.md';
|
||||
const defaultTemplate = Uri.joinPath(templatesDir, defaultFilename);
|
||||
const filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: defaultTemplate.fsPath,
|
||||
valueSelection: [
|
||||
defaultTemplate.fsPath.length - defaultFileName.length,
|
||||
defaultTemplate.fsPath.length - defaultFilename.length,
|
||||
defaultTemplate.fsPath.length - 3,
|
||||
],
|
||||
validateInput: value =>
|
||||
@@ -125,7 +315,7 @@ async function createNewTemplate(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const filenameURI = URI.file(filename);
|
||||
const filenameURI = Uri.file(filename);
|
||||
await workspace.fs.writeFile(
|
||||
filenameURI,
|
||||
new TextEncoder().encode(templateContent)
|
||||
@@ -141,6 +331,12 @@ const feature: FoamFeature = {
|
||||
createNoteFromTemplate
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-default-template',
|
||||
createNoteFromDefaultTemplate
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-new-template',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { Foam, Logger } from 'foam-core';
|
||||
import { Foam, Logger, URI } from 'foam-core';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../settings';
|
||||
import { isSome } from '../utils';
|
||||
@@ -82,11 +82,19 @@ function generateGraphData(foam: Foam) {
|
||||
title: cutTitle(title),
|
||||
};
|
||||
});
|
||||
foam.workspace.getAllConnections().forEach(c => {
|
||||
foam.graph.getAllConnections().forEach(c => {
|
||||
graph.edges.add({
|
||||
source: c.source.path,
|
||||
target: c.target.path,
|
||||
});
|
||||
if (URI.isPlaceholder(c.target)) {
|
||||
graph.nodes[c.target.path] = {
|
||||
id: c.target.path,
|
||||
type: 'placeholder',
|
||||
uri: c.target,
|
||||
title: c.target.path,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, FoamWorkspace, NoteParser, uris } from 'foam-core';
|
||||
import { Foam, FoamWorkspace, ResourceParser, URI } from 'foam-core';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
@@ -25,7 +25,7 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
|
||||
const updateDecorations = (
|
||||
areDecorationsEnabled: () => boolean,
|
||||
parser: NoteParser,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (!editor || !areDecorationsEnabled()) {
|
||||
@@ -36,7 +36,7 @@ const updateDecorations = (
|
||||
let placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = workspace.resolveLink(note, link);
|
||||
if (uris.isPlaceholder(linkUri)) {
|
||||
if (URI.isPlaceholder(linkUri)) {
|
||||
placeholderRanges.push(link.range);
|
||||
} else {
|
||||
linkRanges.push(link.range);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamWorkspace, createMarkdownParser, uris } from 'foam-core';
|
||||
import { FoamWorkspace, createMarkdownParser, URI } from 'foam-core';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
createTestWorkspace,
|
||||
showInEditor,
|
||||
} from '../test/test-utils';
|
||||
import { LinkProvider } from './document-link-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
describe('Document links provider', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
@@ -25,9 +27,8 @@ describe('Document links provider', () => {
|
||||
});
|
||||
|
||||
it('should not return any link for empty documents', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const { uri, content } = await createFile('');
|
||||
ws.set(parser.parse(uri, content)).resolveLinks();
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
@@ -37,11 +38,10 @@ describe('Document links provider', () => {
|
||||
});
|
||||
|
||||
it('should not return any link for documents without links', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const { uri, content } = await createFile(
|
||||
'This is some content without links'
|
||||
);
|
||||
ws.set(parser.parse(uri, content)).resolveLinks();
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
@@ -51,33 +51,31 @@ describe('Document links provider', () => {
|
||||
});
|
||||
|
||||
it('should support wikilinks', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(`this is a link to [[${fileB.name}]].`);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileB.uri));
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 27));
|
||||
});
|
||||
|
||||
it('should support regular links', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(
|
||||
`this is a link to [a file](./${fileB.base}).`
|
||||
);
|
||||
ws.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content))
|
||||
.resolveLinks();
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
@@ -89,9 +87,8 @@ describe('Document links provider', () => {
|
||||
});
|
||||
|
||||
it('should support placeholders', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
ws.set(parser.parse(fileA.uri, fileA.content)).resolveLinks();
|
||||
const ws = new FoamWorkspace().set(parser.parse(fileA.uri, fileA.content));
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
@@ -99,7 +96,7 @@ describe('Document links provider', () => {
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(uris.placeholderUri('a placeholder'))
|
||||
OPEN_COMMAND.asURI(toVsCodeUri(URI.placeholder('a placeholder')))
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 35));
|
||||
});
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, FoamWorkspace, NoteParser, uris } from 'foam-core';
|
||||
import { Foam, FoamWorkspace, ResourceParser, URI } from 'foam-core';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNote, mdDocSelector } from '../utils';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeRange } from '../utils/vsc-utils';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
if (!getFoamVsCodeConfig('links.navigation.enable')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
@@ -22,28 +27,28 @@ const feature: FoamFeature = {
|
||||
};
|
||||
|
||||
export class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
constructor(private workspace: FoamWorkspace, private parser: NoteParser) {}
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private parser: ResourceParser
|
||||
) {}
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
|
||||
if (isNote(resource)) {
|
||||
return resource.links.map(link => {
|
||||
const target = this.workspace.resolveLink(resource, link);
|
||||
const command = OPEN_COMMAND.asURI(target);
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
toVsCodeRange(link.range),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = uris.isPlaceholder(target)
|
||||
? `Create note for '${target.path}'`
|
||||
: `Go to ${target.fsPath}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
return resource.links.map(link => {
|
||||
const target = this.workspace.resolveLink(resource, link);
|
||||
const command = OPEN_COMMAND.asURI(toVsCodeUri(target));
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
toVsCodeRange(link.range),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = URI.isPlaceholder(target)
|
||||
? `Create note for '${target.path}'`
|
||||
: `Go to ${URI.toFsPath(target)}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import backlinks from './backlinks';
|
||||
import utilityCommands from './utility-commands';
|
||||
import documentLinkProvider from './document-link-provider';
|
||||
import previewNavigation from './preview-navigation';
|
||||
import completionProvider from './link-completion';
|
||||
import linkDecorations from './document-decorator';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
@@ -33,4 +34,5 @@ export const features: FoamFeature[] = [
|
||||
utilityCommands,
|
||||
linkDecorations,
|
||||
previewNavigation,
|
||||
completionProvider,
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user