Compare commits

...

31 Commits

Author SHA1 Message Date
Riccardo Ferretti
af0c2bbaa3 v0.13.3 2021-05-11 21:40:56 +02:00
Riccardo Ferretti
700bfc1b26 addressing windows issue with provider matching (related to #617) 2021-05-11 21:25:25 +02:00
Riccardo Ferretti
a6d5c04453 improve bootstrap performance
we wait to create the graph, as changes to the workspace will cause it to be recomputed.
so, first load all resources from the initial providers, then compute the graph.
2021-05-11 21:22:19 +02:00
Riccardo Ferretti
b0c42cead2 setting logging level to error for all tests 2021-05-11 21:12:38 +02:00
Riccardo Ferretti
6c643adb9d Prepare for 0.13.3 2021-05-09 23:13:27 +02:00
Riccardo Ferretti
ca7fdefaae improvemets in matcher and linting 2021-05-09 23:13:27 +02:00
Riccardo Ferretti
149d5f5a7c fix #617 - fixed error in file matching in MarkdownProvider 2021-05-09 22:50:42 +02:00
Michael Overmeyer
be80857fd1 Handle users cancelling "Create New Note" commands (#623) 2021-05-09 22:50:18 +02:00
Michael Overmeyer
611fa7359d Completely ignore unknown Foam variables (#622)
If we don't know it we shouldn't touch it
Previously it replaced the variable with the variables name
2021-05-09 18:23:06 +02:00
Riccardo Ferretti
08b7e7a231 fixed isMarkdown function to check the .md extension (related to #617) 2021-05-07 23:53:14 +02:00
Riccardo Ferretti
0a259168c7 fix #618: properly printing file name 2021-05-07 22:58:44 +02:00
Riccardo Ferretti
f3d0569c76 fixed Barabazs contributor name 2021-05-06 12:26:22 +02:00
Riccardo Ferretti
502129d5ac v0.13.2 2021-05-06 10:10:51 +02:00
Riccardo Ferretti
d29bf16db1 prepare 0.13.2 2021-05-06 10:10:22 +02:00
Riccardo Ferretti
d228c7cb18 fixed test 2021-05-06 10:04:00 +02:00
Riccardo Ferretti
78078cf338 improved foam settings 2021-05-06 10:04:00 +02:00
Riccardo Ferretti
fe65883bc5 fix #600 - clicking on notes in placeholder and orphan panels now navigates to them 2021-05-06 10:04:00 +02:00
allcontributors[bot]
20b5261c5c docs: add EngincanV as a contributor (#615)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-05-06 10:02:12 +02:00
Engincan VESKE
f91cfe5d0d Update capture-notes-with-shortcuts-and-github-actions.md (#613) 2021-05-06 10:01:42 +02:00
Michael Overmeyer
1ab9cc5f4a Add streamline "Create new note" command (#601)
* Add new `Create New Note` command
It is the streamlined counterpart to `Create New Note From Template`

* Simplify the variable Regex
\W is equivalent to [^A-Za-z0-9_]
2021-05-03 13:48:14 +02:00
allcontributors[bot]
02ff681700 docs: add Barabazs as a contributor (#610)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-05-03 13:20:26 +02:00
Barabas
2d9c3be0e6 Add support for stylable tags (#598) 2021-05-03 13:19:37 +02:00
Robin King
78cf602347 fix: (#592) extra autocomplete hints outside wiki-link brackets (#596) 2021-05-03 13:02:28 +02:00
dependabot[bot]
898c7b4387 Bump rexml from 3.2.4 to 3.2.5 in /docs (#607)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.4...v3.2.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 11:40:40 +02:00
Riccardo
7412d518d7 Spike on FoamWorkspace and DataStore refactor (#593)
* extracted graph from workspace

* refactored datastore

* dataviz to use URI for placeholder detection

* graph uses uris, not resources

* adding placeholder in graph

* link completion to use graph

* API v1 - Resource refactoring (#595)

* aside: tweaked jest extension configuration

* added provider for markdown

* removed file check in URI

the problem is that it makes the URI dependent on the disk, which makes testing harder.
The change was originally introduced to prevent Foam from treating directories ending in .md as markdown files, but the check needs to probably happen somewhere else, e.g. in `FileDataStore.list` - or directories should be expressed with a trailing slash (to check whether that breaks the URI convention)

* Various changes

- `resolveLink` now delegates to providers
- added `read` method in providers and `FoamWorkspace`
- improved `Matcher` API
- updated tests to use workspace with providers
- delegating more to workspace now it can read files (simplifies wiring and exposed API surface)
- provider init returns a promise, so it can be awaited on
- `IDataStore` now has `list` method, to encapsulate all access to FS

* improved windows support in URI and matcher

* improved grouped resources interface

* added readAsMarkdown in provider, useful for tooltip generation with preview in vscode
2021-04-30 16:50:16 +02:00
allcontributors[bot]
e6030ac562 docs: add daniel-vera-g as a contributor (#603)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-04-25 23:58:47 +02:00
Daniel VG
d6d958bc92 Docs: Fix markdown formatting issues (#599)
* Fix: Right links and formatting

* Docs: Run markdownlint to automatically fix minor formatting errors

* Style: Format with markdownlint and not prettier
2021-04-25 23:58:02 +02:00
Riccardo Ferretti
fd7a24c5fc v0.13.1 2021-04-21 21:10:48 +02:00
Riccardo Ferretti
41b3c6fbfb Prepare 0.13.1 2021-04-21 21:10:15 +02:00
Robin King
84b2ab6e42 fix:(#591) 'foam-vscode.open-daily-note' error on Windows (#594)
* when calling URI.file more than two time on windows
* a extra slash('/') at path's beginning may cause some problems
* so add a condition to solve it
2021-04-21 21:04:04 +02:00
dependabot[bot]
6cf184ba23 Bump ssri from 6.0.1 to 6.0.2 (#590)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 12:15:02 +02:00
106 changed files with 1855 additions and 1518 deletions

View File

@@ -661,6 +661,33 @@
"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
View File

@@ -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"
}

View File

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

View File

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

View File

@@ -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"

View File

@@ -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>.

View File

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

View File

@@ -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.

View File

@@ -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.

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -28,11 +28,11 @@ To enable the feature:
```
{
"experimental": {
"localPlugins": {
"enabled": true
}
}
"experimental": {
"localPlugins": {
"enabled": true
}
}
}
```

View File

@@ -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`.
![Find the foam log](../assets/images/foam-log.png)
## 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`.

View File

@@ -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:
![Style node by type](../assets/images/style-node-by-type.png)

View File

@@ -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"

View File

@@ -1,20 +1,35 @@
# 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
![Create new template GIF](../assets/images/create-new-template.gif)
_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)
![Create new note from template GIF](../assets/images/create-new-note-from-template.gif)
_Theme: Ayu Light_
### Variables
## 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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -198,6 +198,9 @@ If that sounds like something you're interested in, I'd love to have you along o
</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>

View File

@@ -13,6 +13,7 @@ Present: @jevakallio, @riccardoferretti
- Land work to master
- Create a foam-core package
-
### Open questions

View File

@@ -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"

View File

@@ -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.

View File

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

View File

@@ -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).

View File

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

View File

@@ -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"

View File

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

View File

@@ -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.

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)

View File

@@ -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.)

View File

@@ -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: `![My Diagram](my-diagram.drawio.svg)`
[//begin]: # "Autogenerated link references for markdown compatibility"
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
[//end]: # "Autogenerated link references"

View File

@@ -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.

View File

@@ -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"

View File

@@ -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"

View File

@@ -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).

View File

@@ -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.

View File

@@ -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]]

View File

@@ -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"

View File

@@ -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.

View File

@@ -17,4 +17,3 @@ Also happens to sound quite a lot like Home. Funny, that.
## Bubble
Individual Foam note, written in Markdown.

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.13.0"
"version": "0.13.3"
}

View File

@@ -40,5 +40,6 @@
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}
},
"dependencies": {}
}

View File

@@ -1,7 +1,7 @@
{
"name": "foam-core",
"repository": "https://github.com/foambubble/foam",
"version": "0.13.0",
"version": "0.13.3",
"license": "MIT",
"files": [
"dist"

View File

@@ -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 { URI } from './model/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 (URI.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);
isSome(content) && workspace.set(await parser.parse(uri, content));
}),
dataStore.onDidCreate(async uri => {
const content = await dataStore.read(uri);
isSome(content) && 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;
};

View File

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

View File

@@ -70,6 +70,6 @@ const parseConfig = (path: URI) => {
try {
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));
}
};

View File

@@ -1,37 +1,37 @@
import {
Resource,
Attachment,
Placeholder,
Note,
NoteLink,
isNote,
ResourceLink,
NoteLinkDefinition,
isPlaceholder,
isAttachment,
getTitle,
NoteParser,
ResourceParser,
} from './model/note';
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 { FoamGraph } from '../src/model/graph';
import { URI } from './model/uri';
export { Position } from './model/position';
export { Range } from './model/range';
export { IDataStore, FileDataStore };
export { IDataStore, FileDataStore, Matcher, IMatcher };
export { ILogger };
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
export { Event, Emitter } from './common/event';
export { FoamConfig };
export { ResourceProvider } from './model/provider';
export { IDisposable, isDisposable };
export {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
createMarkdownParser,
MarkdownResourceProvider,
} from './markdown-provider';
export {
@@ -51,27 +51,23 @@ export { bootstrap } from './bootstrap';
export {
Resource,
Attachment,
Placeholder,
Note,
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;
}

View File

@@ -1,5 +1,5 @@
import GithubSlugger from 'github-slugger';
import { Note } from '../model/note';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import {
createMarkdownReferences,
@@ -20,7 +20,7 @@ export interface TextEdit {
}
export const generateLinkReferences = (
note: Note,
note: Resource,
workspace: FoamWorkspace,
includeExtensions: boolean
): TextEdit | null => {
@@ -75,7 +75,7 @@ export const generateLinkReferences = (
}
};
export const generateHeading = (note: Note): TextEdit | null => {
export const generateHeading = (note: Resource): TextEdit | null => {
if (!note) {
return null;
}

View File

@@ -10,10 +10,10 @@ import detectNewline from 'detect-newline';
import os from 'os';
import {
NoteLinkDefinition,
Note,
NoteParser,
isWikilink,
getTitle,
Resource,
ResourceLink,
WikiLink,
ResourceParser,
} from './model/note';
import { Position } from './model/position';
import { Range } from './model/range';
@@ -28,6 +28,117 @@ import { ParserPlugin } from './plugins';
import { Logger } from './utils/log';
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
@@ -59,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;
}
@@ -69,7 +180,7 @@ const titlePlugin: ParserPlugin = {
note.title = props.title ?? note.title;
},
onDidVisitTree: (tree, note) => {
if (note.title == null) {
if (note.title === '') {
note.title = URI.getBasename(note.uri);
}
},
@@ -133,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'])
@@ -155,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 {
@@ -169,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: [],
@@ -311,7 +424,7 @@ export function createMarkdownReferences(
: 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();
@@ -338,3 +451,7 @@ const astPositionToFoamRange = (pos: AstPosition): Range =>
pos.end.line - 1,
pos.end.column - 1
);
const isWikilink = (link: ResourceLink): link is WikiLink => {
return link.type === 'wikilink';
};

View 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);

View File

@@ -23,7 +23,7 @@ export interface DirectLink {
range: Range;
}
export type NoteLink = WikiLink | DirectLink;
export type ResourceLink = WikiLink | DirectLink;
export interface NoteLinkDefinition {
label: string;
@@ -32,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 ?? URI.getBasename(resource.uri)
: 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'
);
}
}

View 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;
}

View File

@@ -65,4 +65,8 @@ export abstract class Range {
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;
}
}

View File

@@ -2,7 +2,6 @@
// See LICENSE for details
import * as paths from 'path';
import { statSync } from 'fs';
import { CharCode } from '../common/charCode';
import { isWindows } from '../common/platform';
@@ -103,7 +102,11 @@ export abstract class URI {
// on other systems bwd-slashes are valid
// filename character, eg /f\oo/ba\r.txt
if (isWindows) {
path = `/${path.replace(/\\/g, _slash)}`;
if (path.startsWith(_slash)) {
path = `${path.replace(/\\/g, _slash)}`;
} else {
path = `/${path.replace(/\\/g, _slash)}`;
}
}
// check for authority as used in UNC shares
@@ -246,7 +249,7 @@ export abstract class URI {
);
}
static isMarkdownFile(uri: URI): boolean {
return uri.path.endsWith('md') && statSync(URI.toFsPath(uri)).isFile();
return uri.path.endsWith('.md');
}
}

View File

@@ -1,18 +1,10 @@
import { diff } from 'fast-array-diff';
import { isEqual } from 'lodash';
import * as path from 'path';
import { Resource, NoteLink, Note } from './note';
import { Range } from './range';
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
@@ -35,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>();
@@ -48,6 +37,8 @@ export class FoamWorkspace implements IDisposable {
onDidUpdate = this.onDidUpdateEmitter.event;
onDidDelete = this.onDidDeleteEmitter.event;
private providers: ResourceProvider[] = [];
/**
* Resources by key / slug
*/
@@ -56,199 +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 = URI.resolve(definitionUri, note.uri);
targetUri =
FoamWorkspace.find(workspace, definedUri, note.uri)?.uri ??
URI.placeholder(definedUri.path);
} else {
targetUri =
FoamWorkspace.find(workspace, link.slug, note.uri)?.uri ??
URI.placeholder(link.slug);
}
break;
case 'link':
targetUri =
FoamWorkspace.find(workspace, link.target, note.uri)?.uri ??
URI.placeholder(URI.resolve(link.target, note.uri).path);
break;
}
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 {
@@ -256,44 +101,28 @@ 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)) {
@@ -301,185 +130,35 @@ export class FoamWorkspace implements IDisposable {
}
const relativePath = resourceId as string;
const targetUri = URI.computeRelativeURI(reference, relativePath);
return (
workspace.resources[uriToResourceId(targetUri)] ??
workspace.placeholders[pathToPlaceholderId(resourceId as string)]
);
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
) {
if (URI.isPlaceholder(target)) {
// we can only add placeholders when links are being resolved
workspace = FoamWorkspace.set(workspace, {
type: 'placeholder',
uri: target,
});
}
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) =>
!URI.isEqual(source, c.source) || !URI.isEqual(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 (URI.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) =>
URI.isEqual(a.source, b.source) &&
URI.isEqual(a.target, b.target) &&
isSameLink(a.link, b.link);
const isSameLink = (a: NoteLink, b: NoteLink) =>
a.type === b.type && Range.isEqual(a.range, b.range);

View File

@@ -2,7 +2,7 @@ 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';
@@ -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 {

View File

@@ -1,141 +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 '../model/uri';
import { FoamConfig } from '../config';
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.
*/
onDidCreate: Event<URI>;
/**
* An event which fires on file change.
*/
onDidChange: Event<URI>;
/**
* An event which fires on file deletion.
*/
onDidDelete: Event<URI>;
}
/**
* Represents a source of files and content
*/
export interface IDataStore {
/**
* List the files available in the store
*/
listFiles: () => Promise<URI[]>;
/**
* Read the content of the file from the store
* Filters the given list of URIs, keepin only the ones that
* are matched by this Matcher
*
* Returns `null` in case of errors while reading
* @param files the URIs to check
*/
read: (uri: URI) => Promise<string | null>;
match(files: URI[]): URI[];
/**
* Returns whether the given URI is a match in
* this data store
* Returns whether this URI is matched by this Matcher
*
* @param uri the URI to check
*/
isMatch: (uri: URI) => boolean;
isMatch(uri: URI): boolean;
/**
* An event which fires on file creation.
* The include globs
*/
onDidCreate: Event<URI>;
include: string[];
/**
* An event which fires on file change.
* The exclude lobs
*/
onDidChange: Event<URI>;
/**
* An event which fires on file deletion.
*/
onDidDelete: Event<URI>;
exclude: string[];
}
/**
* File system based data store
* 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 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;
export const toMatcherPathFormat = isWindows
? (uri: URI) => URI.toFsPath(uri).replace(/\\/g, '/')
: (uri: URI) => URI.toFsPath(uri);
private _folders: readonly string[];
private _includeGlobs: string[] = [];
private _ignoreGlobs: string[] = [];
private _disposables: IDisposable[] = [];
export const toFsPath = isWindows
? (path: string): string => path.replace(/\//g, '\\')
: (path: string): string => path;
constructor(config: FoamConfig, watcher?: IWatcher) {
this._folders = config.workspaceFolders.map(f =>
URI.toFsPath(f).replace(/\\/g, '/')
);
Logger.info('Workspace folders: ', this._folders);
export class Matcher implements IMatcher {
public readonly folders: string[];
public readonly include: string[] = [];
public readonly exclude: string[] = [];
this._folders.forEach(folder => {
constructor(
baseFolders: URI[],
include: string[] = ['**/*'],
exclude: string[] = []
) {
this.folders = baseFolders.map(toMatcherPathFormat);
Logger.info('Workspace folders: ', this.folders);
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 => URI.toFsPath(f)),
this._includeGlobs,
this.include,
{
ignore: this._ignoreGlobs,
ignore: this.exclude,
nocase: true,
format: toFsPath,
}
);
return matches.map(URI.file);
@@ -144,17 +90,33 @@ 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) {
@@ -167,18 +129,14 @@ export class FileDataStore implements IDataStore, IDisposable {
return null;
}
}
dispose() {
this._disposables.forEach(d => d.dispose());
}
}
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;
};

View File

@@ -1,3 +1,3 @@
# Roam Document
[[Second Roam Document]]
[[Second Roam Document]]

View File

@@ -1 +1 @@
# Second Roam Document
# Second Roam Document

View File

@@ -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"

View File

@@ -1,5 +1,8 @@
import path from 'path';
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
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';
@@ -19,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: {
@@ -33,7 +47,7 @@ export const createTestNote = (params: {
links?: Array<{ slug: string } | { to: string }>;
text?: string;
root?: URI;
}): Note => {
}): Resource => {
const root = params.root ?? URI.file('/');
return {
uri: URI.resolve(params.uri, root),

View File

@@ -1,72 +1,93 @@
import { createConfigFromObject } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/model/uri';
import { FileDataStore } from '../src';
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(URI: URI[]) {
return new Set(URI.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);
});
});

View File

@@ -2,28 +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 { 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 { 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 => {
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => URI.getBasename(res.uri) === slug) as Note;
.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;
});

View File

@@ -2,29 +2,37 @@ import * as path from 'path';
import { generateLinkReferences } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Note, Range } from '../../src';
import { FileDataStore } from '../../src/services/datastore';
import { FileDataStore, Matcher } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { FoamWorkspace } from '../../src/model/workspace';
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 => {
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => URI.getBasename(res.uri) === slug) as Note;
.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', () => {
@@ -107,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);
}

View File

@@ -7,7 +7,8 @@ import { ParserPlugin } from '../src/plugins';
import { Logger } from '../src/utils/log';
import { uriToSlug } from '../src/utils/slug';
import { URI } from '../src/model/uri';
import { FoamWorkspace } from '../src/model/workspace';
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)

View File

@@ -1,6 +1,9 @@
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');

View File

@@ -1,6 +1,7 @@
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 { createTestNote, createTestWorkspace } from './core.test';
import { URI } from '../src/model/uri';
Logger.setLevel('error');
@@ -25,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' }));
@@ -38,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: URI.placeholder('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');
@@ -66,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',
@@ -75,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,
@@ -100,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();
});
});
@@ -135,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()
@@ -162,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',
@@ -181,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()
@@ -193,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' }],
@@ -205,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' }),
@@ -224,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,
@@ -248,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,
]);
@@ -271,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,
]);
});
@@ -296,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,
]);
});
@@ -318,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,
]);
});
@@ -349,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,
@@ -386,19 +408,20 @@ 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: 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: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'link' }),
@@ -406,21 +429,22 @@ describe('Placeholders', () => {
});
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: 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' }],
@@ -433,16 +457,17 @@ 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: 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: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'wikilink' }),
@@ -463,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({
@@ -480,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()
@@ -504,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(URI.placeholder('page-b')).type).toEqual('placeholder');
expect(graph2.contains(URI.placeholder('page-b'))).toBeTruthy();
});
it('Adding note should replace placeholder for wikilinks', () => {
@@ -526,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([
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
expect(ws.get(URI.placeholder('page-b')).type).toEqual('placeholder');
expect(graph.contains(URI.placeholder('page-b'))).toBeTruthy();
// add note-b
const noteB = createTestNote({
@@ -540,7 +575,7 @@ describe('Updating workspace happy path', () => {
});
ws.set(noteB);
ws.resolveLinks();
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
@@ -554,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(URI.placeholder('/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', () => {
@@ -578,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([
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(ws.get(URI.placeholder('/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({
@@ -594,7 +631,7 @@ describe('Updating workspace happy path', () => {
});
ws.set(noteB);
ws.resolveLinks();
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
@@ -605,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(URI.placeholder('/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(URI.placeholder('/path/to/another/page-b.md'))
).toThrow();
expect(
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
});
});
@@ -637,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({
@@ -654,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', () => {
@@ -673,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(URI.placeholder('page-b')).type).toEqual('placeholder');
ws.dispose();
graph.dispose();
});
it('Adding note should replace placeholder for wikilinks', () => {
@@ -695,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([
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
expect(ws.get(URI.placeholder('page-b')).type).toEqual('placeholder');
// add note-b
const noteB = createTestNote({
@@ -713,6 +757,7 @@ describe('Monitoring of workspace state', () => {
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', () => {
@@ -723,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(URI.placeholder('/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', () => {
@@ -747,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([
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(ws.get(URI.placeholder('/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({
@@ -767,6 +815,7 @@ describe('Monitoring of workspace state', () => {
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', () => {
@@ -774,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(URI.placeholder('/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({
@@ -786,9 +836,10 @@ describe('Monitoring of workspace state', () => {
links: [],
});
ws.set(noteABis);
expect(() =>
ws.get(URI.placeholder('/path/to/another/page-b.md'))
).toThrow();
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
ws.dispose();
graph.dispose();
});
});

View File

@@ -4,6 +4,33 @@ 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:

View File

@@ -5,9 +5,7 @@
[![Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
> ⚠️ 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.

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.13.0",
"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"
@@ -390,7 +395,7 @@
},
"dependencies": {
"dateformat": "^3.0.3",
"foam-core": "^0.13.0",
"foam-core": "^0.13.3",
"gray-matter": "^4.0.2",
"markdown-it-regex": "^0.2.0",
"micromatch": "^4.0.2",

View File

@@ -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);

View File

@@ -1,10 +1,11 @@
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';
@@ -19,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,
@@ -48,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();
@@ -100,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 () => {

View File

@@ -3,14 +3,13 @@ import { groupBy } from 'lodash';
import {
Foam,
FoamWorkspace,
IDataStore,
isNote,
NoteLink,
FoamGraph,
ResourceLink,
Resource,
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';
@@ -21,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;
@@ -41,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;
@@ -51,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();
@@ -68,9 +58,6 @@ 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
@@ -80,7 +67,7 @@ export class BacklinksTreeDataProvider
.map(async link => {
const item = new BacklinkTreeItem(resource, link);
const lines = (
(await this.dataStore.read(resource.uri)) ?? ''
(await this.workspace.read(resource.uri)) ?? ''
).split('\n');
if (link.range.start.line < lines.length) {
const line = lines[link.range.start.line];
@@ -100,29 +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 => URI.isEqual(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}`;
@@ -139,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,

View File

@@ -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();

View File

@@ -1,6 +1,7 @@
import { window } from 'vscode';
import { SnippetString, window } from 'vscode';
import {
resolveFoamVariables,
resolveFoamTemplateVariables,
substituteFoamVariables,
} from './create-from-template';
@@ -11,7 +12,7 @@ describe('substituteFoamVariables', () => {
# \${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
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
`;
const givenValues = new Map<string, string>();
@@ -87,3 +88,36 @@ describe('resolveFoamVariables', () => {
);
});
});
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);
});
});

View File

@@ -17,6 +17,21 @@ const templatesDir = Uri.joinPath(
'.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.
@@ -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[]> {
@@ -60,16 +75,21 @@ function findFoamVariables(templateText: string): string[] {
output.push(matches[1] || matches[2]);
}
const uniqVariables = [...new Set(output)];
return uniqVariables;
const knownVariables = uniqVariables.filter(x => knownFoamVariables.has(x));
return knownVariables;
}
function resolveFoamTitle() {
return window.showInputBox({
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>>();
@@ -119,7 +139,7 @@ export function substituteFoamVariables(
const regex = new RegExp(
// Matches a limited subset of the the TextMate variable syntax:
// ${VARIABLE} OR $VARIABLE
`\\\${${variable}}|\\$${variable}([^A-Za-z0-9_]|$)`,
`\\\${${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
@@ -133,27 +153,39 @@ export function substituteFoamVariables(
return templateText;
}
async function createNoteFromTemplate(): Promise<void> {
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 templateText = await workspace.fs.readFile(
Uri.joinPath(templatesDir, selectedTemplate)
);
async function askUserForFilepathConfirmation(
defaultFilepath: Uri,
defaultFilename: string
) {
return await window.showInputBox({
prompt: `Enter the filename for the new note`,
value: defaultFilepath.fsPath,
valueSelection: [
defaultFilepath.fsPath.length - defaultFilename.length,
defaultFilepath.fsPath.length - 3,
],
validateInput: value =>
value.trim().length === 0
? 'Please enter a value'
: existsSync(value)
? 'File already exists'
: undefined,
});
}
export async function resolveFoamTemplateVariables(
templateText: string
): Promise<[Map<string, string>, SnippetString]> {
const givenValues = new Map<string, string>();
const variables = findFoamVariables(templateText.toString());
@@ -163,47 +195,113 @@ async function createNoteFromTemplate(): Promise<void> {
resolvedValues
);
const snippet = new SnippetString(subbedText);
return [resolvedValues, snippet];
}
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
const defaultFileName = `${defaultSlug}.md`;
const defaultDir = Uri.joinPath(currentDir, defaultFileName);
const filename = await window.showInputBox({
prompt: `Enter the filename for the new note`,
value: defaultDir.fsPath,
valueSelection: [
defaultDir.fsPath.length - defaultFileName.length,
defaultDir.fsPath.length - 3,
],
validateInput: value =>
value.trim().length === 0
? 'Please enter a value'
: existsSync(value)
? 'File already exists'
: undefined,
});
if (filename === undefined) {
return;
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 filenameURI = Uri.file(filename);
await workspace.fs.writeFile(filenameURI, new TextEncoder().encode(''));
await focusNote(filenameURI, true);
await window.activeTextEditor.insertSnippet(snippet);
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
);
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 =>
@@ -233,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',

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
import * as vscode from 'vscode';
import { Foam, FoamWorkspace, NoteParser, URI } 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()) {

View File

@@ -4,6 +4,7 @@ import {
cleanWorkspace,
closeEditors,
createFile,
createTestWorkspace,
showInEditor,
} from '../test/test-utils';
import { LinkProvider } from './document-link-provider';
@@ -26,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);
@@ -38,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);
@@ -52,14 +51,13 @@ 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);
@@ -71,14 +69,13 @@ describe('Document links provider', () => {
});
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);
@@ -90,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);

View File

@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { Foam, FoamWorkspace, NoteParser, URI } 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, toVsCodeUri } from '../utils/vsc-utils';
import { getFoamVsCodeConfig } from '../services/config';
@@ -27,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(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;
});
}
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;
});
}
}

View File

@@ -12,7 +12,7 @@ import {
generateLinkReferences,
generateHeading,
Foam,
Note,
Resource,
Range,
URI,
} from 'foam-core';
@@ -21,7 +21,6 @@ import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting,
} from '../settings';
import { isNote } from '../utils';
import { toVsCodePosition, toVsCodeRange } from '../utils/vsc-utils';
const feature: FoamFeature = {
@@ -71,7 +70,9 @@ async function janitor(foam: Foam) {
}
async function runJanitor(foam: Foam) {
const notes: Note[] = foam.workspace.list().filter(isNote);
const notes: Resource[] = foam.workspace
.list()
.filter(r => URI.isMarkdownFile(r.uri));
let updatedHeadingCount = 0;
let updatedDefinitionListCount = 0;

View File

@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { FoamWorkspace } from 'foam-core';
import { FoamGraph, FoamWorkspace } from 'foam-core';
import {
cleanWorkspace,
closeEditors,
@@ -30,14 +30,16 @@ describe('Link Completion', () => {
uri: 'path/to/file.md',
links: [{ slug: 'placeholder text' }],
})
)
.resolveLinks();
);
const graph = FoamGraph.fromWorkspace(ws);
beforeAll(async () => {
await cleanWorkspace();
});
afterAll(async () => {
ws.dispose();
graph.dispose();
await cleanWorkspace();
});
@@ -48,7 +50,7 @@ describe('Link Completion', () => {
it('should not return any link for empty documents', async () => {
const { uri } = await createFile('');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws);
const provider = new CompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
@@ -59,15 +61,28 @@ describe('Link Completion', () => {
});
it('should return notes and placeholders', async () => {
const { uri } = await createFile('[[');
const { uri } = await createFile('[[file]] [[');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws);
const provider = new CompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 2)
new vscode.Position(0, 11)
);
expect(links.items.length).toEqual(4);
});
it('should not return link outside the wiki-link brackets', async () => {
const { uri } = await createFile('[[file]] then');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 12)
);
expect(links).toBeNull();
});
});

View File

@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { Foam, FoamWorkspace, URI, isNote } from 'foam-core';
import { Foam, FoamWorkspace, URI, FoamGraph } from 'foam-core';
import { FoamFeature } from '../types';
import { getNoteTooltip, mdDocSelector } from '../utils';
import { toVsCodeUri } from '../utils/vsc-utils';
@@ -13,7 +13,7 @@ const feature: FoamFeature = {
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
mdDocSelector,
new CompletionProvider(foam.workspace),
new CompletionProvider(foam.workspace, foam.graph),
'['
)
);
@@ -22,7 +22,7 @@ const feature: FoamFeature = {
export class CompletionProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
constructor(private ws: FoamWorkspace) {}
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
provideCompletionItems(
document: vscode.TextDocument,
@@ -32,34 +32,34 @@ export class CompletionProvider
.lineAt(position)
.text.substr(0, position.character);
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
// eslint-disable-next-line no-useless-escape
const requiresAutocomplete = cursorPrefix.match(/\[\[([^\[\]]*?)/);
const requiresAutocomplete = cursorPrefix.match(/\[\[[^\[\]]*(?!.*\]\])/);
if (!requiresAutocomplete) {
return null;
}
const results = this.ws.list().map(resource => {
const uri = resource.uri;
if (URI.isPlaceholder(uri)) {
return new vscode.CompletionItem(
uri.path,
vscode.CompletionItemKind.Interface
);
}
const resources = this.ws.list().map(resource => {
const item = new vscode.CompletionItem(
vscode.workspace.asRelativePath(toVsCodeUri(resource.uri)),
vscode.CompletionItemKind.File
);
item.insertText = URI.getBasename(resource.uri);
item.documentation =
isNote(resource) && getNoteTooltip(resource.source.text);
item.documentation = getNoteTooltip(resource.source.text);
return item;
});
return new vscode.CompletionList(results);
const placeholders = Object.values(this.graph.placeholders).map(uri => {
return new vscode.CompletionItem(
uri.path,
vscode.CompletionItemKind.Interface
);
});
return new vscode.CompletionList([...resources, ...placeholders]);
}
}

View File

@@ -1,4 +1,4 @@
import { Foam } from 'foam-core';
import { Foam, URI } from 'foam-core';
import { ExtensionContext, commands, window } from 'vscode';
import { FoamFeature } from '../types';
import { focusNote } from '../utils';
@@ -9,7 +9,9 @@ const feature: FoamFeature = {
commands.registerCommand('foam-vscode.open-random-note', async () => {
const foam = await foamPromise;
const currentFile = window.activeTextEditor?.document.uri.path;
const notes = foam.workspace.list();
const notes = foam.workspace
.list()
.filter(r => URI.isMarkdownFile(r.uri));
if (notes.length <= 1) {
window.showInformationMessage(
'Could not find another note to open. If you believe this is a bug, please file an issue.'

View File

@@ -1,5 +1,5 @@
import { FoamWorkspace } from 'foam-core';
import { createTestNote } from '../test/test-utils';
import { FoamGraph } from 'foam-core';
import { createTestNote, createTestWorkspace } from '../test/test-utils';
import { isOrphan } from './orphans';
const orphanA = createTestNote({
@@ -16,17 +16,17 @@ const nonOrphan2 = createTestNote({
links: [{ slug: 'non-orphan-1' }],
});
const workspace = new FoamWorkspace()
const workspace = createTestWorkspace()
.set(orphanA)
.set(nonOrphan1)
.set(nonOrphan2)
.resolveLinks();
.set(nonOrphan2);
const graph = FoamGraph.fromWorkspace(workspace);
describe('isOrphan', () => {
it('should return true when a note with no connections is provided', () => {
expect(isOrphan(workspace, orphanA)).toBeTruthy();
expect(isOrphan(orphanA.uri, graph)).toBeTruthy();
});
it('should return false when a note with connections is provided', () => {
expect(isOrphan(workspace, nonOrphan1)).toBeFalsy();
expect(isOrphan(nonOrphan1.uri, graph)).toBeFalsy();
});
});

View File

@@ -1,8 +1,12 @@
import * as vscode from 'vscode';
import { Foam, FoamWorkspace, isNote, Resource } from 'foam-core';
import { Foam, FoamGraph, URI } from 'foam-core';
import { getOrphansConfig } from '../settings';
import { FoamFeature } from '../types';
import { GroupedResourcesTreeDataProvider } from '../utils/grouped-resources-tree-data-provider';
import {
GroupedResourcesTreeDataProvider,
ResourceTreeItem,
UriTreeItem,
} from '../utils/grouped-resources-tree-data-provider';
const feature: FoamFeature = {
activate: async (
@@ -16,13 +20,18 @@ const feature: FoamFeature = {
);
const provider = new GroupedResourcesTreeDataProvider(
foam.workspace,
foam.services.dataStore,
'orphans',
'orphan',
(resource: Resource) => isOrphan(foam.workspace, resource),
getOrphansConfig(),
workspacesURIs
workspacesURIs,
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
uri => {
if (URI.isPlaceholder(uri)) {
return new UriTreeItem(uri);
}
const resource = foam.workspace.find(uri);
return new ResourceTreeItem(resource, foam.workspace);
}
);
context.subscriptions.push(
@@ -34,12 +43,8 @@ const feature: FoamFeature = {
);
},
};
export default feature;
export function isOrphan(workspace: FoamWorkspace, resource: Resource) {
if (isNote(resource)) {
return workspace.getConnections(resource.uri).length === 0;
} else {
return false;
}
}
export const isOrphan = (uri: URI, graph: FoamGraph) =>
graph.getConnections(uri).length === 0;
export default feature;

View File

@@ -1,99 +1,70 @@
import {
createAttachment,
createPlaceholder,
createTestNote,
} from '../test/test-utils';
import { FoamWorkspace, URI } from 'foam-core';
import { createTestNote } from '../test/test-utils';
import { isPlaceholderResource } from './placeholders';
describe('isPlaceholderResource', () => {
it('should return true when a placeholder', () => {
expect(isPlaceholderResource(createPlaceholder(''))).toBeTruthy();
});
it('should return true when an empty note is provided', () => {
const noteA = createTestNote({
uri: 'note-a.md',
text: '',
links: [{ slug: 'placeholder' }],
});
const ws = new FoamWorkspace().set(noteA);
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: '',
})
)
isPlaceholderResource(URI.placeholder('placeholder'), ws)
).toBeTruthy();
});
it('should return true when an empty note is provided', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: '',
})
)
).toBeTruthy();
const noteA = createTestNote({ uri: 'note-a.md', text: '' });
const ws = new FoamWorkspace().set(noteA);
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
});
it('should return true when a note containing only whitespace is provided', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: ' \n\t\n\t ',
})
)
).toBeTruthy();
const noteA = createTestNote({
uri: '',
text: ' \n\t\n\t ',
});
const ws = new FoamWorkspace().set(noteA);
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
});
it('should return true when a note containing only a title is provided', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: '# Title',
})
)
).toBeTruthy();
const noteA = createTestNote({
uri: '',
text: '# Title',
});
const ws = new FoamWorkspace().set(noteA);
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
});
it('should return true when a note containing a title followed by whitespace is provided', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: '# Title \n\t\n \t \n ',
})
)
).toBeTruthy();
const noteA = createTestNote({
uri: '',
text: '# Title \n\t\n \t \n ',
});
const ws = new FoamWorkspace().set(noteA);
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
});
it('should return false when there is more than one line containing more than just whitespace', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: '# Title\nA line that is not the title\nAnother line',
})
)
).toBeFalsy();
const noteA = createTestNote({
uri: '',
text: '# Title\nA line that is not the title\nAnother line',
});
const ws = new FoamWorkspace().set(noteA);
expect(isPlaceholderResource(noteA.uri, ws)).toBeFalsy();
});
it('should return false when there is at least one line of non-text content', () => {
expect(
isPlaceholderResource(
createTestNote({
uri: '',
text: 'A line that is not the title\n',
})
)
).toBeFalsy();
});
const noteA = createTestNote({
uri: '',
text: 'A line that is not the title\n',
});
const ws = new FoamWorkspace().set(noteA);
it('should return false when an attachment is provided', () => {
expect(
isPlaceholderResource(
createAttachment({
uri: '',
})
)
).toBeFalsy();
expect(isPlaceholderResource(noteA.uri, ws)).toBeFalsy();
});
});

View File

@@ -1,8 +1,12 @@
import * as vscode from 'vscode';
import { Foam, Resource, isNote, isPlaceholder } from 'foam-core';
import { Foam, FoamWorkspace, URI } from 'foam-core';
import { getPlaceholdersConfig } from '../settings';
import { FoamFeature } from '../types';
import { GroupedResourcesTreeDataProvider } from '../utils/grouped-resources-tree-data-provider';
import {
GroupedResourcesTreeDataProvider,
ResourceTreeItem,
UriTreeItem,
} from '../utils/grouped-resources-tree-data-provider';
const feature: FoamFeature = {
activate: async (
@@ -14,13 +18,21 @@ const feature: FoamFeature = {
dir => dir.uri
);
const provider = new GroupedResourcesTreeDataProvider(
foam.workspace,
foam.services.dataStore,
'placeholders',
'placeholder',
isPlaceholderResource,
getPlaceholdersConfig(),
workspacesURIs
workspacesURIs,
() =>
foam.graph
.getAllNodes()
.filter(uri => isPlaceholderResource(uri, foam.workspace)),
uri => {
if (URI.isPlaceholder(uri)) {
return new UriTreeItem(uri);
}
const resource = foam.workspace.find(uri);
return new ResourceTreeItem(resource, foam.workspace);
}
);
context.subscriptions.push(
@@ -38,22 +50,19 @@ const feature: FoamFeature = {
export default feature;
export function isPlaceholderResource(resource: Resource) {
if (isPlaceholder(resource)) {
// A placeholder is, by default, blank
export function isPlaceholderResource(uri: URI, workspace: FoamWorkspace) {
if (URI.isPlaceholder(uri)) {
return true;
}
if (isNote(resource)) {
const contentLines = resource.source.text
const resource = workspace.find(uri);
const contentLines =
resource?.source.text
.trim()
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.filter(line => !line.startsWith('#'));
.filter(line => !line.startsWith('#')) ?? '';
return contentLines.length === 0;
}
return false;
return contentLines.length === 0;
}

View File

@@ -1,15 +1,18 @@
import MarkdownIt from 'markdown-it';
import { FoamWorkspace, URI } from 'foam-core';
import { createPlaceholder, createTestNote } from '../test/test-utils';
import { markdownItWithFoamLinks } from './preview-navigation';
import { createTestNote } from '../test/test-utils';
import {
markdownItWithFoamLinks,
markdownItWithFoamTags,
} from './preview-navigation';
describe('Link generation in preview', () => {
const noteA = createTestNote({
uri: 'note-a.md',
title: 'My note title',
links: [{ slug: 'placeholder' }],
});
const placeholder = createPlaceholder('placeholder');
const ws = new FoamWorkspace().set(noteA).set(placeholder);
const ws = new FoamWorkspace().set(noteA);
const md = markdownItWithFoamLinks(MarkdownIt(), ws);
it('generates a link to a note', () => {
@@ -32,3 +35,18 @@ describe('Link generation in preview', () => {
);
});
});
describe('Stylable tag generation in preview', () => {
const noteB = createTestNote({
uri: 'note-b.md',
title: 'Note B',
});
const ws = new FoamWorkspace().set(noteB);
const md = markdownItWithFoamTags(MarkdownIt(), ws);
it('transforms a string containing multiple tags to a stylable html element', () => {
expect(md.render(`Lorem #ipsum dolor #sit`)).toMatch(
`<p>Lorem <span class='foam-tag'>#ipsum</span> dolor <span class='foam-tag'>#sit</span></p>`
);
});
});

View File

@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import markdownItRegex from 'markdown-it-regex';
import { Foam, FoamWorkspace, Logger, URI } from 'foam-core';
import { FoamFeature } from '../types';
import fp from 'lodash/fp';
const feature: FoamFeature = {
activate: async (
@@ -11,8 +12,13 @@ const feature: FoamFeature = {
const foam = await foamPromise;
return {
extendMarkdownIt: (md: markdownit) =>
markdownItWithFoamLinks(md, foam.workspace),
extendMarkdownIt: (md: markdownit) => {
const markdownItExtends = fp.compose(
markdownItWithFoamLinks,
markdownItWithFoamTags
);
return markdownItExtends(md, foam.workspace);
},
};
},
};
@@ -30,18 +36,9 @@ export const markdownItWithFoamLinks = (
if (resource == null) {
return getPlaceholderLink(wikilink);
}
switch (resource.type) {
case 'note':
return `<a class='foam-note-link' title='${
resource.title
}' href='${URI.toFsPath(resource.uri)}'>${wikilink}</a>`;
case 'attachment':
return `<a class='foam-attachment-link' title='attachment' href='${URI.toFsPath(
resource.uri
)}'>${wikilink}</a>`;
case 'placeholder':
return getPlaceholderLink(wikilink);
}
return `<a class='foam-note-link' title='${
resource.title
}' href='${URI.toFsPath(resource.uri)}'>${wikilink}</a>`;
} catch (e) {
Logger.error(
`Error while creating link for [[${wikilink}]] in Preview panel`,
@@ -56,4 +53,31 @@ export const markdownItWithFoamLinks = (
const getPlaceholderLink = (content: string) =>
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
export const markdownItWithFoamTags = (
md: markdownit,
workspace: FoamWorkspace
) => {
return md.use(markdownItRegex, {
name: 'foam-tags',
regex: /(#\w+)/,
replace: (tag: string) => {
try {
const resource = workspace.find(tag);
if (resource == null) {
return getFoamTag(tag);
}
} catch (e) {
Logger.error(
`Error while creating link for ${tag} in Preview panel`,
e
);
return getFoamTag(tag);
}
},
});
};
const getFoamTag = (content: string) =>
`<span class='foam-tag'>${content}</span>`;
export default feature;

View File

@@ -1,12 +1,7 @@
import * as vscode from 'vscode';
import { Foam, Note, IDataStore, URI } from 'foam-core';
import { Foam, Resource, URI, FoamWorkspace } from 'foam-core';
import { FoamFeature } from '../../types';
import {
getNoteTooltip,
getContainsTooltip,
isNote,
isSome,
} from '../../utils';
import { getNoteTooltip, getContainsTooltip, isSome } from '../../utils';
const feature: FoamFeature = {
activate: async (
@@ -14,7 +9,7 @@ const feature: FoamFeature = {
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const provider = new TagsProvider(foam, foam.services.dataStore);
const provider = new TagsProvider(foam, foam.workspace);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
'foam-vscode.tags-explorer',
@@ -40,7 +35,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
notes: TagMetadata[];
}[];
constructor(private foam: Foam, private dataStore: IDataStore) {
constructor(private foam: Foam, private workspace: FoamWorkspace) {
this.computeTags();
}
@@ -54,7 +49,6 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
[key: string]: TagMetadata[];
} = this.foam.workspace
.list()
.filter(isNote)
.reduce((acc: { [key: string]: TagMetadata[] }, note) => {
note.tags.forEach(tag => {
acc[tag] = acc[tag] ?? [];
@@ -75,7 +69,6 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
if (element) {
const references: TagReference[] = element.notes
.map(({ uri }) => this.foam.workspace.get(uri))
.filter(isNote)
.map(note => new TagReference(element.tag, note));
return Promise.resolve([
@@ -93,7 +86,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
if (item instanceof TagReference) {
const content = await this.dataStore.read(item.note.uri);
const content = await this.workspace.read(item.note.uri);
if (isSome(content)) {
item.tooltip = getNoteTooltip(content);
}
@@ -147,7 +140,7 @@ export class TagSearch extends vscode.TreeItem {
export class TagReference extends vscode.TreeItem {
public readonly title: string;
constructor(public readonly tag: string, public readonly note: Note) {
constructor(public readonly tag: string, public readonly note: Resource) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.title = note.title;
this.description = note.uri.path;

View File

@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../types';
import { commands } from 'vscode';
import { createNoteFromPlacehoder, focusNote, isSome } from '../utils';
import { createNoteFromPlaceholder, focusNote, isSome } from '../utils';
import { URI } from 'foam-core';
import { toVsCodeUri } from '../utils/vsc-utils';
@@ -16,7 +16,7 @@ export const OPEN_COMMAND = {
return vscode.commands.executeCommand('vscode.open', toVsCodeUri(uri));
case 'placeholder':
const newNote = await createNoteFromPlacehoder(uri);
const newNote = await createNoteFromPlaceholder(uri);
if (isSome(newNote)) {
const title = uri.path.split('/').slice(-1);

View File

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

View File

@@ -5,15 +5,19 @@ import * as vscode from 'vscode';
import path from 'path';
import {
URI,
Attachment,
NoteLinkDefinition,
Note,
Placeholder,
Resource,
Range,
FoamWorkspace,
Matcher,
MarkdownResourceProvider,
Logger,
} from 'foam-core';
import { TextEncoder } from 'util';
import { toVsCodeUri } from '../utils/vsc-utils';
Logger.setLevel('error');
const position = Range.create(0, 0, 0, 100);
const documentStart = position.start;
@@ -27,18 +31,15 @@ const eol = '\n';
*/
export const strToUri = URI.file;
export const createPlaceholder = (key: string): Placeholder => {
return {
uri: URI.placeholder(key),
type: 'placeholder',
};
};
export const createAttachment = (params: { uri: string }): Attachment => {
return {
uri: strToUri(params.uri),
type: 'attachment',
};
export const createTestWorkspace = () => {
const workspace = new FoamWorkspace();
const matcher = new Matcher([URI.file('/')], ['**/*']);
const provider = new MarkdownResourceProvider(matcher, undefined, undefined, {
read: _ => Promise.resolve(''),
list: _ => Promise.resolve([]),
});
workspace.registerProvider(provider);
return workspace;
};
export const createTestNote = (params: {
@@ -48,7 +49,7 @@ export const createTestNote = (params: {
links?: Array<{ slug: string } | { to: string }>;
text?: string;
root?: URI;
}): Note => {
}): Resource => {
const root = params.root ?? URI.file('/');
return {
uri: URI.resolve(params.uri, root),
@@ -132,7 +133,7 @@ export const createFile = async (content: string, filepath?: string) => {
return { uri, content, ...filenameComponents };
};
export const createNote = (r: Note) => {
export const createNote = (r: Resource) => {
let content = `# ${r.title}
some content and ${r.links

View File

@@ -1,5 +1,5 @@
import { ExtensionContext } from 'vscode';
import { Foam } from 'foam-core';
import { ExtensionContext } from 'vscode';
export interface FoamFeature {
activate: (

View File

@@ -12,7 +12,7 @@ import {
Uri,
} from 'vscode';
import * as fs from 'fs';
import { Logger, Resource, Note, URI } from 'foam-core';
import { Logger, URI } from 'foam-core';
import matter from 'gray-matter';
import removeMarkdown from 'remove-markdown';
import { TextEncoder } from 'util';
@@ -254,10 +254,6 @@ export function stripImages(markdown: string): string {
);
}
export const isNote = (resource: Resource): resource is Note => {
return resource.type === 'note';
};
/**
* Creates a note from the given placeholder Uri.
*
@@ -265,7 +261,7 @@ export const isNote = (resource: Resource): resource is Note => {
* @returns the Uri of the created note, or `null`
* if the Uri was not a placeholder or no reference directory could be found
*/
export const createNoteFromPlacehoder = async (
export const createNoteFromPlaceholder = async (
placeholder: URI
): Promise<Uri | null> => {
const basedir =

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