mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ae468a70 | ||
|
|
6119278915 | ||
|
|
34c179123e | ||
|
|
c34394a0ea | ||
|
|
26c38a06ff | ||
|
|
d4623a2d91 | ||
|
|
2f9507dc87 | ||
|
|
4db09070a8 | ||
|
|
5f99d9d5c6 | ||
|
|
9c1480197c | ||
|
|
f1a6426046 | ||
|
|
6de8baa6b5 | ||
|
|
27ff023a26 | ||
|
|
690eb10856 | ||
|
|
1cb8174a9f | ||
|
|
2141bac24a | ||
|
|
6abef8f8e7 | ||
|
|
c797a00223 | ||
|
|
7c01fb13f0 | ||
|
|
abbc2bbb14 | ||
|
|
bb7fee24bb | ||
|
|
43ef3a3e2b | ||
|
|
da69a3057f | ||
|
|
2d06f26bbf | ||
|
|
edbe128e1e | ||
|
|
af0c2bbaa3 | ||
|
|
700bfc1b26 | ||
|
|
a6d5c04453 | ||
|
|
b0c42cead2 | ||
|
|
6c643adb9d | ||
|
|
ca7fdefaae | ||
|
|
149d5f5a7c | ||
|
|
be80857fd1 | ||
|
|
611fa7359d | ||
|
|
08b7e7a231 | ||
|
|
0a259168c7 | ||
|
|
f3d0569c76 | ||
|
|
502129d5ac | ||
|
|
d29bf16db1 | ||
|
|
d228c7cb18 | ||
|
|
78078cf338 | ||
|
|
fe65883bc5 | ||
|
|
20b5261c5c | ||
|
|
f91cfe5d0d | ||
|
|
1ab9cc5f4a | ||
|
|
02ff681700 | ||
|
|
2d9c3be0e6 | ||
|
|
78cf602347 | ||
|
|
898c7b4387 | ||
|
|
7412d518d7 | ||
|
|
e6030ac562 | ||
|
|
d6d958bc92 |
@@ -661,6 +661,42 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pderaaij",
|
||||
"name": "Paul de Raaij",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/495374?v=4",
|
||||
"profile": "http://www.paulderaaij.nl",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -16,16 +16,16 @@
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
"**/node_modules/**/*",
|
||||
"packages/**/*"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": [
|
||||
"fail",
|
||||
"unknown",
|
||||
"pass"
|
||||
],
|
||||
"gitdoc.enabled": false
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"gitdoc.enabled": false,
|
||||
"jest.autoEnable": false,
|
||||
"jest.runAllTestsFirst": false,
|
||||
"search.mode": "reuseEditor"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -203,14 +203,14 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.5.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
minitest (5.14.2)
|
||||
multipart-post (2.1.1)
|
||||
nokogiri (1.11.1)
|
||||
nokogiri (1.11.5)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
octokit (4.19.0)
|
||||
@@ -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
|
||||
|
||||
BIN
docs/assets/images/template-picker-annotated.png
Normal file
BIN
docs/assets/images/template-picker-annotated.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -12,7 +12,6 @@
|
||||
- What use cases are we working towards?
|
||||
-[[todo]] User round table
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -118,7 +118,7 @@ the community.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
@@ -126,7 +126,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
tags: todo, good-first-task
|
||||
---
|
||||
# Contribution Guide
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
|
||||
|
||||
## Getting Up To Speed
|
||||
|
||||
Before you start contributing we recommend that you read the following links:
|
||||
|
||||
- [[principles]] - This document describes the guiding principles behind Foam.
|
||||
- [[code-of-conduct]] - Rules we hope every contributor aims to follow, allowing everyone to participate in our community!
|
||||
|
||||
## Diving In
|
||||
|
||||
We understand that diving in an unfamiliar codebase may seem scary,
|
||||
to make it easier for new contributors we provide some resources:
|
||||
|
||||
- [[architecture]] - This document describes the architecture of Foam and how the repository is structured.
|
||||
|
||||
You can also see [existing issues](https://github.com/foambubble/foam/issues) and help out!
|
||||
@@ -41,6 +45,7 @@ You should now be ready to start working!
|
||||
|
||||
Code needs to come with tests.
|
||||
We use the following convention in Foam:
|
||||
|
||||
- *.test.ts are unit tests
|
||||
- *.spec.ts are integration tests
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ tags: architecture
|
||||
This document aims to provide a quick overview of the Foam architecture!
|
||||
|
||||
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
|
||||
|
||||
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VSCode plugin.
|
||||
|
||||
Exceptions to the monorepo are:
|
||||
|
||||
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
|
||||
- All other [[recommended-extensions]] live in their respective GitHub repos.
|
||||
- [foam-cli](https://github.com/foambubble/foam-cli) - The Foam CLI tool.
|
||||
|
||||
@@ -10,5 +10,3 @@ But there's also a bunch of roadmap items that are hard to implement this way, a
|
||||
Overall, we should strive to build big things from small things. Focused, interoperable modules are better, because they allow users to pick and mix which features work for them. A good example of why this matters is the Markdown All In One extension we rely on: While it provides many of the things we need, a few of its features are incompatible with how I would like to work, and therefore it becomes a limiter of how well I can improve my own workflow.
|
||||
|
||||
However, there becomes a point where we may benefit from implementing a centralised solution, e.g. a syntax, an extension or perhaps a VSCode language server. As much as possible, we should allow users to operate in a decentralised manner.
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ The potential solution:
|
||||
- This would be a GitHub action (or a local script, ran via foam-cli) that outputs publish-friendly markdown format for static site generators and other publishing tools
|
||||
- This build step should be pluggable, so that other transformations could be ran during it
|
||||
- Have publish targets defined in settings, that support both turning the link reference definitions on/off and defining their format (.md or not). Example draft (including also edit-time aspect):
|
||||
|
||||
```typescript
|
||||
// settings json
|
||||
// see enumerations below for explanations on values
|
||||
@@ -120,6 +121,7 @@ The potential solution:
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- With Foam repo, just use edit-time link reference definitions with '.md' extension - this makes the links work in the Github UI
|
||||
- Have publish target defined for Github pages, that doesn't use '.md' extension, but still has the link reference definitions. Generate the output into gh-pages branch (or separate repo) with automation.
|
||||
- This naturally requires first removing the existing link reference definitions during the build
|
||||
|
||||
@@ -11,8 +11,7 @@ The idea would be to automatically generate lists of backlinks (and optionally,
|
||||
- Make every link two-way navigable in published sites
|
||||
- Make Foam notes more portable to different apps and long-term storage
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
- Write out a new `[[wiki-link]]` and `Cmd` + `Click` to create a new file and enter it.
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `New Note` from [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) and enter a **Title Case Name** to create `title-case-name.md`
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create New Note` and enter a **Title Case Name** to create `Title Case Name.md`
|
||||
- Add a keyboard binding to make creating new notes easier.
|
||||
- The [[note-templates]] used by this command can be customized.
|
||||
- You shouldn't worry too much about categorizing your notes. You can always [[search-for-notes]], and explore them using the [[graph-visualisation]].
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[search-for-notes]: ../recipes/search-for-notes.md "Search for Notes"
|
||||
[graph-visualisation]: graph-visualisation.md "Graph Visualisation"
|
||||
[note-templates]: note-templates "Note Templates"
|
||||
[search-for-notes]: ../recipes/search-for-notes "Search for Notes"
|
||||
[graph-visualisation]: graph-visualisation "Graph Visualisation"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -28,11 +28,11 @@ To enable the feature:
|
||||
|
||||
```
|
||||
{
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# Foam logging in VsCode
|
||||
|
||||
## Find the Foam log
|
||||
|
||||
The Foam log can be found in the `Output` tab.
|
||||
|
||||
1. To show the tab, click on `View > Output`.
|
||||
2. In the dropdown on the right of the tab, select `Foam`.
|
||||
|
||||

|
||||
|
||||
## Change the default logging level
|
||||
|
||||
1. Open workspace settings (`cmd+,`, or execute the `Preferences: Open Workspace Settings` command)
|
||||
2. Look for the entry `Foam > Logging: Level`
|
||||
|
||||
Set to debug when reporting an issue
|
||||
|
||||
## Change the log level for the session
|
||||
|
||||
Execute the command `Foam: Set log level`.
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Foam comes with a graph visualisation of your notes. To see the graph execute the `Foam: Show Graph` command.
|
||||
|
||||
The graph will:
|
||||
|
||||
- allow you to highlight a node by hovering on it, to quickly see how it's connected to the rest of your notes
|
||||
- allow you to select one or more (by keeping `SHIFT` pressed while selecting) nodes by clicking on them, to better understand the structure of your notes
|
||||
- allow you to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
|
||||
@@ -32,6 +33,7 @@ A sample configuration object is provided below:
|
||||
```
|
||||
|
||||
### Style nodes by type
|
||||
|
||||
It is possible to customize the style of a node based on the `type` property in the YAML frontmatter of the corresponding document.
|
||||
|
||||
For example the following `backlinking.md` note:
|
||||
@@ -46,6 +48,7 @@ type: feature
|
||||
```
|
||||
|
||||
And the following `settings.json`:
|
||||
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"node": {
|
||||
@@ -57,6 +60,3 @@ And the following `settings.json`:
|
||||
Will result in the following graph:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@ When you use `[[wiki-links]]`, the [foam-vscode](https://github.com/foambubble/f
|
||||
## Example
|
||||
|
||||
The following example:
|
||||
|
||||
```md
|
||||
- [[wiki-links]]
|
||||
- [[github-pages]]
|
||||
```
|
||||
|
||||
...generates the following link reference definitions to the bottom of the file:
|
||||
|
||||
```md
|
||||
[wiki-links]: wiki-links "Wiki Links"
|
||||
[github-pages]: github-pages "Github Pages"
|
||||
```
|
||||
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
|
||||
## Specification
|
||||
@@ -67,7 +71,6 @@ After changing the setting in your workspace, you can run the [[workspace-janito
|
||||
|
||||
See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[workspace-janitor]: workspace-janitor.md "Janitor"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
|
||||
@@ -1,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
|
||||
|
||||

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

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
### 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).
|
||||
|
||||
@@ -25,3 +40,110 @@ In addition, you can also use variables provided by Foam:
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
|
||||
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables.
|
||||
|
||||
## Metadata
|
||||
|
||||
Templates can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `filepath` | The filepath to use when creating the new note. If the filepath is a relative filepath, it is relative to the current workspace. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
|
||||
Foam-specific variables (e.g. `$FOAM_TITLE`) can be used within template metadata. However, VS Code snippet variables are ([currently](https://github.com/foambubble/foam/pull/655)) not supported.
|
||||
|
||||
### `filepath` attribute
|
||||
|
||||
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template.
|
||||
If the filepath is a relative filepath, it is relative to the current workspace.
|
||||
|
||||
#### Example of relative `filepath`
|
||||
|
||||
For example, `filepath` can be used to customize `.foam/templates/new-note.md`, overriding the default `Foam: Create New Note` behaviour of opening the file in the same directory as the active file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# This will create the note in the "journal" subdirectory of the current workspace,
|
||||
# regardless of which file is the active file.
|
||||
foam_template:
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of absolute `filepath`
|
||||
|
||||
`filepath` can be an absolute filepath, so that the notes get created in the same location, regardless of which file or workspace the editor currently has open.
|
||||
The format of an absolute filepath may vary depending on the filesystem used.
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
# Unix / MacOS filesystems
|
||||
filepath: '/Users/john.smith/foam/journal/$FOAM_TITLE.md'
|
||||
|
||||
# Windows filesystems
|
||||
filepath: 'C:\Users\john.smith\Documents\foam\journal\$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
### `name` and `description` attributes
|
||||
|
||||
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):
|
||||
|
||||

|
||||
|
||||
### Adding template metadata to an existing YAML Frontmatter block
|
||||
|
||||
If your template already has a YAML Frontmatter block, you can add the Foam template metadata to it.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Foam only supports adding the template metadata to *YAML* Frontmatter blocks. If the existing Frontmatter block uses some other format (e.g. JSON), you will have to add the template metadata to its own YAML Frontmatter block.
|
||||
|
||||
Further, the template metadata must be provided as a [YAML block mapping](https://yaml.org/spec/1.2/spec.html#id2798057), with the attributes placed on the lines immediately following the `foam_template` line:
|
||||
|
||||
```yaml
|
||||
---
|
||||
existing_frontmatter: "Existing Frontmatter block"
|
||||
foam_template: # this is a YAML "Block" mapping ("Flow" mappings aren't supported)
|
||||
name: My Note Template # Attributes must be on the lines immediately following `foam_template`
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
Due to the technical limitations of parsing the complex YAML format, unless the metadata is provided this specific form, Foam is unable to correctly remove the template metadata before creating the resulting note.
|
||||
|
||||
If this limitation proves inconvenient to you, please let us know. We may be able to extend our parsing capabilities to cover your use case. In the meantime, you can add the template metadata without this limitation by providing it in its own YAML Frontmatter block.
|
||||
|
||||
### Adding template metadata to its own YAML Frontmatter block
|
||||
|
||||
You can add the template metadata to its own YAML Frontmatter block at the start of the template:
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
If the note already has a Frontmatter block, a Foam-specific Frontmatter block can be added to the start of the template. The Foam-specific Frontmatter block must always be placed at the very beginning of the file, and only whitespace can separate the two Frontmatter blocks.
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
|
||||
---
|
||||
existing_frontmatter: "Existing Frontmatter block"
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
@@ -3,15 +3,22 @@
|
||||
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
|
||||
|
||||
Tags can also be hierarchical, so you can have `#parent/child`.
|
||||
|
||||
## Navigating tags
|
||||
|
||||
It's possible to navigate tags via the Tag Explorer panel.
|
||||
In the future it will be possible to explore tags via the graph as well.
|
||||
|
||||
## Styling tags
|
||||
Inline tags can be styled using custom CSS with the selector `.foam-tag`.
|
||||
|
||||
## An alternative to tags
|
||||
|
||||
Given the power of backlinks, some people prefer to use them also as tags.
|
||||
|
||||
@@ -5,10 +5,12 @@ To store your personal knowledge graph in markdown files instead of a database,
|
||||
**Foam Janitor** (inspired by Andy Matuschak's [note-link-janitor](https://github.com/andymatuschak/note-link-janitor)) helps you migrate existing notes to Foam, and maintain your Foam's health over time.
|
||||
|
||||
Currently, Foam's Janitor helps you to:
|
||||
|
||||
- Ensure your [[link-reference-definitions]] are up to date
|
||||
- Ensure every document has a well-formatted title (required for Markdown Links, Markdown Notes, and Foam Gatsby Template compatibility)
|
||||
|
||||
In the future, Janitor can help you with
|
||||
|
||||
- Updating [[materialized-backlinks]]
|
||||
- Lint, format and structure notes
|
||||
- Rename and move notes around while keeping their references up to date.
|
||||
|
||||
@@ -5,7 +5,7 @@ Uncategorised thoughts, to be added
|
||||
- Release notes
|
||||
- Markdown Preview
|
||||
- It's possible to customise the markdown preview styling. **Maybe make it use local foam workspace styles for live preview of the site??**
|
||||
- See: https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles
|
||||
- See: <https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles>
|
||||
- Use VS Code [CodeTour](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for onboarding
|
||||
- Investigate other similar extensions:
|
||||
- [Unotes](https://marketplace.visualstudio.com/items?itemName=ryanmcalister.Unotes)
|
||||
@@ -16,7 +16,7 @@ Uncategorised thoughts, to be added
|
||||
- Every Foam could have a different theme even in the editor, so you'll see it like they see it
|
||||
- UI and layout design of your workspace can become a thing
|
||||
- VS Code Notebooks API
|
||||
- https://code.visualstudio.com/api/extension-guides/notebook
|
||||
- <https://code.visualstudio.com/api/extension-guides/notebook>
|
||||
- Future architecture
|
||||
- Could we do publish-related settings as a pre-push git hook, e.g. generating footnote labels
|
||||
- Running them on Github Actions to edit stuff as it comes in
|
||||
@@ -32,5 +32,3 @@ Uncategorised thoughts, to be added
|
||||
- Maps have persistent topologies. As the graph grows, you should be able to visualise where an idea belongs. Maybe a literal map? And island? A DeckGL visualisation?
|
||||
|
||||
Testing: This file is served from the /docs directory.
|
||||
|
||||
|
||||
|
||||
@@ -198,6 +198,10 @@ 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>
|
||||
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Present: @jevakallio, @riccardoferretti
|
||||
|
||||
- Land work to master
|
||||
- Create a foam-core package
|
||||
|
||||
-
|
||||
|
||||
### Open questions
|
||||
|
||||
@@ -37,7 +37,7 @@ This principle may seem like it contradicts [Foam wants you to own your thoughts
|
||||
|
||||
- **Foam is a collection of ideas.** Foam was released to the public not to share the few good ideas in it, but to learn many good ideas from others. As you improve your own workflow, share your work on your own Foam blog.
|
||||
- **Foam is open for contributions.** If you use a tool or workflow that you like that fits these principles, please contribute them back to the Foam template as [[recipes]], [[recommended-extensions]] or documentation in [this workspace](https://github.com/foambubble/foam). See also: [[contribution-guide]].
|
||||
- **Foam is open source.** Feel free to fork it, improve it and remix it. Just don't sell it, as per our [license](license).
|
||||
- **Foam is open source.** Feel free to fork it, improve it and remix it. Just don't sell it, as per our [license](LICENSE.txt).
|
||||
- **Foam is not Roam.** This project was inspired by Roam Research, but we're not limited by what Roam does. No idea is too big (though if it doesn't fit with Foam's core workflow, we might make it a [[recipes]] page instead).
|
||||
|
||||
## Foam is for hackers, not only for programmers
|
||||
@@ -48,7 +48,6 @@ While Foam uses tools popular among computer programmers, Foam should be inclusi
|
||||
- **Foam is not just for programmers.** If you're a programmer, feel free to write scripts and extensions to support your own workflow, and publish them for others to use, but the out of the box Foam experience should not require you to know how to do so. You should, however, be curious and open to adopting new tools that are unfamiliar to you, and evaluate whether they could work for you.
|
||||
- **Foam is for everyone** As a foam user, you support everyone's quest for knowledge and self-improvement, not only your own, or folks' who look like you. All participants in Foam repositories, discussion forums, physical and virtual meeting spaces etc are expected to respect each other as described in our [[code-of-conduct]]. **Foam is not for toxic tech bros.**
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: recipes/recipes.md "Recipes"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
|
||||
@@ -42,6 +42,7 @@ The current capabilities of templates is limited in some important ways. This do
|
||||
Creating of new notes in Foam is too cumbersome and slow. Despite their power, Foam templates can currently only be used in very limited scenarios.
|
||||
|
||||
This proposal aims to address these issues by streamlining note creation and by allowing templates to be used everywhere.
|
||||
|
||||
## Limitations of current templating
|
||||
|
||||
### Too much friction to create a new note
|
||||
@@ -162,14 +163,16 @@ This would open use the template found at `.foam/templates/new-note.md` to creat
|
||||
#### Case 1: `.foam/templates/new-note.md` doesn't exist
|
||||
|
||||
If `.foam/templates/new-note.md` doesn't exist, it behaves the same as `Markdown Notes: New Note`:
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
**Note:** this would use an implicit default template, making use of the `${title}` variable.
|
||||
|
||||
#### Case 2: `.foam/templates/new-note.md` exists
|
||||
|
||||
If `.foam/templates/new-note.md` exists:
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
**Progress:** At this point, we have a faster way to create new notes from templates.
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
You can use [foam-gatsby-template](https://github.com/mathieudutour/foam-gatsby-template) to generate a static site to host it online on Github or [Vercel](https://vercel.com).
|
||||
|
||||
### Publishing your foam to GitHub pages
|
||||
|
||||
It comes configured with Github actions to auto deploy to Github pages when changes are pushed to your main branch.
|
||||
|
||||
### Publishing your foam to Vercel
|
||||
|
||||
When you're ready to publish, run a local build.
|
||||
|
||||
```bash
|
||||
cd _layouts
|
||||
npm run build
|
||||
|
||||
@@ -11,14 +11,15 @@ The following recipe is written with the assumption that you already have an [Az
|
||||
1. Generate a Foam workspace using the [foam-template project](https://github.com/foambubble/foam-template).
|
||||
2. Change the remote to a git repository in Azure DevOps (Repos -> Import a Repository -> Add Clone URL with Authentication), or copy all the files into a new Azure DevOps git repository.
|
||||
3. Define which document will be the wiki home page. To do that, create a file called `.order` in the Foam workspace root folder, with first line being the document filename without `.md` extension. For a project created from the Foam template, the file would look like this:
|
||||
|
||||
```
|
||||
readme
|
||||
```
|
||||
|
||||
4. Push the repository to remote in Azure DevOps.
|
||||
|
||||
## Publish repository to a wiki
|
||||
|
||||
|
||||
1. Navigate to your Azure DevOps project in a web browser.
|
||||
2. Choose **Overview** > **Wiki**. If you don't have wikis for your project, choose **Publish code as a wiki** on welcome page.
|
||||
3. Choose repository with your Foam workspace, branch (usually `master` or `main`), folder (for workspace created from foam-template it is `/`), and wiki name, and press **Publish**.
|
||||
@@ -40,6 +41,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
|
||||
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
|
||||
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
|
||||
|
||||
```bash
|
||||
[core]
|
||||
...
|
||||
@@ -58,6 +60,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
url = git@github.com:username/repo.git
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
```
|
||||
|
||||
6. You can then push to both repositories by: `git push origin master` or a single one using: `git push github master` or `git push azure master`
|
||||
|
||||
For more information, read the [Azure DevOps documentation](https://docs.microsoft.com/en-us/azure/devops/project/wiki/publish-repo-to-wiki).
|
||||
|
||||
@@ -9,36 +9,41 @@
|
||||
## How to publish locally
|
||||
|
||||
If you want to test your published foam, follow the instructions:
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll
|
||||
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll>
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll>
|
||||
|
||||
Assuming you have installed ruby/jekyll and the rest:
|
||||
|
||||
- `touch Gemfile`
|
||||
- open the file and paste the following:
|
||||
|
||||
```
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem "github-pages", "VERSION"
|
||||
```
|
||||
replacing `VERSION` with the latest from https://rubygems.org/gems/github-pages (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
replacing `VERSION` with the latest from <https://rubygems.org/gems/github-pages> (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
- `bundle`
|
||||
- `bundle exec jekyll 3.9.0 new .`
|
||||
- edit the `Gemfile` according to the instructions at [Creating Your Site](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll#creating-your-site) Point n.8
|
||||
- `bundle exec jekyll serve`
|
||||
|
||||
|
||||
## Other templates
|
||||
|
||||
There are many other templates which also support publish your foam workspace to github pages
|
||||
|
||||
* gatsby-digital-garden
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* foam-mkdocs-template
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ The standard [foam-template](https://github.com/foambubble/foam-template) is rea
|
||||
## Enable navigation in GitHub
|
||||
|
||||
To allow navigation from within the GitHub repo, make sure to generate the link references, by setting
|
||||
|
||||
- `Foam › Edit: Link Reference Definitions` -> `withExtensions`
|
||||
|
||||
See [[link-reference-definitions]] for more information.
|
||||
|
||||
## Customising the style
|
||||
|
||||
You can edit `assets/css/style.scss` to change how published pages look.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -11,6 +11,7 @@ Generate a solution using the [Foam template].
|
||||
Change the remote to GitLab, or copy all the files into a new GitLab repo.
|
||||
|
||||
### Add a _config.yaml
|
||||
|
||||
Add another file to the root directory (the one with `readme.md` in it) called `_config.yaml` (no extension)
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
You can use [foam-eleventy-template](https://github.com/juanfrank77/foam-eleventy-template) to generate a static site with [Eleventy](https://www.11ty.dev/), and host it online on [Netlify](https://www.netlify.com/).
|
||||
|
||||
With this template you can
|
||||
|
||||
- Have control over what to publish and what to keep private
|
||||
- Customize the styling of the site to your own liking
|
||||
|
||||
@@ -12,8 +13,6 @@ When you're ready to publish, import the GitHub repository you created with **fo
|
||||
|
||||
Once that's done, all you have to do is make changes to your workspace in VS Code and push them to the main branch on GitHub. Netlify will recognize the changes, deploy them automatically and give you a link where your Foam is published.
|
||||
|
||||
|
||||
That's it!
|
||||
|
||||
You can now see it online and use that link to share it with your friends, so that they can see it too.
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
- [VSCode Extensions Packs](https://code.visualstudio.com/blogs/2017/03/07/extension-pack-roundup) [[todo]] Evaluate for deployment
|
||||
- [Dark mode](https://css-tricks.com/dark-modes-with-css/)
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -5,7 +5,7 @@ You can also easily manipulate the git history to reduce clutter.
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -18,9 +18,6 @@ __For Foam specific needs, you can add a comment here by following the [[contrib
|
||||
- Feedback and issues with the extension should be reported to the authors themselves
|
||||
- Feedback and issues with the integration of the extension in Foam can be reported in our [issue tracker](https://github.com/foambubble/foam/issues)
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[contribution-guide]: ../contribution-guide.md "Contribution Guide"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,7 @@ With this #recipe you can convert a link to a fully-formed Markdown link, using
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
|
||||
Markdown Link Expander will scrape your URL's `<title>` tag to create a nice Markdown-style link.
|
||||
|
||||
@@ -22,4 +22,3 @@ Tip: If you paste a lot of links, give the action a custom [key binding](https:/
|
||||
## Feedback and issues
|
||||
|
||||
Have an idea for the extension? [Feel free to share! 🎉](https://github.com/Skn0tt/markdown-link-expander/issues)
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ With this #recipe you can create notes on your iOS device, which will automatica
|
||||
|
||||
## Instructions
|
||||
|
||||
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
|
||||
```
|
||||
name: Manually triggered workflow
|
||||
@@ -26,15 +25,17 @@ on:
|
||||
data:
|
||||
description: 'What information to put in the knowledge base.'
|
||||
required: true
|
||||
|
||||
|
||||
jobs:
|
||||
store_data:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: anglinb/foam-capture-action@main
|
||||
with:
|
||||
with:
|
||||
{% raw %}
|
||||
capture: ${{ github.event.inputs.data }}
|
||||
{% endraw %}
|
||||
- run: |
|
||||
git config --local user.email "example@gmail.com"
|
||||
git config --local user.name "Your name"
|
||||
@@ -44,14 +45,16 @@ jobs:
|
||||
|
||||
2. In GitHub [create a Personal Access Token](https://github.com/settings/tokens) and give it `repo` scope - make a note of the token
|
||||
3. Run this command to find your `workflow-id` to be used in the Shortcut.
|
||||
|
||||
```bash
|
||||
curl \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: Bearer <GITHUB_TOKEN>" \
|
||||
https://api.github.com/repos/<owner>/<repository>/actions/workflows
|
||||
```
|
||||
|
||||
4. Copy this [Shortcut](https://www.icloud.com/shortcuts/57d2ed90c40e43a5badcc174ebfaaf1d) to your iOS devices and edit the contents of the last step, `GetContentsOfURL`
|
||||
- Make sure you update the URL of the shortcut step with the `owner`, `repository`, `workflow-id` (from the previous step)
|
||||
- Make sure you update the headers of the shortcut step, replaceing `[GITHUB_TOKEN]` with your Personal Access Token (from step 2)
|
||||
- Make sure you update the headers of the shortcut step, replaceing `[GITHUB_TOKEN]` with your Personal Access Token (from step 2)
|
||||
|
||||
5. Run the shortcut & celebrate! ✨ (You should see a GitHub Action run start and the text you entered show up in `inbox.md` in your repository.)
|
||||
|
||||
@@ -7,7 +7,6 @@ We have two alternative #recipe for displaying diagrams in markdown:
|
||||
- [Draw.io](#drawio)
|
||||
- [Using Draw.io](#using-drawio)
|
||||
|
||||
|
||||
## Mermaid
|
||||
|
||||
You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) plugin to draw and preview diagrams in your content.
|
||||
@@ -27,7 +26,6 @@ You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierne
|
||||
3. Start drawing your diagram. Once you done, save it.
|
||||
4. Embed the diagram file as you embedding the image file, for example: ``
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This is an example of how to structure a Recipe. The first paragraph or two should explain the purpose of the recipe succinctly, including why it's useful, if that's not obvious.
|
||||
|
||||
Recipes are intended to document:
|
||||
|
||||
- How to use Foam's basic features
|
||||
- Power user pro-tips
|
||||
- Useful customisations of the default Foam environment
|
||||
@@ -10,8 +11,8 @@ Recipes are intended to document:
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
|
||||
The first section should be a bulleted list of extensions required to use this recipe. At a minimum, this section should list all additional, non-standard extensions.
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@ In the future we'll want to improve this feature by
|
||||
- Make back links editable using [VS Code Search Editors](https://code.visualstudio.com/updates/v1_43#_search-editors)
|
||||
- [Suggested by @Jash on Discord](https://discordapp.com/channels/729975036148056075/729978910363746315/730999992419876956)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[materialized-backlinks]: ../dev/materialized-backlinks.md "Materialized Backlinks (stub)"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
If you're interested in working on it, please start a conversation in [GitHub issues](https://github.com/foambubble/foam/issues).
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
[roadmap]: ../dev/roadmap.md "Roadmap"
|
||||
|
||||
@@ -56,6 +56,3 @@ in `keybindings.json` (Code|File > Preferences > Keyboard Shortcuts) add binding
|
||||
If you have any issues or questions please look at the [README.md](https://github.com/kneely/note-macros#note-macros) on the [note-macros](https://github.com/kneely/note-macros) GitHub.
|
||||
|
||||
If you run into any issues that are not fixed by referring to the README or feature requests please open an [issue](https://github.com/kneely/note-macros/issues).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Real-time Collaboration
|
||||
|
||||
This #recipe is here to just tell you that VS Code Live Share will allow you to collaborate live on your notes.
|
||||
|
||||
|
||||
@@ -26,14 +26,17 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Clip webpages with [[web-clipper]]
|
||||
|
||||
## Discover
|
||||
|
||||
- Explore your notes using [[graph-visualisation]]
|
||||
- Discover relationships with [[backlinking]]
|
||||
- Simulating [[unlinked-references]]
|
||||
|
||||
## Organise
|
||||
|
||||
- Using [[backlinking]] for reference lists.
|
||||
|
||||
## Write
|
||||
|
||||
- Link documents with [[wiki-links]].
|
||||
- Use shortcuts for [[creating-new-notes]]
|
||||
- Instantly create and access your [[daily-notes]]
|
||||
|
||||
@@ -9,6 +9,7 @@ The most promising options are:
|
||||
### [GitJournal](https://gitjournal.io/)
|
||||
|
||||
Pros
|
||||
|
||||
- Open source
|
||||
- Already a usable solution.
|
||||
- Provides functionality to edit, create, and browser markdown files.
|
||||
@@ -19,6 +20,7 @@ Pros
|
||||
- Developer is happy to prioritize Foam compatibility
|
||||
|
||||
Cons
|
||||
|
||||
- Doesn't generate link reference lists (but this is ok, since [[workspace-janitor]] as a GitHub action can solve this)
|
||||
- Not as sleek as Apple/Google notes, some keyboard state glitching on Android, etc.
|
||||
- Lack of control over roadmap. Established product with a paid plan, so may not be open to Foam-supportive changes and additions that don't benefit most users.
|
||||
@@ -28,13 +30,15 @@ Verdict: Good. By far best effort/outcome ratio would be to help improve GitJour
|
||||
### GitHub Codespaces
|
||||
|
||||
Pros
|
||||
|
||||
- Works out of the box just like the desktop app
|
||||
|
||||
Cons
|
||||
|
||||
- not generally available quite yet
|
||||
- [Pricing](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/about-billing-for-codespaces)
|
||||
|
||||
For a quick demo, see https://www.youtube.com/watch?v=KI5m4Uy8_4I.
|
||||
For a quick demo, see <https://www.youtube.com/watch?v=KI5m4Uy8_4I>.
|
||||
|
||||
Verdict: Good. Pricing should be reasonable for taking notes on the fly. Harder to assess for people who would constantly use Foam from mobile phone.
|
||||
|
||||
@@ -54,7 +58,6 @@ If such an app was worth building, it would have to have the following features:
|
||||
|
||||
Given the effort vs reward ratio, it's a low priority for core team, but if someone wants to work on this, we can provide support! Talk to us on the #mobile-apps channel on [Foam Discord](https://foambubble.github.io/join-discord/w).
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[build-vs-assemble]: ../dev/build-vs-assemble.md "Build vs Assemble"
|
||||
[workspace-janitor]: ../features/workspace-janitor.md "Janitor"
|
||||
|
||||
@@ -9,7 +9,6 @@ This list is subject to change. Especially the Git ones.
|
||||
- [Markdown Links](https://marketplace.visualstudio.com/items?itemName=tchayen.markdown-links)
|
||||
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
|
||||
|
||||
|
||||
## Extensions For Additional Features
|
||||
|
||||
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
|
||||
|
||||
@@ -17,4 +17,3 @@ Also happens to sound quite a lot like Home. Funny, that.
|
||||
## Bubble
|
||||
|
||||
Individual Foam note, written in Markdown.
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.13.1"
|
||||
"version": "0.13.6"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.6",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -33,7 +33,7 @@
|
||||
"fast-array-diff": "^1.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"lodash": "^4.17.19",
|
||||
"lodash": "^4.17.21",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,17 +170,17 @@ 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;
|
||||
}
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = props.title ?? note.title;
|
||||
note.title = props.title?.toString() ?? 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: [],
|
||||
@@ -200,8 +313,6 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
...note.properties,
|
||||
...yamlProperties,
|
||||
};
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = note.properties.title ?? note.title;
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = Position.create(
|
||||
node.position!.end.line! + 2,
|
||||
@@ -311,7 +422,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 +449,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';
|
||||
};
|
||||
|
||||
232
packages/foam-core/src/model/graph.ts
Normal file
232
packages/foam-core/src/model/graph.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { IDisposable } from '../index';
|
||||
import { FoamWorkspace, uriToResourceName } from './workspace';
|
||||
import { Range } from './range';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
link: ResourceLink;
|
||||
};
|
||||
|
||||
const pathToPlaceholderId = (value: string) => value;
|
||||
const uriToPlaceholderId = (uri: URI) => pathToPlaceholderId(uri.path);
|
||||
|
||||
export class FoamGraph implements IDisposable {
|
||||
/**
|
||||
* Placehoders by key / slug / value
|
||||
*/
|
||||
public readonly placeholders: { [key: string]: URI } = {};
|
||||
/**
|
||||
* Maps the connections starting from a URI
|
||||
*/
|
||||
public readonly links: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* Maps the connections arriving to a URI
|
||||
*/
|
||||
public readonly backlinks: { [key: string]: Connection[] } = {};
|
||||
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(private readonly workspace: FoamWorkspace) {}
|
||||
|
||||
public contains(uri: URI): boolean {
|
||||
return this.getConnections(uri).length > 0;
|
||||
}
|
||||
|
||||
public getAllNodes(): URI[] {
|
||||
return [
|
||||
...Object.values(this.placeholders),
|
||||
...this.workspace.list().map(r => r.uri),
|
||||
];
|
||||
}
|
||||
|
||||
public getAllConnections(): Connection[] {
|
||||
return Object.values(this.links).flat();
|
||||
}
|
||||
|
||||
public getConnections(uri: URI): Connection[] {
|
||||
return [
|
||||
...(this.links[uri.path] || []),
|
||||
...(this.backlinks[uri.path] || []),
|
||||
];
|
||||
}
|
||||
|
||||
public getLinks(uri: URI): Connection[] {
|
||||
return this.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public getBacklinks(uri: URI): Connection[] {
|
||||
return this.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all the links in the workspace, connecting notes and
|
||||
* creating placeholders.
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @returns the FoamGraph
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
): FoamGraph {
|
||||
let graph = new FoamGraph(workspace);
|
||||
|
||||
Object.values(workspace.list()).forEach(resource =>
|
||||
graph.resolveResource(resource)
|
||||
);
|
||||
if (keepMonitoring) {
|
||||
graph.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
graph.updateLinksRelatedToAddedResource(resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
graph.updateLinksForResource(change.old, change.new);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
graph.updateLinksRelatedToDeletedResource(resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
if (name in this.placeholders) {
|
||||
const placeholder = this.placeholders[name];
|
||||
delete this.placeholders[name];
|
||||
const resourcesToUpdate = this.backlinks[placeholder.path] ?? [];
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res.source))
|
||||
);
|
||||
}
|
||||
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
}
|
||||
|
||||
private updateLinksForResource(oldResource: Resource, newResource: Resource) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
patch.removed.forEach(link => {
|
||||
const target = this.workspace.resolveLink(oldResource, link);
|
||||
return this.disconnect(oldResource.uri, target, link);
|
||||
}, this);
|
||||
patch.added.forEach(link => {
|
||||
const target = this.workspace.resolveLink(newResource, link);
|
||||
return this.connect(newResource.uri, target, link);
|
||||
}, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToDeletedResource(resource: Resource) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = this.links[uri.path] ?? [];
|
||||
delete this.links[uri.path];
|
||||
resourcesPointedByDeletedNote.forEach(connection =>
|
||||
this.disconnect(uri, connection.target, connection.link)
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = this.backlinks[uri.path] ?? [];
|
||||
delete this.backlinks[uri.path];
|
||||
notesPointingToDeletedResource.forEach(link =>
|
||||
this.resolveResource(this.workspace.get(link.source))
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
private connect(source: URI, target: URI, link: ResourceLink) {
|
||||
const connection = { source, target, link };
|
||||
|
||||
this.links[source.path] = this.links[source.path] ?? [];
|
||||
this.links[source.path].push(connection);
|
||||
this.backlinks[target.path] = this.backlinks[target.path] ?? [];
|
||||
this.backlinks[target.path].push(connection);
|
||||
|
||||
if (URI.isPlaceholder(target)) {
|
||||
this.placeholders[uriToPlaceholderId(target)] = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private disconnect(source: URI, target: URI, link: ResourceLink | true) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links[source.path] =
|
||||
this.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.links[source.path].length === 0) {
|
||||
delete this.links[source.path];
|
||||
}
|
||||
this.backlinks[target.path] =
|
||||
this.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.backlinks[target.path].length === 0) {
|
||||
delete this.backlinks[target.path];
|
||||
if (URI.isPlaceholder(target)) {
|
||||
delete this.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public resolveResource(resource: Resource) {
|
||||
delete this.links[resource.uri.path];
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
URI.isEqual(a.source, b.source) &&
|
||||
URI.isEqual(a.target, b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
a.type === b.type && Range.isEqual(a.range, b.range);
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/foam-core/src/model/provider.ts
Normal file
17
packages/foam-core/src/model/provider.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
import { ResourceLink, URI } from 'index';
|
||||
import { Resource } from './note';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
|
||||
export interface ResourceProvider extends IDisposable {
|
||||
init: (workspace: FoamWorkspace) => Promise<void>;
|
||||
supports: (uri: URI) => boolean;
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
readAsMarkdown: (uri: URI) => Promise<string | null>;
|
||||
fetch: (uri: URI) => Promise<Resource | null>;
|
||||
resolveLink: (
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
link: ResourceLink
|
||||
) => URI;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -250,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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(^|\s)#([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
const WORD_REGEX = /(^|\s)([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
const HASHTAG_REGEX = /(^|\s)#([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX = /(^|\s)([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
|
||||
export const extractHashtags = (text: string): Set<string> => {
|
||||
return isSome(text)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Roam Document
|
||||
|
||||
[[Second Roam Document]]
|
||||
[[Second Roam Document]]
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Second Roam Document
|
||||
# Second Roam Document
|
||||
|
||||
@@ -9,4 +9,4 @@ All the link references are correct in this file.
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
[second-document]: second-document "Second Document"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -170,6 +171,26 @@ date: 20-12-12
|
||||
expect(note.title).toBe('Note Title');
|
||||
});
|
||||
|
||||
it('should support numbers', () => {
|
||||
const note1 = createNoteFromMarkdown('/157.md', `hello`);
|
||||
expect(note1.title).toBe('157');
|
||||
|
||||
const note2 = createNoteFromMarkdown('/157.md', `# 158`);
|
||||
expect(note2.title).toBe('158');
|
||||
|
||||
const note3 = createNoteFromMarkdown(
|
||||
'/157.md',
|
||||
`
|
||||
---
|
||||
title: 159
|
||||
---
|
||||
|
||||
# 158
|
||||
`
|
||||
);
|
||||
expect(note3.title).toBe('159');
|
||||
});
|
||||
|
||||
it('should not break on empty titles (see #276)', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/Hello Page.md',
|
||||
@@ -233,7 +254,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 +266,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 +278,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)
|
||||
@@ -315,6 +336,19 @@ this is some #text that includes #tags we #care-about.
|
||||
new Set(['text', 'tags', 'care-about', 'hello', 'world', 'this_is_good'])
|
||||
);
|
||||
});
|
||||
|
||||
it('can find nested tags as array in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
---
|
||||
tags: [hello, world, parent/child]
|
||||
---
|
||||
# this is a heading
|
||||
`
|
||||
);
|
||||
expect(noteA.tags).toEqual(new Set(['hello', 'world', 'parent/child']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser plugins', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -19,6 +19,11 @@ describe('hashtag extraction', () => {
|
||||
new Set(['hello-world', 'this_planet'])
|
||||
);
|
||||
});
|
||||
it('supports nested tags', () => {
|
||||
expect(extractHashtags('#parent/child on #planet')).toEqual(
|
||||
new Set(['parent/child', 'planet'])
|
||||
);
|
||||
});
|
||||
it('ignores tags that only have numbers in text', () => {
|
||||
expect(
|
||||
extractHashtags('this #123 tag should be ignore, but not #123four')
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3870,9 +3870,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
html-encoding-sniffer@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -4944,10 +4944,10 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@^2.2.0:
|
||||
version "2.2.0"
|
||||
|
||||
@@ -4,6 +4,53 @@ 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.6] - 2021-06-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed #667, incorrect resolution of foam-core library
|
||||
|
||||
## [0.13.5] - 2021-06-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved support for nested tags (#661 - thanks @pderaaij)
|
||||
- Allow YAML metadata in templates (#655 - thanks @movermeyer)
|
||||
- Fixed template exclusion globs (#665)
|
||||
|
||||
## [0.13.4] - 2021-05-26
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for nested tags (#643 - thanks @pderaaij)
|
||||
- Improved the flow of creating note from template (#645 - thanks @movermeyer)
|
||||
- Fixed handling of title property in YAML (#647 - thanks @pderaaij and #546)
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated various dependencies
|
||||
|
||||
## [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:
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
|
||||
|
||||
> ⚠️ This is an early stage software. Use at your own peril.
|
||||
|
||||
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
|
||||
|
||||
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VsCode, which means you can pair it with your favorite extensions for a great editing experience.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.6",
|
||||
"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.1",
|
||||
"foam-core": "^0.13.6",
|
||||
"gray-matter": "^4.0.2",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import { bootstrap, FoamConfig, Foam, Logger, FileDataStore } from 'foam-core';
|
||||
import {
|
||||
bootstrap,
|
||||
FoamConfig,
|
||||
Logger,
|
||||
FileDataStore,
|
||||
Matcher,
|
||||
MarkdownResourceProvider,
|
||||
ResourceProvider,
|
||||
} from 'foam-core';
|
||||
|
||||
import { features } from './features';
|
||||
import { getConfigFromVscode } from './services/config';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
|
||||
function createMarkdownProvider(config: FoamConfig): ResourceProvider {
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
const provider = new MarkdownResourceProvider(matcher, triggers => {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
return [
|
||||
watcher.onDidChange(triggers.onDidChange),
|
||||
watcher.onDidCreate(triggers.onDidCreate),
|
||||
watcher.onDidDelete(triggers.onDidDelete),
|
||||
watcher,
|
||||
];
|
||||
});
|
||||
return provider;
|
||||
}
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
Logger.setDefaultLogger(logger);
|
||||
@@ -13,18 +39,18 @@ export async function activate(context: ExtensionContext) {
|
||||
try {
|
||||
Logger.info('Starting Foam');
|
||||
|
||||
// Prepare Foam
|
||||
const config: FoamConfig = getConfigFromVscode();
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
const dataStore = new FileDataStore(config, watcher);
|
||||
|
||||
const foamPromise: Promise<Foam> = bootstrap(config, dataStore);
|
||||
const dataStore = new FileDataStore();
|
||||
const markdownProvider = createMarkdownProvider(config);
|
||||
const foamPromise = bootstrap(config, dataStore, [markdownProvider]);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
|
||||
|
||||
context.subscriptions.push(dataStore, foam, watcher);
|
||||
context.subscriptions.push(foam, markdownProvider);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
|
||||
@@ -1,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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { window } from 'vscode';
|
||||
import { window, workspace } from 'vscode';
|
||||
import {
|
||||
resolveFoamVariables,
|
||||
resolveFoamTemplateVariables,
|
||||
substituteFoamVariables,
|
||||
determineDefaultFilepath,
|
||||
} from './create-from-template';
|
||||
import path from 'path';
|
||||
import { isWindows } from '../utils';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
describe('substituteFoamVariables', () => {
|
||||
test('Does nothing if no Foam-specific variables are used', () => {
|
||||
@@ -11,7 +16,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>();
|
||||
@@ -57,15 +62,15 @@ describe('resolveFoamVariables', () => {
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foam_title)));
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
@@ -74,16 +79,115 @@ describe('resolveFoamVariables', () => {
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', foam_title);
|
||||
givenValues.set('FOAM_TITLE', foamTitle);
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFoamTemplateVariables', () => {
|
||||
test('Does nothing for template without Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# \${AnotherVariable} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
|
||||
# $AnotherVariable} <-- Unrelated to foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
|
||||
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 expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
|
||||
expect(await resolveFoamTemplateVariables(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Allows extra variables to be provided; only resolves the unique set', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
# My note title
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(input, new Set(['FOAM_TITLE']))
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineDefaultFilepath', () => {
|
||||
test('Absolute filepath metadata is unchanged', () => {
|
||||
const absolutePath = isWindows
|
||||
? 'c:\\absolute_path\\journal\\My Note Title.md'
|
||||
: '/absolute_path/journal/My Note Title.md';
|
||||
|
||||
const resolvedValues = new Map<string, string>();
|
||||
const templateMetadata = new Map<string, string>();
|
||||
templateMetadata.set('filepath', absolutePath);
|
||||
|
||||
const resultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(absolutePath);
|
||||
});
|
||||
|
||||
test('Relative filepath metadata is appended to current directory', () => {
|
||||
const relativePath = isWindows
|
||||
? 'journal\\My Note Title.md'
|
||||
: 'journal/My Note Title.md';
|
||||
|
||||
const resolvedValues = new Map<string, string>();
|
||||
const templateMetadata = new Map<string, string>();
|
||||
templateMetadata.set('filepath', relativePath);
|
||||
|
||||
const resultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
|
||||
const expectedPath = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
relativePath
|
||||
);
|
||||
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import {
|
||||
window,
|
||||
commands,
|
||||
ExtensionContext,
|
||||
workspace,
|
||||
QuickPickItem,
|
||||
SnippetString,
|
||||
Uri,
|
||||
window,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { TextEncoder } from 'util';
|
||||
import { focusNote } from '../utils';
|
||||
import { existsSync } from 'fs';
|
||||
import { isAbsolute } from 'path';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
|
||||
const templatesDir = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates'
|
||||
);
|
||||
|
||||
export class UserCancelledOperation extends Error {
|
||||
constructor(message?: string) {
|
||||
super('UserCancelledOperation');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const knownFoamVariables = new Set(['FOAM_TITLE']);
|
||||
|
||||
const defaultTemplateDefaultText: string = `---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
`;
|
||||
const defaultTemplateUri = Uri.joinPath(templatesDir, 'new-note.md');
|
||||
|
||||
const templateContent = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
Welcome to Foam templates.
|
||||
@@ -33,12 +57,22 @@ 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[]> {
|
||||
const templates = await workspace.findFiles('.foam/templates/**.md');
|
||||
return templates.map(template => path.basename(template.path));
|
||||
async function templateMetadata(
|
||||
templateUri: Uri
|
||||
): Promise<Map<string, string>> {
|
||||
const contents = await workspace.fs
|
||||
.readFile(templateUri)
|
||||
.then(bytes => bytes.toString());
|
||||
const [templateMetadata] = extractFoamTemplateFrontmatterMetadata(contents);
|
||||
return templateMetadata;
|
||||
}
|
||||
|
||||
async function getTemplates(): Promise<Uri[]> {
|
||||
const templates = await workspace.findFiles('.foam/templates/**.md', null);
|
||||
return templates;
|
||||
}
|
||||
|
||||
async function offerToCreateTemplate(): Promise<void> {
|
||||
@@ -60,16 +94,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 +158,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,46 +172,85 @@ export function substituteFoamVariables(
|
||||
return templateText;
|
||||
}
|
||||
|
||||
async function createNoteFromTemplate(): Promise<void> {
|
||||
function sortTemplatesMetadata(
|
||||
t1: Map<string, string>,
|
||||
t2: Map<string, string>
|
||||
) {
|
||||
// Sort by name's existence, then name, then path
|
||||
|
||||
if (t1.get('name') === undefined && t2.get('name') !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (t1.get('name') !== undefined && t2.get('name') === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const pathSortOrder = t1
|
||||
.get('templatePath')
|
||||
.localeCompare(t2.get('templatePath'));
|
||||
|
||||
if (t1.get('name') === undefined && t2.get('name') === undefined) {
|
||||
return pathSortOrder;
|
||||
}
|
||||
|
||||
const nameSortOrder = t1.get('name').localeCompare(t2.get('name'));
|
||||
|
||||
return nameSortOrder || pathSortOrder;
|
||||
}
|
||||
|
||||
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, {
|
||||
|
||||
const templatesMetadata = (
|
||||
await Promise.all(
|
||||
templates.map(async templateUri => {
|
||||
const metadata = await templateMetadata(templateUri);
|
||||
metadata.set('templatePath', path.basename(templateUri.path));
|
||||
return metadata;
|
||||
})
|
||||
)
|
||||
).sort(sortTemplatesMetadata);
|
||||
|
||||
const items: QuickPickItem[] = await Promise.all(
|
||||
templatesMetadata.map(metadata => {
|
||||
const label = metadata.get('name') || metadata.get('templatePath');
|
||||
const description = metadata.get('name')
|
||||
? metadata.get('templatePath')
|
||||
: null;
|
||||
const detail = metadata.get('description');
|
||||
const item = {
|
||||
label: label,
|
||||
description: description,
|
||||
detail: detail,
|
||||
};
|
||||
Object.keys(item).forEach(key => {
|
||||
if (!item[key]) {
|
||||
delete item[key];
|
||||
}
|
||||
});
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
return await window.showQuickPick(items, {
|
||||
placeHolder: 'Select a template to use.',
|
||||
});
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const templateText = await workspace.fs.readFile(
|
||||
Uri.joinPath(templatesDir, selectedTemplate)
|
||||
);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
const variables = findFoamVariables(templateText.toString());
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(variables, givenValues);
|
||||
const subbedText = substituteFoamVariables(
|
||||
templateText.toString(),
|
||||
resolvedValues
|
||||
);
|
||||
const snippet = new SnippetString(subbedText);
|
||||
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
const defaultFileName = `${defaultSlug}.md`;
|
||||
const defaultDir = Uri.joinPath(currentDir, defaultFileName);
|
||||
const filename = await window.showInputBox({
|
||||
async function askUserForFilepathConfirmation(
|
||||
defaultFilepath: Uri,
|
||||
defaultFilename: string
|
||||
) {
|
||||
return await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: defaultDir.fsPath,
|
||||
value: defaultFilepath.fsPath,
|
||||
valueSelection: [
|
||||
defaultDir.fsPath.length - defaultFileName.length,
|
||||
defaultDir.fsPath.length - 3,
|
||||
defaultFilepath.fsPath.length - defaultFilename.length,
|
||||
defaultFilepath.fsPath.length - 3,
|
||||
],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
@@ -181,29 +259,176 @@ async function createNoteFromTemplate(): Promise<void> {
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
if (filename === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function resolveFoamTemplateVariables(
|
||||
templateText: string,
|
||||
extraVariablesToResolve: Set<string> = new Set()
|
||||
): Promise<[Map<string, string>, string]> {
|
||||
const givenValues = new Map<string, string>();
|
||||
const variables = findFoamVariables(templateText.toString()).concat(
|
||||
...extraVariablesToResolve
|
||||
);
|
||||
const uniqVariables = [...new Set(variables)];
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(uniqVariables, givenValues);
|
||||
const subbedText = substituteFoamVariables(
|
||||
templateText.toString(),
|
||||
resolvedValues
|
||||
);
|
||||
return [resolvedValues, subbedText];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function determineDefaultFilepath(
|
||||
resolvedValues: Map<string, string>,
|
||||
templateMetadata: Map<string, string>
|
||||
) {
|
||||
let defaultFilepath: Uri;
|
||||
if (templateMetadata.get('filepath')) {
|
||||
const filepathFromMetadata = templateMetadata.get('filepath');
|
||||
if (isAbsolute(filepathFromMetadata)) {
|
||||
defaultFilepath = Uri.file(filepathFromMetadata);
|
||||
} else {
|
||||
defaultFilepath = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
filepathFromMetadata
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
defaultFilepath = currentDirectoryFilepath(`${defaultSlug}.md`);
|
||||
}
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
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: Map<string, string>,
|
||||
templateWithResolvedVariables: string;
|
||||
try {
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(
|
||||
templateText,
|
||||
new Set(['FOAM_TITLE'])
|
||||
);
|
||||
} 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 [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
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 QuickPickItem).description ||
|
||||
(selectedTemplate as QuickPickItem).label;
|
||||
const templateUri = Uri.joinPath(templatesDir, templateFilename);
|
||||
const templateText = await workspace.fs
|
||||
.readFile(templateUri)
|
||||
.then(bytes => bytes.toString());
|
||||
|
||||
let resolvedValues, templateWithResolvedVariables;
|
||||
try {
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(templateText);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
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 +458,12 @@ const feature: FoamFeature = {
|
||||
createNoteFromTemplate
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-default-template',
|
||||
createNoteFromDefaultTemplate
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-new-template',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { Foam, Logger } from 'foam-core';
|
||||
import { Foam, Logger, URI } from 'foam-core';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../settings';
|
||||
import { isSome } from '../utils';
|
||||
@@ -82,11 +82,19 @@ function generateGraphData(foam: Foam) {
|
||||
title: cutTitle(title),
|
||||
};
|
||||
});
|
||||
foam.workspace.getAllConnections().forEach(c => {
|
||||
foam.graph.getAllConnections().forEach(c => {
|
||||
graph.edges.add({
|
||||
source: c.source.path,
|
||||
target: c.target.path,
|
||||
});
|
||||
if (URI.isPlaceholder(c.target)) {
|
||||
graph.nodes[c.target.path] = {
|
||||
id: c.target.path,
|
||||
type: 'placeholder',
|
||||
uri: c.target,
|
||||
title: c.target.path,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, FoamWorkspace, NoteParser, 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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,10 @@ const feature: FoamFeature = {
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) =>
|
||||
markdownItWithFoamLinks(md, foam.workspace),
|
||||
[markdownItWithFoamTags, markdownItWithFoamLinks].reduce(
|
||||
(acc, extension) => extension(acc, foam.workspace),
|
||||
md
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -30,18 +33,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 +50,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;
|
||||
|
||||
152
packages/foam-vscode/src/features/tags-tree-view/index.spec.ts
Normal file
152
packages/foam-vscode/src/features/tags-tree-view/index.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createTestNote,
|
||||
} from '../../test/test-utils';
|
||||
|
||||
import { Tag, TagReference, TagsProvider } from '.';
|
||||
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
Foam,
|
||||
FileDataStore,
|
||||
FoamConfig,
|
||||
MarkdownResourceProvider,
|
||||
Matcher,
|
||||
} from 'foam-core';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const config: FoamConfig = createConfigFromFolders([]);
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
_foam.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_foam.dispose();
|
||||
});
|
||||
|
||||
it('correctly provides a tag from a set of notes', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['test']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
provider.refresh();
|
||||
|
||||
const treeItems = (await provider.getChildren()) as Tag[];
|
||||
|
||||
treeItems.map(item => expect(item.tag).toContain('test'));
|
||||
});
|
||||
|
||||
it('correctly handles a parent and child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['parent/child']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof Tag) {
|
||||
expect(child.title).toEqual('child');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and multiple child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['parent/child']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
const noteB = createTestNote({
|
||||
tags: new Set(['parent/subchild']),
|
||||
uri: './note-b.md',
|
||||
});
|
||||
_foam.workspace.set(noteB);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.filter(
|
||||
item => item instanceof Tag
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTreeItems).toHaveLength(1);
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof Tag) {
|
||||
expect(['child', 'subchild']).toContain(child.title);
|
||||
expect(child.title).not.toEqual('parent');
|
||||
}
|
||||
});
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and child tag in the same note', async () => {
|
||||
const noteC = createTestNote({
|
||||
tags: new Set(['main', 'main/subtopic']),
|
||||
title: 'Test note',
|
||||
uri: './note-c.md',
|
||||
});
|
||||
|
||||
_foam.workspace.set(noteC);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.filter(
|
||||
item => item instanceof Tag
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('main');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems
|
||||
.filter(item => item instanceof TagReference)
|
||||
.forEach(item => {
|
||||
expect(item.title).toEqual('Test note');
|
||||
});
|
||||
|
||||
childTreeItems
|
||||
.filter(item => item instanceof Tag)
|
||||
.forEach(item => {
|
||||
expect(['main/subtopic']).toContain(item.tag);
|
||||
expect(item.title).toEqual('subtopic');
|
||||
});
|
||||
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,16 @@
|
||||
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 TAG_SEPARATOR = '/';
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
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 +36,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 +50,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] ?? [];
|
||||
@@ -73,27 +68,52 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
|
||||
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
|
||||
if (element) {
|
||||
const references: TagReference[] = element.notes
|
||||
const nestedTagItems: TagTreeItem[] = this.tags
|
||||
.filter(item => item.tag.indexOf(element.title + TAG_SEPARATOR) > -1)
|
||||
.map(
|
||||
item =>
|
||||
new Tag(
|
||||
item.tag,
|
||||
item.tag.substring(item.tag.indexOf(TAG_SEPARATOR) + 1),
|
||||
item.notes
|
||||
)
|
||||
)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
const references: TagTreeItem[] = element.notes
|
||||
.map(({ uri }) => this.foam.workspace.get(uri))
|
||||
.filter(isNote)
|
||||
.map(note => new TagReference(element.tag, note));
|
||||
.filter(note => note.tags.has(element.tag))
|
||||
.map(note => new TagReference(element.tag, note))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return Promise.resolve([
|
||||
new TagSearch(element.tag),
|
||||
...references.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
new TagSearch(element.title),
|
||||
...nestedTagItems,
|
||||
...references,
|
||||
]);
|
||||
}
|
||||
if (!element) {
|
||||
const tags: Tag[] = this.tags.map(
|
||||
({ tag, notes }) => new Tag(tag, notes)
|
||||
);
|
||||
const tags: Tag[] = this.tags
|
||||
.map(({ tag, notes }) => {
|
||||
const parentTag =
|
||||
tag.indexOf(TAG_SEPARATOR) > 0
|
||||
? tag.substring(0, tag.indexOf(TAG_SEPARATOR))
|
||||
: tag;
|
||||
|
||||
return new Tag(parentTag, parentTag, notes);
|
||||
})
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
array.findIndex(tag => tag.title === value.title) === index
|
||||
);
|
||||
|
||||
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -109,9 +129,10 @@ type TagMetadata = { title: string; uri: URI };
|
||||
export class Tag extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly tag: string,
|
||||
public readonly title: string,
|
||||
public readonly notes: TagMetadata[]
|
||||
) {
|
||||
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
super(title, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.description = `${this.notes.length} reference${
|
||||
this.notes.length !== 1 ? 's' : ''
|
||||
}`;
|
||||
@@ -123,6 +144,7 @@ export class Tag extends vscode.TreeItem {
|
||||
}
|
||||
|
||||
export class TagSearch extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: string) {
|
||||
super(`Search #${tag}`, vscode.TreeItemCollapsibleState.None);
|
||||
const searchString = `#${tag}`;
|
||||
@@ -147,7 +169,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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user