mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31cfeb3034 | ||
|
|
5d11818ffc | ||
|
|
e7ee143544 | ||
|
|
aa311b2688 | ||
|
|
0fca141a7b | ||
|
|
6a4bd341ab | ||
|
|
764750f591 | ||
|
|
2686b9a365 | ||
|
|
5a6ef644bd | ||
|
|
d7c92f8284 | ||
|
|
c2e5e4bf2a | ||
|
|
9d0ba879d2 | ||
|
|
9606dcc64c | ||
|
|
d70e441790 | ||
|
|
dde11f8c6f | ||
|
|
cd9ee4d556 | ||
|
|
a8296c2c88 | ||
|
|
13a340eb1d | ||
|
|
d2dd979e70 | ||
|
|
4989796cb0 | ||
|
|
d24814d065 | ||
|
|
4a410d1f5c | ||
|
|
ccb92ad5ee | ||
|
|
e6512cffa8 | ||
|
|
1fa4f37d96 | ||
|
|
27b9b451ad | ||
|
|
362d6f8e09 | ||
|
|
cef8d2a532 | ||
|
|
22b837f252 | ||
|
|
07e02c2d69 | ||
|
|
931ad7a5b6 | ||
|
|
db7eb9775f | ||
|
|
b25152d115 | ||
|
|
1545079c62 | ||
|
|
4835164902 | ||
|
|
06efdc2865 | ||
|
|
b68fd7e138 | ||
|
|
d8baa2fd36 | ||
|
|
7f587095e8 | ||
|
|
77ad245319 | ||
|
|
b892c783da | ||
|
|
e4f6259104 | ||
|
|
aa197239fc | ||
|
|
8f3c23dd60 | ||
|
|
9a027c08ba | ||
|
|
959d0f1ea1 | ||
|
|
57e32c4349 | ||
|
|
f168f66368 | ||
|
|
103ff12b2d | ||
|
|
96a3afa132 | ||
|
|
d586e63104 | ||
|
|
2fba6e9008 | ||
|
|
cdbb965661 | ||
|
|
0c958e31f6 | ||
|
|
fd84fcfd74 | ||
|
|
becf495edc | ||
|
|
4d99883c03 | ||
|
|
8bd679c751 | ||
|
|
8986ee286c | ||
|
|
51b1af1981 | ||
|
|
4276e8043f | ||
|
|
8d1e9b15ce | ||
|
|
bfcfad32e8 | ||
|
|
abe18cc961 | ||
|
|
9490aa2dad | ||
|
|
6c3f4588b3 | ||
|
|
21b8c5b827 | ||
|
|
c66ed74aea | ||
|
|
3876811fb6 | ||
|
|
d9299ee9d4 | ||
|
|
23e21a62f3 | ||
|
|
e7c8d5a4eb | ||
|
|
3ef1b69b2e | ||
|
|
5859b2a9c6 | ||
|
|
54086fdd7e | ||
|
|
a308dfd109 | ||
|
|
e327115673 | ||
|
|
c019767476 | ||
|
|
5e8b817a82 | ||
|
|
e0acc0ba8c | ||
|
|
df4efc5138 | ||
|
|
1e2b3b1bc3 | ||
|
|
eee364de50 | ||
|
|
3ed2bcd37b | ||
|
|
797aa9f29a | ||
|
|
30ab58485c | ||
|
|
ba98f1990c | ||
|
|
6c1d6868f7 | ||
|
|
e773e1ff68 | ||
|
|
86e2bb1ba0 | ||
|
|
fc2fb6a0ab | ||
|
|
956d0119be | ||
|
|
8ddb6a2d12 | ||
|
|
fb9447630f | ||
|
|
06a5988a52 | ||
|
|
fac2247382 | ||
|
|
b089b997bb | ||
|
|
a504054504 | ||
|
|
a00d18cfbb | ||
|
|
5ca7c3eb52 | ||
|
|
571b6a3528 | ||
|
|
b6c9eac86c | ||
|
|
557330413c | ||
|
|
20894a1166 |
@@ -1031,8 +1031,90 @@
|
||||
"contributions": [
|
||||
"tool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tcheneau",
|
||||
"name": "Tony Cheneau",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/952059?v=4",
|
||||
"profile": "https://amnesiak.org/me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nicholas-l",
|
||||
"name": "Nicholas Latham",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12977174?v=4",
|
||||
"profile": "https://github.com/nicholas-l",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thara",
|
||||
"name": "Tomochika Hara",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1532891?v=4",
|
||||
"profile": "https://thara.dev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dcarosone",
|
||||
"name": "Daniel Carosone",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11495017?v=4",
|
||||
"profile": "https://github.com/dcarosone",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MABruni",
|
||||
"name": "Miguel Angel Bruni Montero",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/100445384?v=4",
|
||||
"profile": "https://github.com/MABruni",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Walshkev",
|
||||
"name": "Kevin Walsh ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/77123083?v=4",
|
||||
"profile": "https://github.com/Walshkev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hereistheusername",
|
||||
"name": "Xinglan Liu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33437051?v=4",
|
||||
"profile": "http://hereistheusername.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Hegghammer",
|
||||
"name": "Thomas Hegghammer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/64712218?v=4",
|
||||
"profile": "http://www.hegghammer.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PiotrAleksander",
|
||||
"name": "Piotr Mrzygłosz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6314591?v=4",
|
||||
"profile": "https://github.com/PiotrAleksander",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitType": "docs"
|
||||
}
|
||||
|
||||
8
.github/workflows/update-docs.yml
vendored
8
.github/workflows/update-docs.yml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- docs/user/**/*
|
||||
- docs/.vscode/**/*
|
||||
- docs/assets/**/*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -20,11 +22,15 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: foam
|
||||
- name: Copy and fixup user docs files
|
||||
- name: Copy and fixup user docs files
|
||||
id: copy
|
||||
run: |
|
||||
rm -r foam-template/docs
|
||||
rm -r foam-template/assets
|
||||
rm -r foam-template/.vscode
|
||||
cp -r foam/docs/user foam-template/docs
|
||||
cp -r foam/docs/assets foam-template/assets
|
||||
cp -r foam/docs/.vscode foam-template/.vscode
|
||||
|
||||
# Strip autogenerated wikileaks references because
|
||||
# they are not an appropriate default user experience.
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.vscode-test/
|
||||
.vscode-test-web/
|
||||
*.tsbuildinfo
|
||||
*.vsix
|
||||
*.log
|
||||
|
||||
10
docs/.vscode/extensions.json
vendored
10
docs/.vscode/extensions.json
vendored
@@ -5,17 +5,11 @@
|
||||
// Foam's own extension
|
||||
"foam.foam-vscode",
|
||||
|
||||
// Prettier for auto formatting code
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// GitLens for seeing version history inline
|
||||
"eamodio.gitlens",
|
||||
|
||||
// Tons of markdown goodies (lists, tables of content, so much more)
|
||||
"yzhang.markdown-all-in-one",
|
||||
|
||||
// Graph visualizer
|
||||
"tchayen.markdown-links",
|
||||
// Prettier for auto formatting code
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// Understated grayscale theme (light and dark variants)
|
||||
"philipbe.theme-gray-matter"
|
||||
|
||||
4
docs/.vscode/foam.json
vendored
4
docs/.vscode/foam.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"purpose": "this file exists to tell the foam-vscode plugin that it's currently in a foam workspace",
|
||||
"future": "we may use this for custom configuration"
|
||||
}
|
||||
2
docs/.vscode/keybindings.json
vendored
2
docs/.vscode/keybindings.json
vendored
@@ -3,6 +3,6 @@
|
||||
[
|
||||
{
|
||||
"key": "cmd+shift+n",
|
||||
"command": "vscodeMarkdownNotes.newNote"
|
||||
"command": "foam-vscode.create-note"
|
||||
}
|
||||
]
|
||||
|
||||
13
docs/.vscode/settings.json
vendored
13
docs/.vscode/settings.json
vendored
@@ -5,7 +5,6 @@
|
||||
"editor.overviewRulerBorder": false,
|
||||
"editor.lineHeight": 24,
|
||||
"foam.edit.linkReferenceDefinitions": "withExtensions",
|
||||
"vscodeMarkdownNotes.noteCompletionConvention": "noExtension",
|
||||
"[markdown]": {
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
@@ -13,21 +12,11 @@
|
||||
"strings": false
|
||||
}
|
||||
},
|
||||
"cSpell.language": "en-GB",
|
||||
"git.enableSmartCommit": true,
|
||||
"git.postCommitCommand": "sync",
|
||||
"spellright.language": [
|
||||
"en"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"files.exclude": {
|
||||
"_site/**": true
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"markdown.styles": [
|
||||
".vscode/custom-tag-style.css"
|
||||
]
|
||||
"markdown.styles": [".vscode/custom-tag-style.css"]
|
||||
}
|
||||
|
||||
1
docs/.vscode/spellright.dict
vendored
1
docs/.vscode/spellright.dict
vendored
@@ -1 +0,0 @@
|
||||
Backlinking
|
||||
BIN
docs/assets/images/note-embed-type-demo.gif
Normal file
BIN
docs/assets/images/note-embed-type-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 568 KiB |
BIN
docs/assets/images/pdf_output.png
Normal file
BIN
docs/assets/images/pdf_output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,3 +1,8 @@
|
||||
---
|
||||
redirect_from:
|
||||
- /code-of-conduct
|
||||
---
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
We follow the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct.
|
||||
|
||||
@@ -93,7 +93,7 @@ After you have made your changes to your copy of the project, it is time to try
|
||||
|
||||
1. Return to the project's [home repository page](https://github.com/foambubble/foam).
|
||||
2. Github should show you an button called "Compare & pull request" linking your forked repository to the community repository.
|
||||
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
|
||||
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://blog.robsewell.com/blog/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
|
||||
4. Add as many relevant details to the PR message to make it clear to the project maintainers and other members of the community what you have accomplished with your new changes. Link to any issues the changes are related to.
|
||||
5. Your PR will then need to be reviewed and accepted by the other members of the community. Any discussion about the changes will occur in your PR thread.
|
||||
6. Once reviewed and accept you can complete the merge request!
|
||||
|
||||
@@ -251,6 +251,19 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=60" width="60px;" alt="Zhizhen He"/><br /><sub><b>Zhizhen He</b></sub></a><br /><a href="#tool-hezhizhen" title="Tools">🔧</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://amnesiak.org/me"><img src="https://avatars.githubusercontent.com/u/952059?v=4?s=60" width="60px;" alt="Tony Cheneau"/><br /><sub><b>Tony Cheneau</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tcheneau" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-l"><img src="https://avatars.githubusercontent.com/u/12977174?v=4?s=60" width="60px;" alt="Nicholas Latham"/><br /><sub><b>Nicholas Latham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nicholas-l" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Walshkev"><img src="https://avatars.githubusercontent.com/u/77123083?v=4?s=60" width="60px;" alt="Kevin Walsh "/><br /><sub><b>Kevin Walsh </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Walshkev" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hereistheusername.github.io/"><img src="https://avatars.githubusercontent.com/u/33437051?v=4?s=60" width="60px;" alt="Xinglan Liu"/><br /><sub><b>Xinglan Liu</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hereistheusername" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hegghammer.com"><img src="https://avatars.githubusercontent.com/u/64712218?v=4?s=60" width="60px;" alt="Thomas Hegghammer"/><br /><sub><b>Thomas Hegghammer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Hegghammer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PiotrAleksander"><img src="https://avatars.githubusercontent.com/u/6314591?v=4?s=60" width="60px;" alt="Piotr Mrzygłosz"/><br /><sub><b>Piotr Mrzygłosz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=PiotrAleksander" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Backlinking
|
||||
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the **Backlinks Explorer**
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the **Connections Explorer**
|
||||
|
||||
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "backlinks" and run the **Explorer: Focus on Backlinks** view.
|
||||
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "connections" and run the **Explorer: Focus on Connections** view.
|
||||
- Keep this pane always visible to discover relationships between your thoughts
|
||||
- You can drag the backlinks pane to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
|
||||
- You can drag the connections panel to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
|
||||
- You can filter the connections to see just backlinks, forward links, or all connections
|
||||
- Finding backlinks in published Foam workspaces via [[materialized-backlinks]] is on the [[roadmap]] but not yet implemented.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
29
docs/user/features/built-in-note-embedding-types.md
Normal file
29
docs/user/features/built-in-note-embedding-types.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Built-In Note Embedding Types
|
||||
|
||||
When embedding a note, there are a few ways to modify the scope of the content as well as its display style. The following are Foam keywords that are used to describe note embedding.
|
||||
|
||||
Note, this only applies to note embedding, not embedding of attachments or images.
|
||||
|
||||

|
||||
|
||||
## Scope
|
||||
|
||||
- `full` - the entire note in the case of `![[note]]` or the entire section in the case of `![[note#section1]]`
|
||||
- `content` - everything excluding the title of the section. So the entire note minus the title for `![[note]]`, or the entire section minus the section header for `![[note#section1]]`
|
||||
|
||||
## Style
|
||||
|
||||
- `card` - outlines the embedded note with a border
|
||||
- `inline` - adds the note continuously as if the text were part of the calling note
|
||||
|
||||
## Default Setting
|
||||
|
||||
Foam expresses note display type as `<scope>-<style>`.
|
||||
|
||||
By default, Foam configures note embedding to be `full-card`. That is, whenever the standard embedding syntax is used, `![[note]]`, the note will have `full` scope and `card` style display. This setting is stored under `foam.preview.embedNoteStyle` and can be modified.
|
||||
|
||||
## Explicit Modifiers
|
||||
|
||||
Prepend the wikilink with one of the scope or style keywords, or a combination of the two to explicitly modify a note embedding if you would like to override the default setting.
|
||||
|
||||
For example, given your `foam.embedNoteStyle` is set to `content-card`, embedding a note with standard syntax `![[note-a]]` would show a bordered note without its title. Say, for a specific `note-b` you would like to display the title. You can simply use one of the above keywords to override your default setting like so: `full![[note-b]]`. In this case, `full` overrides the default `content` scope and because a style is not specified, it falls back to the default style setting, `card`. If you would like it to be inline, override that as well: `full-inline![[note-b]]`.
|
||||
@@ -10,13 +10,13 @@ This command creates a note.
|
||||
Although it works fine on its own, it can be customized to achieve various use cases.
|
||||
Here are the settings available for the command:
|
||||
|
||||
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
|
||||
- templatePath: The path of the template to use. If relative it will be resolved against the workspace root.
|
||||
- title: The title of the note (that is, the `FOAM_TITLE` variable)
|
||||
- text: The text to use for the note. If also a template is provided, the template has precedence
|
||||
- variables: Variables to use in the text or template
|
||||
- date: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
|
||||
- onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel': What to do in case the target file already exists
|
||||
- `notePath`: The path of the note to create. If relative it will be resolved against the workspace root.
|
||||
- `templatePath`: The path of the template to use. If relative it will be resolved against the workspace root.
|
||||
- `title`: The title of the note (that is, the `FOAM_TITLE` variable)
|
||||
- `text`: The text to use for the note. If also a template is provided, the template has precedence
|
||||
- `variables`: Variables to use in the text or template
|
||||
- `date`: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
|
||||
- `onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel'`: What to do in case the target file already exists
|
||||
|
||||
To customize a command and associate a key binding to it, open the key binding settings and add the appropriate configuration, here are some examples:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Daily Notes
|
||||
|
||||
Daily notes allow you to quickly create and access a new notes file for each day. This is a surpisingly effective and increasingly common strategy to organize notes and manage events.
|
||||
Daily notes allow you to quickly create and access a new notes file for each day. This is a surprisingly effective and increasingly common strategy to organize notes and manage events.
|
||||
|
||||
View today's note file by running the `Foam: Open Daily Note` command, by using the shortcut `alt+d` (note: shortcuts can be [overridden](https://code.visualstudio.com/docs/getstarted/keybindings)), or by using [#snippets](#Snippets). The name, location, and title of daily notes files is [#configurable](#Configuration).
|
||||
View today's note file by running the `Foam: Open Daily Note` command, by using the shortcut `alt+d` (note: shortcuts can be [overridden](https://code.visualstudio.com/docs/getstarted/keybindings)), or by using [#snippets](#Snippets). The name, location, and title of daily notes files are [#configurable](#Configuration).
|
||||
|
||||
## Roam-style Automatic Daily Notes
|
||||
|
||||
@@ -29,11 +29,11 @@ Create a link to a recent daily note using [snippets](https://code.visualstudio.
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the workspace's `journals` folder, with a heading `yyyy-mm-dd`.
|
||||
By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the workspace's `journals` folder, with the heading `yyyy-mm-dd`.
|
||||
|
||||
These settings can be overridden in your workspace or global `.vscode/settings.json` file, using the [**dateformat** date masking syntax](https://github.com/felixge/node-dateformat#mask-options):
|
||||
|
||||
It's possible to customize path and heading of your daily notes, by following the [dateformat masking syntax](https://github.com/felixge/node-dateformat#mask-options).
|
||||
It's possible to customize the path and heading of your daily notes, by following the [dateformat masking syntax](https://github.com/felixge/node-dateformat#mask-options).
|
||||
The following properties can be used:
|
||||
|
||||
```json
|
||||
@@ -45,7 +45,7 @@ The following properties can be used:
|
||||
|
||||
The above configuration would create a file `journal/daily-note-2020-07-25.mdx`, with the heading `Journal Entry, Sunday, July 25`.
|
||||
|
||||
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
|
||||
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically, see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
|
||||
|
||||
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
|
||||
|
||||
|
||||
@@ -8,13 +8,16 @@ Including a note can be done by adding an `!` before a wikilink definition. For
|
||||
|
||||
## Custom styling
|
||||
|
||||
Displaying the inclusion of notes allows for some custom styling, see [[custom-markdown-preview-styles]]
|
||||
To modify how an embedded note looks and the scope of its content, see [[built-in-note-embedding-types]]
|
||||
|
||||
For more fine-grained custom styling, see [[custom-markdown-preview-styles]]
|
||||
|
||||
## Future possibilities
|
||||
|
||||
Work on this feature is evolving and progressing. See the [[inclusion-of-notes]] proposal for the current discussion.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md "Custom Markdown Preview Styles"
|
||||
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md "Inclusion of notes Proposal "
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[built-in-note-embedding-types]: built-in-note-embedding-types.md 'Built-In Note Embedding Types'
|
||||
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md 'Custom Markdown Preview Styles'
|
||||
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md 'Inclusion of notes Proposal '
|
||||
[//end]: # 'Autogenerated link references'
|
||||
|
||||
@@ -58,6 +58,7 @@ In addition, you can also use variables provided by Foam:
|
||||
| -------------------- | ------------ |
|
||||
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| `FOAM_TITLE_SAFE` | The title of the note in a file system safe format. If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, `FOAM_DATE_WEEK` etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
|
||||
|
||||
@@ -193,7 +194,7 @@ You can add the template metadata to its own YAML Frontmatter block at the start
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
@@ -205,7 +206,7 @@ If the note already has a Frontmatter block, a Foam-specific Frontmatter block c
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
@@ -20,7 +20,10 @@ Remember, with `CTRL/CMD+click` on a wikilink you can navigate to the note, or c
|
||||
|
||||
## Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
- If it's an external file, `[your link will need the filename](other-file.md#that-section-I-want-to-link-to)`, but
|
||||
- if it's an anchor within the same document, `[you just need an octothorpe and the section name](#that-section-above)`.
|
||||
- Doesn't matter what heading-level the anchor is; whether you're linking to an `H1` like `# MEN WALK ON MOON` or an `H2` like `## Astronauts Land on Plain`, the link syntax uses a single octothorpe: `[Walk!](#men-walk-on-moon)` and `[Land!](#astronauts-land-on-plain-collect-rocks-plant-flag)`. Autocomplete is your friend here.
|
||||
|
||||
## Markdown compatibility
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ This list is subject to change.
|
||||
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (alpha)
|
||||
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
|
||||
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## 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.
|
||||
These extensions are not defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
|
||||
|
||||
- [Emojisense](https://marketplace.visualstudio.com/items?itemName=bierner.emojisense)
|
||||
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
This #recipe allows you to paste images on to your notes.
|
||||
|
||||
You can directly link and paste images that are copied to the clipboard using either the [Paste
|
||||
Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
|
||||
extension, or the [Markdown Image](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) extension.
|
||||
VScode (since
|
||||
[1.79](https://code.visualstudio.com/updates/v1_79#_copy-external-media-files-into-workspace-on-drop-or-paste-for-markdown))
|
||||
now has the ability to paste images from the clipboard, or drag-and-drop image
|
||||
files, directly into markdown documents. The file will be created in the
|
||||
workspace, and a link generated in Markdown format.
|
||||
|
||||
The former does not have MDX support (yet), the latter does.
|
||||
VSCode settings under `Markdown > Copy Files` and `Markdown > Editor > Drop` can
|
||||
be used to configure where the files get placed in your workspace, how they're
|
||||
named, how conflicts with existing files are handled, and more.
|
||||
|
||||
@@ -29,6 +29,9 @@ on:
|
||||
jobs:
|
||||
store_data:
|
||||
runs-on: ubuntu-latest
|
||||
# If you encounter a 403 error from a workflow run, try uncommenting the following 2 lines (taken from: https://stackoverflow.com/questions/75880266/cant-make-push-on-a-repo-with-github-actions accepted answer)
|
||||
# permissions:
|
||||
# contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: anglinb/foam-capture-action@main
|
||||
|
||||
53
docs/user/recipes/export-to-pdf.md
Normal file
53
docs/user/recipes/export-to-pdf.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Export to PDF
|
||||
|
||||
This #recipe shows how to export a note to PDF.
|
||||
|
||||
## Required extensions
|
||||
|
||||
- **[vscode-pandoc](https://marketplace.visualstudio.com/items?itemName=chrischinchilla.vscode-pandoc)**
|
||||
|
||||
## Required third-party tools
|
||||
|
||||
- [Pandoc](https://pandoc.org/installing.html)
|
||||
- A [LaTeX distribution](https://www.latex-project.org/get/) such as TeXLive (Linux), MacTeX (MacOS), or MikTeX (Windows)
|
||||
|
||||
Check that Pandoc is installed by opening a terminal and running `pandoc --version`.
|
||||
|
||||
Check that Pandoc can produce PDFs with LaTeX by running the following in the terminal.
|
||||
|
||||
```
|
||||
echo It is working > test.md
|
||||
pandoc test.md -o test.pdf
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Create a folder in your workspace named `.pandoc`. Take note of the full path to this directory. The rest of this recipe will refer to this path as `$WORKSPACE/.pandoc`.
|
||||
|
||||
2. Download the template file [`foam.latex`](https://raw.githubusercontent.com/Hegghammer/foam-templates/main/foam.latex) from [Hegghammer/foam-templates](https://github.com/Hegghammer/foam-templates) and place it in `$WORKSPACE/.pandoc`.
|
||||
|
||||
3. In VSCode, open `settings.json` for your user (or just for your workspace if you prefer), and add the following line:
|
||||
|
||||
```
|
||||
"pandoc.pdfOptString": "--from=markdown+wikilinks_title_after_pipe --resource-path $WORKSPACE/.pandoc --template foam --listings",
|
||||
```
|
||||
|
||||
Make sure to replace `$WORKSPACE/.pandoc` with the real full path to the `.pandoc` directory you created earlier.
|
||||
|
||||
4. Open a Foam note in VSCode.
|
||||
|
||||
5. Press `Ctrl` + `k`, `p`. Choose "pdf", and press `Enter`.
|
||||
|
||||
The PDF should look something like this:
|
||||
|
||||

|
||||
|
||||
## Options
|
||||
|
||||
If you include a name in the `author` parameter in the YAML of the Foam note, that name will feature in the PDF header on the top left.
|
||||
|
||||
If you don't want syntax highlighting and frames around the codeblocks, remove `--listings` from the `pandoc.pdfOptString` parameter in `settings.json`.
|
||||
|
||||
## Further customization
|
||||
|
||||
If you know some LaTeX, you can [tweak](https://bookdown.org/yihui/rmarkdown-cookbook/latex-template.html) the `foam.latex` template to your needs. Alternatively, you can supply another ready-made template such as [Eisvogel](https://github.com/Wandmalfarbe/pandoc-latex-template); just place the `TEMPLATE_NAME.latex` file in `$WORKSPACE/.pandoc`. You can also use all of Pandoc's [other functionalities](https://learnbyexample.github.io/customizing-pandoc/) by tweaking the `pandoc.pdfOptString` parameter in `settings.json`.
|
||||
@@ -1,19 +1,21 @@
|
||||
<!-- omit in toc -->
|
||||
|
||||
# Recipes
|
||||
|
||||
A #recipe is a guide, tip or strategy for getting the most out of your Foam workspace!
|
||||
|
||||
- [Contribute](#contribute)
|
||||
- [Take smart notes](#take-smart-notes)
|
||||
- [Discover](#discover)
|
||||
- [Organise](#organise)
|
||||
- [Write](#write)
|
||||
- [Version control](#version-control)
|
||||
- [Publish](#publish)
|
||||
- [Collaborate](#collaborate)
|
||||
- [Workflow](#workflow)
|
||||
- [Creative ideas](#creative-ideas)
|
||||
- [Other](#other)
|
||||
- [Recipes](#recipes)
|
||||
- [Contribute](#contribute)
|
||||
- [Take smart notes](#take-smart-notes)
|
||||
- [Discover](#discover)
|
||||
- [Organise](#organise)
|
||||
- [Write](#write)
|
||||
- [Version control](#version-control)
|
||||
- [Publish](#publish)
|
||||
- [Collaborate](#collaborate)
|
||||
- [Workflow](#workflow)
|
||||
- [Creative ideas](#creative-ideas)
|
||||
- [Other](#other)
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -75,11 +77,11 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Publish using community templates
|
||||
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
|
||||
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
|
||||
- [foamy-nextjs](https://github.com/yenly/foamy-nextjs) by [@yenly](https://github.com/yenly)
|
||||
- Make the site your own by [[publish-to-github]].
|
||||
- Render math symbols, by either
|
||||
- adding client-side [[math-support-with-mathjax]] to the default [[publish-to-github-pages]] site
|
||||
- adding a custom Jekyll plugin to support [[math-support-with-katex]]
|
||||
- Export note to PDF [[export-to-pdf]]
|
||||
|
||||
## Collaborate
|
||||
|
||||
@@ -140,6 +142,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[publish-to-github]: ../publishing/publish-to-github.md "Publish to GitHub"
|
||||
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
|
||||
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
|
||||
[export-to-pdf]: export-to-pdf.md "Export to PDF"
|
||||
[real-time-collaboration]: real-time-collaboration.md "Real-time Collaboration"
|
||||
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
|
||||
[capture-notes-with-shortcuts-and-github-actions]: capture-notes-with-shortcuts-and-github-actions.md "Capture Notes With Shortcuts and GitHub Actions"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.24.0"
|
||||
"version": "0.26.4"
|
||||
}
|
||||
|
||||
31816
package-lock.json
generated
Normal file
31816
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "yarn workspace foam-vscode test",
|
||||
"test": "yarn workspace foam-vscode test --stream",
|
||||
"lint": "lerna run lint",
|
||||
"watch": "lerna run watch --concurrency 20 --stream"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ out/**/*.spec.*
|
||||
test-data/**
|
||||
src/**
|
||||
jest.config.js
|
||||
esbuild.js
|
||||
.test-workspace
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
|
||||
@@ -4,6 +4,134 @@ 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.26.4] - 2024-11-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved handling of virtual FS URIs (#1409)
|
||||
|
||||
## [0.26.3] - 2024-11-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Finetuned use of triemap (#1411 - thanks @pderaaij)
|
||||
|
||||
## [0.26.2] - 2024-11-06
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Performance improvements (#1406 - thanks @pderaaij)
|
||||
|
||||
## [0.26.1] - 2024-10-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed issue with Buffer in web extension (#1401 - thanks @pderaaij)
|
||||
|
||||
## [0.26.0] - 2024-10-01
|
||||
|
||||
Features:
|
||||
|
||||
- Foam is now a web extension! (#1395 - many thanks @pderaaij)
|
||||
|
||||
## [0.25.12] - 2024-07-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved YAML support (#1367)
|
||||
- Added convesion of wikilinks to markdown links (#1365 - thanks @hereistheusername)
|
||||
- Refactored util and settings code
|
||||
|
||||
## [0.25.11] - 2024-03-18
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Actually fixed bug in graph computation (#1345)
|
||||
|
||||
## [0.25.10] - 2024-03-18
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed bug in graph computation (#1345)
|
||||
|
||||
## [0.25.9] - 2024-03-17
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved note creation from placeholder (#1344)
|
||||
|
||||
## [0.25.8] - 2024-02-21
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Upgraded dataformat to improve support for daily note naming (#1326 - thanks @rcyeh)
|
||||
|
||||
## [0.25.7] - 2024-01-16
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Modifies url encoding to target only the filename and skip spaces (#1322 - thanks @MABruni)
|
||||
- Minor tweak to quick action menu with suggestions for section name
|
||||
|
||||
## [0.25.6] - 2023-12-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed wikilink definition encoding (#1311 - thanks @MABruni)
|
||||
|
||||
## [0.25.5] - 2023-11-30
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Using note title in preview (#1309)
|
||||
|
||||
## [0.25.4] - 2023-09-19
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for linking sections within same document (#1289)
|
||||
- Fixed note embedding bug (#1286 - thanks @badsketch)
|
||||
|
||||
## [0.25.3] - 2023-09-07
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed incorrect handling of embedding of non-existing notes (#1283 - thanks @badsketch)
|
||||
- Introduced Note Embedding Sytanx (#1281 - thanks @badsketch)
|
||||
- Attachments are not considered when computing orphan notes (#1242)
|
||||
|
||||
## [0.25.2] - 2023-09-02
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added content-only embed styles (#1279 - thanks @badsketch)
|
||||
- Added expand-all button to tree views (#1276)
|
||||
|
||||
## [0.25.1] - 2023-08-23
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for path parameter in filter (#1250)
|
||||
- Added grouping and filtering to tag explorer (#1275)
|
||||
- Added new setting to control note embedding (#1273 - thanks @badsketch)
|
||||
- Added last week's days to snippets (#1248 - thanks @jimgraham)
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated jest to v29 (#1271 - thanks @nicholas-l)
|
||||
- Improved test cleanup and management (#1274)
|
||||
|
||||
## [0.25.0] - 2023-06-30
|
||||
|
||||
Features:
|
||||
|
||||
- Support for multiple extensions and custom default extension (#1235)
|
||||
- Added `FOAM_TITLE_SAFE` template variable (#1232)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Connections panel tweaks (#1233)
|
||||
|
||||
## [0.24.0] - 2023-05-19
|
||||
|
||||
Features:
|
||||
|
||||
111
packages/foam-vscode/esbuild.js
Normal file
111
packages/foam-vscode/esbuild.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// also see https://code.visualstudio.com/api/working-with-extensions/bundling-extension
|
||||
const assert = require('assert');
|
||||
const esbuild = require('esbuild');
|
||||
const polyfillPlugin = require('esbuild-plugin-polyfill-node');
|
||||
|
||||
// pass the platform to esbuild as an argument
|
||||
|
||||
function getPlatform() {
|
||||
const args = process.argv.slice(2);
|
||||
const pArg = args.find(arg => arg.startsWith('--platform='));
|
||||
if (pArg) {
|
||||
return pArg.split('=')[1];
|
||||
}
|
||||
throw new Error('No platform specified. Pass --platform <web|node>.');
|
||||
}
|
||||
|
||||
const platform = getPlatform();
|
||||
assert(['web', 'node'].includes(platform), 'Platform must be "web" or "node".');
|
||||
|
||||
const production = process.argv.includes('--production');
|
||||
const watch = process.argv.includes('--watch');
|
||||
|
||||
const config = {
|
||||
web: {
|
||||
platform: 'browser',
|
||||
format: 'cjs',
|
||||
outfile: `out/bundles/extension-web.js`,
|
||||
plugins: [
|
||||
polyfillPlugin.polyfillNode({
|
||||
// Options (optional)
|
||||
}),
|
||||
{
|
||||
name: 'path-browserify',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^path$/ }, args => {
|
||||
return { path: require.resolve('path-browserify') };
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'wikilink-embed',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /wikilink-embed/ }, args => {
|
||||
return {
|
||||
path: require.resolve(
|
||||
args.resolveDir + '/wikilink-embed-web-extension.ts'
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: `out/bundles/extension-node.js`,
|
||||
plugins: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const ctx = await esbuild.context({
|
||||
...config[platform],
|
||||
entryPoints: ['src/extension.ts'],
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
external: ['vscode'],
|
||||
logLevel: 'silent',
|
||||
plugins: [
|
||||
...config[platform].plugins,
|
||||
/* add to the end of plugins array */
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
});
|
||||
if (watch) {
|
||||
await ctx.watch();
|
||||
} else {
|
||||
await ctx.rebuild();
|
||||
await ctx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const esbuildProblemMatcherPlugin = {
|
||||
name: 'esbuild-problem-matcher',
|
||||
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log('[watch] build started');
|
||||
});
|
||||
build.onEnd(result => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`);
|
||||
console.error(
|
||||
` ${location.file}:${location.line}:${location.column}:`
|
||||
);
|
||||
});
|
||||
console.log('[watch] build finished');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.24.0",
|
||||
"version": "0.26.4",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -21,7 +21,8 @@
|
||||
"activationEvents": [
|
||||
"workspaceContains:.vscode/foam.json"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"main": "./out/bundles/extension-node.js",
|
||||
"browser": "./out/bundles/extension-web.js",
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": "limited",
|
||||
@@ -109,17 +110,17 @@
|
||||
"view/title": [
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:backlinks",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'connections'",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'all links'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:links",
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'backlinks'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:connections",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'links'",
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'forward links'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
@@ -132,6 +133,31 @@
|
||||
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'all'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'for-current-file'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'off'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"when": "view == foam-vscode.tags-explorer",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'all'",
|
||||
@@ -152,6 +178,11 @@
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"when": "view == foam-vscode.placeholders",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'all'",
|
||||
@@ -161,6 +192,11 @@
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'notes-only'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"when": "view == foam-vscode.notes-explorer",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
@@ -173,7 +209,7 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:connections",
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
@@ -181,7 +217,7 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:links",
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
@@ -192,6 +228,26 @@
|
||||
"command": "foam-vscode.views.orphans.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"when": "false"
|
||||
@@ -208,6 +264,10 @@
|
||||
"command": "foam-vscode.views.placeholders.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"when": "false"
|
||||
@@ -216,6 +276,10 @@
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"when": "false"
|
||||
@@ -283,6 +347,14 @@
|
||||
"command": "foam-vscode.open-resource",
|
||||
"title": "Foam: Open Resource"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-inplace",
|
||||
"title": "Foam: convert link style in place"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-incopy",
|
||||
"title": "Foam: convert link format in copy"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:folder",
|
||||
"title": "Group By Folder",
|
||||
@@ -294,12 +366,12 @@
|
||||
"icon": "$(arrow-left)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:links",
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"title": "Show Links",
|
||||
"icon": "$(arrow-right)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:connections",
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"title": "Show All",
|
||||
"icon": "$(arrow-swap)"
|
||||
},
|
||||
@@ -308,6 +380,31 @@
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"title": "Show tags in current file",
|
||||
"icon": "$(file)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"title": "Show tags in workspace",
|
||||
"icon": "$(files)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"title": "Group By Folder",
|
||||
"icon": "$(list-tree)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"title": "Show placeholders in current file",
|
||||
@@ -328,11 +425,21 @@
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"title": "Show all resources",
|
||||
"icon": "$(files)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"title": "Show only notes",
|
||||
@@ -395,6 +502,16 @@
|
||||
"default": "pdf mp3 webm wav m4a mp4 avi mov rtf txt doc docx pages xls xlsx numbers ppt pptm pptx",
|
||||
"description": "Space separated list of file extensions that will be considered attachments"
|
||||
},
|
||||
"foam.files.notesExtensions": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Space separated list of extra file extensions that will be considered text notes (e.g. 'mdx txt markdown')"
|
||||
},
|
||||
"foam.files.defaultNoteExtension": {
|
||||
"type": "string",
|
||||
"default": "md",
|
||||
"description": "The default extension for new notes"
|
||||
},
|
||||
"foam.files.newNotePath": {
|
||||
"type": "string",
|
||||
"default": "root",
|
||||
@@ -501,10 +618,21 @@
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.preview.embedNoteInContainer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Wrap embedded notes in a container when displayed in preview panel"
|
||||
"foam.preview.embedNoteType": {
|
||||
"type": "string",
|
||||
"default": "full-card",
|
||||
"enum": [
|
||||
"full-inline",
|
||||
"full-card",
|
||||
"content-inline",
|
||||
"content-card"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Include the section with title and style inline",
|
||||
"Include the section with title and style it within a container",
|
||||
"Include the section without title and style inline",
|
||||
"Include the section without title and style it within a container"
|
||||
]
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
@@ -530,28 +658,30 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p ./",
|
||||
"pretest": "yarn build",
|
||||
"test": "node ./out/test/run-tests.js",
|
||||
"pretest:unit": "yarn build",
|
||||
"test:unit": "node ./out/test/run-tests.js --unit",
|
||||
"pretest:e2e": "yarn build",
|
||||
"test:e2e": "node ./out/test/run-tests.js --e2e",
|
||||
"build:node": "node esbuild.js --platform=node",
|
||||
"build:web": "node esbuild.js --platform=web",
|
||||
"build": "yarn build:node && yarn build:web",
|
||||
"vscode:prepublish": "yarn clean && yarn build:node --production && yarn build:web --production",
|
||||
"compile": "tsc -p ./",
|
||||
"test-reset-workspace": "rm -rf .test-workspace && mkdir .test-workspace && touch .test-workspace/.keep",
|
||||
"test-setup": "yarn compile && yarn build && yarn test-reset-workspace",
|
||||
"test": "yarn test-setup && node ./out/test/run-tests.js",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
|
||||
"lint": "dts lint src",
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
|
||||
"vscode:prepublish": "yarn run esbuild-base -- --minify",
|
||||
"package-extension": "npx vsce package --yarn",
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"open-in-browser": "vscode-test-web --quality=stable --browser=chromium --extensionDevelopmentPath=. ",
|
||||
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
|
||||
"publish-extension-vscode": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
@@ -561,39 +691,44 @@
|
||||
"@types/vscode": "^1.70.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@vscode/test-web": "^0.0.62",
|
||||
"dts-cli": "^1.6.3",
|
||||
"esbuild": "^0.17.7",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^27.5.1",
|
||||
"jest": "^29.6.2",
|
||||
"jest-extended": "^3.2.3",
|
||||
"markdown-it": "^12.0.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vscode-test": "^1.3.0",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"dateformat": "4.5.1",
|
||||
"detect-newline": "^3.1.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"js-sha1": "^0.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"mnemonist": "^0.39.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-wiki-link": "^0.0.4",
|
||||
"title-case": "^3.0.2",
|
||||
"unified": "^9.0.0",
|
||||
"unist-util-visit": "^2.0.2",
|
||||
"yaml": "^1.10.0"
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"__metadata": {
|
||||
"id": "b85c6625-454b-4b61-8a22-c42f3d0f2e1e",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { convertLinkFormat } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { Resource } from '../model/note';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { Logger } from '../utils/log';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { FileDataStore } from '../../test/test-datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateStdMdLink', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
// TODO slug must be reserved for actual slugs, not file names
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
const readFile = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
const dataStore = new FileDataStore(
|
||||
readFile,
|
||||
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
|
||||
);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_workspace.list().length).toEqual(11);
|
||||
});
|
||||
|
||||
it('can generate markdown links correctly', async () => {
|
||||
const note = findBySlug('file-with-different-link-formats');
|
||||
const actual = note.links
|
||||
.filter(link => link.type === 'wikilink')
|
||||
.map(link => convertLinkFormat(link, 'link', _workspace, note));
|
||||
const expected: string[] = [
|
||||
'[first-document](first-document.md)',
|
||||
'[second-document](second-document.md)',
|
||||
'[[non-exist-file]]',
|
||||
'[#one section](<file-with-different-link-formats.md#one section>)',
|
||||
'[another name](<file-with-different-link-formats.md#one section>)',
|
||||
'[an alias](first-document.md)',
|
||||
'[first-document](first-document.md)',
|
||||
];
|
||||
expect(actual.length).toEqual(expected.length);
|
||||
actual.forEach((LinkReplace, index) => {
|
||||
expect(LinkReplace.newText).toEqual(expected[index]);
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate wikilinks correctly', async () => {
|
||||
const note = findBySlug('file-with-different-link-formats');
|
||||
const actual = note.links
|
||||
.filter(link => link.type === 'link')
|
||||
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
|
||||
const expected: string[] = ['[[first-document|file]]'];
|
||||
expect(actual.length).toEqual(expected.length);
|
||||
actual.forEach((LinkReplace, index) => {
|
||||
expect(LinkReplace.newText).toEqual(expected[index]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Resource, ResourceLink } from '../model/note';
|
||||
import { URI } from '../model/uri';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { isNone } from '../utils';
|
||||
import { MarkdownLink } from '../services/markdown-link';
|
||||
|
||||
export interface LinkReplace {
|
||||
newText: string;
|
||||
range: Range /* old range */;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert a link based on its workspace and the note containing it.
|
||||
* According to targetFormat parameter to decide output format. If link.type === targetFormat, then it simply copy
|
||||
* the rawText into LinkReplace. Therefore, it's recommended to filter before conversion.
|
||||
* If targetFormat isn't supported, or the target resource pointed by link cannot be found, the function will throw
|
||||
* exception.
|
||||
* @param link
|
||||
* @param targetFormat 'wikilink' | 'link'
|
||||
* @param workspace
|
||||
* @param note
|
||||
* @returns LinkReplace { newText: string; range: Range; }
|
||||
*/
|
||||
export function convertLinkFormat(
|
||||
link: ResourceLink,
|
||||
targetFormat: 'wikilink' | 'link',
|
||||
workspace: FoamWorkspace,
|
||||
note: Resource | URI
|
||||
): LinkReplace {
|
||||
const resource = note instanceof URI ? workspace.find(note) : note;
|
||||
const targetUri = workspace.resolveLink(resource, link);
|
||||
/* If it's already the target format or a placeholder, no transformation happens */
|
||||
if (link.type === targetFormat || targetUri.scheme === 'placeholder') {
|
||||
return {
|
||||
newText: link.rawText,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
|
||||
let { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
let sectionDivider = section ? '#' : '';
|
||||
|
||||
if (isNone(targetUri)) {
|
||||
throw new Error(
|
||||
`Unexpected state: link to: "${link.rawText}" is not resolvable`
|
||||
);
|
||||
}
|
||||
|
||||
const targetRes = workspace.find(targetUri);
|
||||
let relativeUri = targetRes.uri.relativeTo(resource.uri.getDirectory());
|
||||
|
||||
if (targetFormat === 'wikilink') {
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: workspace.getIdentifier(relativeUri),
|
||||
type: 'wikilink',
|
||||
});
|
||||
}
|
||||
|
||||
if (targetFormat === 'link') {
|
||||
/* if alias is empty, construct one as target#section */
|
||||
if (alias === '') {
|
||||
/* in page anchor have no filename */
|
||||
if (relativeUri.getBasename() === resource.uri.getBasename()) {
|
||||
target = '';
|
||||
}
|
||||
alias = `${target}${sectionDivider}${section}`;
|
||||
}
|
||||
|
||||
/* if it's originally an embedded note, the markdown link shouldn't be embedded */
|
||||
const isEmbed = targetRes.type === 'image' ? link.isEmbed : false;
|
||||
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
alias: alias,
|
||||
target: relativeUri.path,
|
||||
isEmbed: isEmbed,
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: targetFormat: ${targetFormat} is not supported`
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ describe('generateLinkReferences', () => {
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_workspace.list().length).toEqual(10);
|
||||
expect(_workspace.list().length).toEqual(11);
|
||||
});
|
||||
|
||||
it('should add link references to a file that does not have them', async () => {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { generateLinkReferences } from './generate-link-references';
|
||||
export { generateHeading } from './generate-headings';
|
||||
export { convertLinkFormat } from './convert-links-format';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FoamGraph } from './graph';
|
||||
import { ResourceParser } from './note';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { FoamTags } from './tags';
|
||||
import { Logger } from '../utils/log';
|
||||
import { Logger, withTiming, withTimingAsync } from '../utils/log';
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
@@ -25,25 +25,28 @@ export const bootstrap = async (
|
||||
watcher: IWatcher | undefined,
|
||||
dataStore: IDataStore,
|
||||
parser: ResourceParser,
|
||||
initialProviders: ResourceProvider[]
|
||||
initialProviders: ResourceProvider[],
|
||||
defaultExtension: string = '.md'
|
||||
) => {
|
||||
const tsStart = Date.now();
|
||||
|
||||
const workspace = await FoamWorkspace.fromProviders(
|
||||
initialProviders,
|
||||
dataStore
|
||||
const workspace = await withTimingAsync(
|
||||
() =>
|
||||
FoamWorkspace.fromProviders(
|
||||
initialProviders,
|
||||
dataStore,
|
||||
defaultExtension
|
||||
),
|
||||
ms => Logger.info(`Workspace loaded in ${ms}ms`)
|
||||
);
|
||||
|
||||
const tsWsDone = Date.now();
|
||||
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
|
||||
const graph = withTiming(
|
||||
() => FoamGraph.fromWorkspace(workspace, true),
|
||||
ms => Logger.info(`Graph loaded in ${ms}ms`)
|
||||
);
|
||||
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true);
|
||||
const tsGraphDone = Date.now();
|
||||
Logger.info(`Graph loaded in ${tsGraphDone - tsWsDone}ms`);
|
||||
|
||||
const tags = FoamTags.fromWorkspace(workspace, true);
|
||||
const tsTagsEnd = Date.now();
|
||||
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
|
||||
const tags = withTiming(
|
||||
() => FoamTags.fromWorkspace(workspace, true),
|
||||
ms => Logger.info(`Tags loaded in ${ms}ms`)
|
||||
);
|
||||
|
||||
watcher?.onDidChange(async uri => {
|
||||
if (matcher.isMatch(uri)) {
|
||||
|
||||
@@ -139,6 +139,21 @@ describe('Graph', () => {
|
||||
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
|
||||
});
|
||||
|
||||
it('should create inbound connections when targeting a section', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'page-b#section 2' }],
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/somewhere/page-b.md',
|
||||
text: '## Section 1\n\n## Section 2',
|
||||
});
|
||||
const ws = createTestWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getBacklinks(noteB.uri).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should support attachments', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
|
||||
34
packages/foam-vscode/src/core/model/location.ts
Normal file
34
packages/foam-vscode/src/core/model/location.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Range } from './range';
|
||||
import { URI } from './uri';
|
||||
|
||||
/**
|
||||
* Represents a location inside a resource, such as a line
|
||||
* inside a text file.
|
||||
*/
|
||||
export interface Location<T> {
|
||||
/**
|
||||
* The resource identifier of this location.
|
||||
*/
|
||||
uri: URI;
|
||||
/**
|
||||
* The document range of this locations.
|
||||
*/
|
||||
range: Range;
|
||||
/**
|
||||
* The data associated to this location.
|
||||
*/
|
||||
data: T;
|
||||
}
|
||||
|
||||
export abstract class Location<T> {
|
||||
static create<T>(uri: URI, range: Range, data: T): Location<T> {
|
||||
return { uri, range, data };
|
||||
}
|
||||
|
||||
static forObjectWithRange<T extends { range: Range }>(
|
||||
uri: URI,
|
||||
obj: T
|
||||
): Location<T> {
|
||||
return Location.create(uri, obj.range, obj);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ export abstract class Resource {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
public static sortByPath(a: Resource, b: Resource) {
|
||||
return a.uri.path.localeCompare(b.uri.path);
|
||||
}
|
||||
|
||||
public static isResource(thing: any): thing is Resource {
|
||||
if (!thing) {
|
||||
return false;
|
||||
|
||||
@@ -195,7 +195,6 @@ function encode(uri: URI, skipEncoding: boolean): string {
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
@@ -382,10 +381,20 @@ function encodeURIComponentMinimal(path: string): string {
|
||||
* TODO this probably needs to be moved to the workspace service
|
||||
*/
|
||||
export function asAbsoluteUri(uri: URI, baseFolders: URI[]): URI {
|
||||
return URI.file(
|
||||
pathUtils.asAbsolutePaths(
|
||||
uri.path,
|
||||
baseFolders.map(f => f.path)
|
||||
)[0]
|
||||
);
|
||||
const path = uri.path;
|
||||
if (pathUtils.isAbsolute(path)) {
|
||||
return uri;
|
||||
}
|
||||
let tokens = path.split('/');
|
||||
const firstDir = tokens[0];
|
||||
if (baseFolders.length > 1) {
|
||||
for (const folder of baseFolders) {
|
||||
const lastDir = folder.path.split('/').pop();
|
||||
if (lastDir === firstDir) {
|
||||
tokens = tokens.slice(1);
|
||||
return folder.joinPath(...tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseFolders[0].joinPath(...tokens);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
|
||||
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
|
||||
@@ -124,7 +124,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
@@ -170,7 +170,7 @@ describe('Identifier computation', () => {
|
||||
});
|
||||
|
||||
it('should ignore elements from the exclude list', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = new FoamWorkspace('.md');
|
||||
const noteA = createTestNote({ uri: '/path/to/note-a.md' });
|
||||
const noteB = createTestNote({ uri: '/path/to/note-b.md' });
|
||||
const noteC = createTestNote({ uri: '/path/to/note-c.md' });
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore } from '../services/datastore';
|
||||
import TrieMap from 'mnemonist/trie-map';
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -20,7 +21,12 @@ export class FoamWorkspace implements IDisposable {
|
||||
/**
|
||||
* Resources by path
|
||||
*/
|
||||
private _resources: Map<string, Resource> = new Map();
|
||||
private _resources: TrieMap<string, Resource> = new TrieMap();
|
||||
|
||||
/**
|
||||
* @param defaultExtension: The default extension for notes in this workspace (e.g. `.md`)
|
||||
*/
|
||||
constructor(public defaultExtension: string = '.md') {}
|
||||
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
@@ -28,7 +34,10 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
set(resource: Resource) {
|
||||
const old = this.find(resource.uri);
|
||||
this._resources.set(normalize(resource.uri.path), resource);
|
||||
|
||||
// store resource
|
||||
this._resources.set(this.getTrieIdentifier(resource.uri.path), resource);
|
||||
|
||||
isSome(old)
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
@@ -36,8 +45,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
delete(uri: URI) {
|
||||
const deleted = this._resources.get(normalize(uri.path));
|
||||
this._resources.delete(normalize(uri.path));
|
||||
const deleted = this._resources.get(this.getTrieIdentifier(uri));
|
||||
this._resources.delete(this.getTrieIdentifier(uri));
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
@@ -52,7 +61,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public resources(): IterableIterator<Resource> {
|
||||
return this._resources.values();
|
||||
const resources: Array<Resource> = Array.from(
|
||||
this._resources.values()
|
||||
).sort(Resource.sortByPath);
|
||||
|
||||
return resources.values();
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
@@ -65,16 +78,22 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
const needle = normalize('/' + identifier);
|
||||
let needle = this.getTrieIdentifier(identifier);
|
||||
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
const resources = [];
|
||||
for (const key of this._resources.keys()) {
|
||||
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
|
||||
resources.push(this._resources.get(normalize(key)));
|
||||
}
|
||||
getExtension(normalize(identifier)) !== this.defaultExtension
|
||||
? this.getTrieIdentifier(identifier + this.defaultExtension)
|
||||
: undefined;
|
||||
|
||||
const resources: Resource[] = [];
|
||||
|
||||
this._resources.find(needle).forEach(elm => resources.push(elm[1]));
|
||||
|
||||
if (mdNeedle) {
|
||||
this._resources.find(mdNeedle).forEach(elm => resources.push(elm[1]));
|
||||
}
|
||||
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
|
||||
|
||||
return resources.sort(Resource.sortByPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,50 +104,71 @@ export class FoamWorkspace implements IDisposable {
|
||||
public getIdentifier(forResource: URI, exclude?: URI[]): string {
|
||||
const amongst = [];
|
||||
const basename = forResource.getBasename();
|
||||
for (const res of this._resources.values()) {
|
||||
// skip elements that cannot possibly match
|
||||
if (!res.uri.path.endsWith(basename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.listByIdentifier(basename).map(res => {
|
||||
// skip self
|
||||
if (res.uri.isEqual(forResource)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// skip exclude list
|
||||
if (exclude && exclude.find(ex => ex.isEqual(res.uri))) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
});
|
||||
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
identifier = changeExtension(identifier, '.md', '');
|
||||
identifier = changeExtension(identifier, this.defaultExtension, '');
|
||||
if (forResource.fragment) {
|
||||
identifier += `#${forResource.fragment}`;
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a note identifier in reversed order. Used to optimise the storage of notes in
|
||||
* the workspace to optimise retrieval of notes.
|
||||
*
|
||||
* @param reference the URI path to reverse
|
||||
*/
|
||||
private getTrieIdentifier(reference: URI | string): string {
|
||||
let path: string;
|
||||
if (reference instanceof URI) {
|
||||
path = (reference as URI).path;
|
||||
} else {
|
||||
path = reference as string;
|
||||
}
|
||||
|
||||
let reversedPath = normalize(path).split('/').reverse().join('/');
|
||||
|
||||
if (reversedPath.indexOf('/') < 0) {
|
||||
reversedPath = reversedPath + '/';
|
||||
}
|
||||
|
||||
return reversedPath;
|
||||
}
|
||||
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this._resources.get(normalize((reference as URI).path)) ?? null;
|
||||
return this._resources.get(this.getTrieIdentifier(reference)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
const [path, fragment] = (reference as string).split('#');
|
||||
if (FoamWorkspace.isIdentifier(path)) {
|
||||
resource = this.listByIdentifier(path)[0];
|
||||
} else {
|
||||
const candidates = [path, path + '.md'];
|
||||
const candidates = [path, path + this.defaultExtension];
|
||||
for (const candidate of candidates) {
|
||||
const searchKey = isAbsolute(candidate)
|
||||
? candidate
|
||||
: isSome(baseUri)
|
||||
? baseUri.resolve(candidate).path
|
||||
: null;
|
||||
resource = this._resources.get(normalize(searchKey));
|
||||
resource = this._resources.get(this.getTrieIdentifier(searchKey));
|
||||
if (resource) {
|
||||
break;
|
||||
}
|
||||
@@ -141,7 +181,6 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
// TODO add tests
|
||||
for (const provider of this.providers) {
|
||||
if (provider.supports(resource.uri)) {
|
||||
return provider.resolveLink(this, resource, link);
|
||||
@@ -237,9 +276,10 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
static async fromProviders(
|
||||
providers: ResourceProvider[],
|
||||
dataStore: IDataStore
|
||||
dataStore: IDataStore,
|
||||
defaultExtension: string = '.md'
|
||||
): Promise<FoamWorkspace> {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = new FoamWorkspace(defaultExtension);
|
||||
await Promise.all(providers.map(p => workspace.registerProvider(p)));
|
||||
const files = await dataStore.list();
|
||||
await Promise.all(files.map(f => workspace.fetchAndSet(f)));
|
||||
|
||||
@@ -3,17 +3,15 @@ import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
|
||||
const attachmentExtConfig = getFoamVsCodeConfig(
|
||||
'files.attachmentExtensions',
|
||||
''
|
||||
)
|
||||
.split(' ')
|
||||
.map(ext => '.' + ext.trim());
|
||||
|
||||
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
|
||||
const attachmentExtensions = [...attachmentExtConfig, ...imageExtensions];
|
||||
export const imageExtensions = [
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.svg',
|
||||
'.webp',
|
||||
];
|
||||
|
||||
const asResource = (uri: URI): Resource => {
|
||||
const type = imageExtensions.includes(uri.getExtension())
|
||||
@@ -34,9 +32,14 @@ const asResource = (uri: URI): Resource => {
|
||||
|
||||
export class AttachmentResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
public readonly attachmentExtensions: string[];
|
||||
|
||||
constructor(attachmentExtensions: string[] = []) {
|
||||
this.attachmentExtensions = [...imageExtensions, ...attachmentExtensions];
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return attachmentExtensions.includes(
|
||||
return this.attachmentExtensions.includes(
|
||||
uri.getExtension().toLocaleLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,4 +254,185 @@ describe('MarkdownLink', () => {
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert wikilink to link', () => {
|
||||
it('should generate default alias if no one', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[wikilink](wikilink)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
|
||||
const wikilinkWithSection = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section]]`
|
||||
).links[0];
|
||||
const wikilinkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
wikilinkWithSection,
|
||||
{
|
||||
type: 'link',
|
||||
}
|
||||
);
|
||||
expect(wikilinkWithSectionEdit.newText).toEqual(
|
||||
`[wikilink#section](wikilink#section)`
|
||||
);
|
||||
expect(wikilinkWithSectionEdit.range).toEqual(wikilinkWithSection.range);
|
||||
});
|
||||
|
||||
it('should use alias in the wikilik the if there has one', () => {
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert link to wikilink', () => {
|
||||
it('should reorganize target, section, and alias in wikilink manner', () => {
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
type: 'wikilink',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[[to/path.md|link]]`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
|
||||
const linkWithSection = parser.parse(
|
||||
getRandomURI(),
|
||||
`[link](to/path.md#section)`
|
||||
).links[0];
|
||||
const linkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
linkWithSection,
|
||||
{
|
||||
type: 'wikilink',
|
||||
}
|
||||
);
|
||||
expect(linkWithSectionEdit.newText).toEqual(
|
||||
`[[to/path.md#section|link]]`
|
||||
);
|
||||
expect(linkWithSectionEdit.range).toEqual(linkWithSection.range);
|
||||
});
|
||||
|
||||
it('should use alias in the wikilik the if there has one', () => {
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert to its original type', () => {
|
||||
it('should remain unchanged', () => {
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md#section)`)
|
||||
.links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md#section)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'wikilink',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[[wikilink#section|alias]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change isEmbed property', () => {
|
||||
it('should change isEmbed only', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
isEmbed: true,
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`![[wikilink]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
|
||||
const link = parser.parse(getRandomURI(), ``).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
isEmbed: false,
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
});
|
||||
|
||||
it('should be unchanged if the update value is the same as the original one', () => {
|
||||
const embeddedWikilink = parser.parse(getRandomURI(), `![[wikilink]]`)
|
||||
.links[0];
|
||||
const embeddedWikilinkEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
embeddedWikilink,
|
||||
{
|
||||
isEmbed: true,
|
||||
}
|
||||
);
|
||||
expect(embeddedWikilinkEdit.newText).toEqual(`![[wikilink]]`);
|
||||
expect(embeddedWikilinkEdit.range).toEqual(embeddedWikilink.range);
|
||||
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
isEmbed: false,
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert angles', () => {
|
||||
it('should insert angles when meeting space in links', () => {
|
||||
const link = parser.parse(getRandomURI(), ``).links[0];
|
||||
const linkAddSection = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: 'one section',
|
||||
});
|
||||
expect(linkAddSection.newText).toEqual(
|
||||
``
|
||||
);
|
||||
expect(linkAddSection.range).toEqual(link.range);
|
||||
|
||||
const linkChangingTarget = parser.parse(
|
||||
getRandomURI(),
|
||||
`[link](to/path.md#one-section)`
|
||||
).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(linkChangingTarget, {
|
||||
target: 'to/another path.md',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(
|
||||
`[link](<to/another path.md#one-section>)`
|
||||
);
|
||||
expect(linkEdit.range).toEqual(linkChangingTarget.range);
|
||||
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
|
||||
.links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(
|
||||
`[wikilink#one section](<wikilink#one section>)`
|
||||
);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
|
||||
it('should not insert angles in wikilink', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
|
||||
.links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
target: 'another wikilink',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[[another wikilink#one section]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,13 @@ export abstract class MarkdownLink {
|
||||
|
||||
public static createUpdateLinkEdit(
|
||||
link: ResourceLink,
|
||||
delta: { target?: string; section?: string; alias?: string }
|
||||
delta: {
|
||||
target?: string;
|
||||
section?: string;
|
||||
alias?: string;
|
||||
type?: 'wikilink' | 'link';
|
||||
isEmbed?: boolean;
|
||||
}
|
||||
) {
|
||||
const { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
const newTarget = delta.target ?? target;
|
||||
@@ -46,21 +52,27 @@ export abstract class MarkdownLink {
|
||||
const newAlias = delta.alias ?? alias ?? '';
|
||||
const sectionDivider = newSection ? '#' : '';
|
||||
const aliasDivider = newAlias ? '|' : '';
|
||||
const embed = link.isEmbed ? '!' : '';
|
||||
if (link.type === 'wikilink') {
|
||||
const embed = delta.isEmbed ?? link.isEmbed ? '!' : '';
|
||||
const type = delta.type ?? link.type;
|
||||
if (type === 'wikilink') {
|
||||
return {
|
||||
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
if (type === 'link') {
|
||||
const defaultAlias = () => {
|
||||
return `${newTarget}${sectionDivider}${newSection}`;
|
||||
};
|
||||
const useAngles =
|
||||
newTarget.indexOf(' ') > 0 || newSection.indexOf(' ') > 0;
|
||||
return {
|
||||
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
newText: `${embed}[${newAlias ? newAlias : defaultAlias()}](${
|
||||
useAngles ? '<' : ''
|
||||
}${newTarget}${sectionDivider}${newSection}${useAngles ? '>' : ''})`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: link of type ${link.type} is not supported`
|
||||
);
|
||||
throw new Error(`Unexpected state: link of type ${type} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +459,9 @@ export const getBlockFor = (
|
||||
}
|
||||
});
|
||||
|
||||
let nLines = startLine == -1 ? 1 : endLine - startLine;
|
||||
let nLines = startLine === -1 ? 1 : endLine - startLine;
|
||||
let block =
|
||||
startLine == -1
|
||||
startLine === -1
|
||||
? lines[searchLine] ?? ''
|
||||
: lines.slice(startLine, endLine).join('\n');
|
||||
|
||||
|
||||
@@ -166,6 +166,65 @@ describe('Link resolution', () => {
|
||||
noteA.uri.withFragment('section')
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve wikilinks with special characters', () => {
|
||||
const ws = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
|
||||
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
|
||||
[[page /j]]`
|
||||
);
|
||||
const noteB = createNoteFromMarkdown(
|
||||
'Note containing :',
|
||||
'/dir1/page: a.md'
|
||||
);
|
||||
const noteC = createNoteFromMarkdown(
|
||||
'Note containing %',
|
||||
'/dir1/page %b%.md'
|
||||
);
|
||||
const noteD = createNoteFromMarkdown(
|
||||
'Note containing ?',
|
||||
'/dir1/page? c.md'
|
||||
);
|
||||
const noteE = createNoteFromMarkdown(
|
||||
'Note containing ]',
|
||||
'/dir1/[page] d.md'
|
||||
);
|
||||
const noteF = createNoteFromMarkdown(
|
||||
'Note containing ^',
|
||||
'/dir1/page ^e^.md'
|
||||
);
|
||||
const noteG = createNoteFromMarkdown(
|
||||
'Note containing `',
|
||||
'/dir1/page `f`.md'
|
||||
);
|
||||
const noteH = createNoteFromMarkdown(
|
||||
'Note containing { and }',
|
||||
'/dir1/page {g}.md'
|
||||
);
|
||||
const noteI = createNoteFromMarkdown(
|
||||
'Note containing ~',
|
||||
'/dir1/page ~i.md'
|
||||
);
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD)
|
||||
.set(noteE)
|
||||
.set(noteF)
|
||||
.set(noteG)
|
||||
.set(noteH)
|
||||
.set(noteI);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteC.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[2])).toEqual(noteD.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[3])).toEqual(noteE.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[4])).toEqual(noteF.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[5])).toEqual(noteG.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[6])).toEqual(noteH.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[7])).toEqual(noteI.uri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown direct links', () => {
|
||||
@@ -311,9 +370,78 @@ describe('Generation of markdown references', () => {
|
||||
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => r.url)).toEqual([
|
||||
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
|
||||
'../dir2/page-b.md',
|
||||
'../dir3/page-c.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate links for embedded notes that are formatted properly', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'Link to ![[page-b]] and [[page-c]]',
|
||||
'/dir1/page-a.md'
|
||||
);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => [decodeURIComponent(r.url), r.label])).toEqual([
|
||||
['../dir2/page-b.md', 'page-b'],
|
||||
['../dir3/page-c.md', 'page-c'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not generate links for placeholders', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'Link to ![[page-b]] and [[page-c]] and [[does-not-exist]] and ![[does-not-exist-either]]',
|
||||
'/dir1/page-a.md'
|
||||
);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
|
||||
'../dir2/page-b.md',
|
||||
'../dir3/page-c.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should encode special characters in links', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
|
||||
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
|
||||
[[page /j]]`
|
||||
);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('Note containing :', '/dir1/page: a.md'))
|
||||
.set(createNoteFromMarkdown('Note containing %', '/dir1/page %b%.md'))
|
||||
.set(createNoteFromMarkdown('Note containing ?', '/dir1/page? c.md'))
|
||||
.set(createNoteFromMarkdown('Note containing ]', '/dir1/[page] d.md'))
|
||||
.set(createNoteFromMarkdown('Note containing ^', '/dir1/page ^e^.md'))
|
||||
.set(createNoteFromMarkdown('Note containing `', '/dir1/page `f`.md'))
|
||||
.set(
|
||||
createNoteFromMarkdown('Note containing { and }', '/dir1/page {g}.md')
|
||||
)
|
||||
.set(createNoteFromMarkdown('Note containing ~', '/dir1/page ~i.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
|
||||
'../dir1/page: a.md',
|
||||
'../dir1/page %b%.md',
|
||||
'../dir1/page? c.md',
|
||||
'../dir1/[page] d.md',
|
||||
'../dir1/page ^e^.md',
|
||||
'../dir1/page `f`.md',
|
||||
'../dir1/page {g}.md',
|
||||
'../dir1/page ~i.md',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,11 +19,12 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
constructor(
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly parser: ResourceParser
|
||||
private readonly parser: ResourceParser,
|
||||
public readonly noteExtensions: string[] = ['.md']
|
||||
) {}
|
||||
|
||||
supports(uri: URI) {
|
||||
return uri.isMarkdown();
|
||||
return this.noteExtensions.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
@@ -129,17 +130,28 @@ export function createMarkdownReferences(
|
||||
}
|
||||
|
||||
let relativeUri = target.uri.relativeTo(resource.uri.getDirectory());
|
||||
if (!includeExtension && relativeUri.path.endsWith('.md')) {
|
||||
if (
|
||||
!includeExtension &&
|
||||
relativeUri.path.endsWith(workspace.defaultExtension)
|
||||
) {
|
||||
relativeUri = relativeUri.changeExtension('*', '');
|
||||
}
|
||||
|
||||
// Extract base path and link name separately.
|
||||
const basePath = relativeUri.path.split('/').slice(0, -1).join('/');
|
||||
const linkName = relativeUri.path.split('/').pop();
|
||||
|
||||
const encodedURL = encodeURIComponent(linkName).replace(/%20/g, ' ');
|
||||
|
||||
// [wikilink-text]: path/to/file.md "Page title"
|
||||
return {
|
||||
label:
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText,
|
||||
url: relativeUri.path,
|
||||
// embedded looks like ![[note-a]]
|
||||
// regular note looks like [[note-a]]
|
||||
label: link.rawText.substring(
|
||||
link.isEmbed ? 3 : 2,
|
||||
link.rawText.length - 2
|
||||
),
|
||||
url: `${basePath ? basePath + '/' : ''}${encodedURL}`,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -6,6 +6,22 @@ Logger.setLevel('error');
|
||||
|
||||
describe('Resource Filter', () => {
|
||||
describe('Filter parameters', () => {
|
||||
it('should support the path regex', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/foo.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: '/path/to/bar.md',
|
||||
});
|
||||
|
||||
const filter = createFilter({ path: 'foo' }, false);
|
||||
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support expressions when code execution is enabled', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
|
||||
@@ -53,6 +53,9 @@ export function createFilter(
|
||||
if (expressionFn && !expressionFn(resource)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.path && !resource.uri.path.match(filter.path)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type && resource.type !== filter.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import crypto from 'crypto';
|
||||
import sha1 from 'js-sha1';
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
@@ -20,5 +20,4 @@ export function isNumeric(value: string): boolean {
|
||||
return /-?\d+$/.test(value);
|
||||
}
|
||||
|
||||
export const hash = (text: string) =>
|
||||
crypto.createHash('sha1').update(text).digest('hex');
|
||||
export const hash = (text: string) => sha1.sha1(text);
|
||||
|
||||
@@ -89,3 +89,25 @@ export class Logger {
|
||||
Logger.defaultLogger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
export const withTiming = <T>(
|
||||
fn: () => T,
|
||||
onDidComplete: (elapsed: number) => void
|
||||
): T => {
|
||||
const tsStart = Date.now();
|
||||
const res = fn();
|
||||
const tsEnd = Date.now();
|
||||
onDidComplete(tsEnd - tsStart);
|
||||
return res;
|
||||
};
|
||||
|
||||
export const withTimingAsync = async <T>(
|
||||
fn: () => Promise<T>,
|
||||
onDidComplete: (elapsed: number) => void
|
||||
): Promise<T> => {
|
||||
const tsStart = Date.now();
|
||||
const res = await fn();
|
||||
const tsEnd = Date.now();
|
||||
onDidComplete(tsEnd - tsStart);
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -1,67 +1,4 @@
|
||||
import {
|
||||
isInFrontMatter,
|
||||
isOnYAMLKeywordLine,
|
||||
removeBrackets,
|
||||
toTitleCase,
|
||||
} from './utils';
|
||||
|
||||
describe('removeBrackets', () => {
|
||||
it('removes the brackets', () => {
|
||||
const input = 'hello world [[this-is-it]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the md file extension', () => {
|
||||
const input = 'hello world [[this-is-it.md]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the mdx file extension', () => {
|
||||
const input = 'hello world [[this-is-it.mdx]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the markdown file extension', () => {
|
||||
const input = 'hello world [[this-is-it.markdown]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets even with numbers', () => {
|
||||
const input = 'hello world [[2020-07-21.markdown]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world 2020 07 21';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes brackets for more than one word', () => {
|
||||
const input =
|
||||
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
|
||||
const actual = removeBrackets(input);
|
||||
const expected =
|
||||
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTitleCase', () => {
|
||||
it('title cases a word', () => {
|
||||
const input =
|
||||
'look at this really long sentence but I am calling it a word';
|
||||
const actual = toTitleCase(input);
|
||||
const expected =
|
||||
'Look At This Really Long Sentence But I Am Calling It A Word';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('works on one word', () => {
|
||||
const input = 'word';
|
||||
const actual = toTitleCase(input);
|
||||
const expected = 'Word';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine } from './md';
|
||||
|
||||
describe('isInFrontMatter', () => {
|
||||
it('is true for started front matter', () => {
|
||||
@@ -81,6 +18,12 @@ describe('isInFrontMatter', () => {
|
||||
const actual = isInFrontMatter(content, 1);
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
it('is false for non valid front matter delimiter #1347', () => {
|
||||
const content = '---\ntitle: A title\n-..\n\n\n---\ntest\n';
|
||||
expect(isInFrontMatter(content, 1)).toBeTruthy();
|
||||
expect(isInFrontMatter(content, 4)).toBeTruthy();
|
||||
expect(isInFrontMatter(content, 6)).toBeFalsy();
|
||||
});
|
||||
it('is false for outside completed front matter', () => {
|
||||
const content = '---\ntitle: A title\n---\ncontent\nmore content\n';
|
||||
const actual = isInFrontMatter(content, 3);
|
||||
70
packages/foam-vscode/src/core/utils/md.ts
Normal file
70
packages/foam-vscode/src/core/utils/md.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import matter from 'gray-matter';
|
||||
|
||||
export function getExcerpt(
|
||||
markdown: string,
|
||||
maxLines: number
|
||||
): { excerpt: string; lines: number } {
|
||||
const OFFSET_LINES_LIMIT = 5;
|
||||
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
|
||||
const excerpt: string[] = [];
|
||||
let lines = 0;
|
||||
for (const paragraph of paragraphs) {
|
||||
const n = paragraph.split('\n').length;
|
||||
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
|
||||
break;
|
||||
}
|
||||
excerpt.push(paragraph);
|
||||
lines = lines + n + 1;
|
||||
}
|
||||
return { excerpt: excerpt.join('\n\n'), lines };
|
||||
}
|
||||
|
||||
export function stripFrontMatter(markdown: string): string {
|
||||
return matter(markdown).content.trim();
|
||||
}
|
||||
|
||||
export function stripImages(markdown: string): string {
|
||||
return markdown.replace(
|
||||
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
|
||||
'$1'.length ? '[Image: $1]' : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the given line is inside a front matter block
|
||||
* @param content the string to check
|
||||
* @param lineNumber the line number within the string, 0-based
|
||||
* @returns true if the line is inside a frontmatter block in content
|
||||
*/
|
||||
export function isInFrontMatter(content: string, lineNumber: number): boolean {
|
||||
const FIRST_DELIMITER_MATCH = /^---\s*?$/m;
|
||||
const LAST_DELIMITER_MATCH = /^(-{3}|\.{3})/;
|
||||
|
||||
// if we're on the first line, we're not _yet_ in the front matter
|
||||
if (lineNumber === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// look for --- at start, and a second --- or ... to end
|
||||
if (content.match(FIRST_DELIMITER_MATCH) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
lines.shift();
|
||||
const endLineNumber = lines.findIndex(l => l.match(LAST_DELIMITER_MATCH));
|
||||
|
||||
return endLineNumber === -1 || endLineNumber >= lineNumber;
|
||||
}
|
||||
|
||||
export function isOnYAMLKeywordLine(content: string, keyword: string): boolean {
|
||||
const keywordMatch = /^\s*(\w+):/gm;
|
||||
|
||||
if (content.match(keywordMatch) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = Array.from(content.matchAll(keywordMatch));
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
return lastMatch[1] === keyword;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { NoteFactory } from './services/templates';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
import { asAbsoluteWorkspaceUri } from './services/editor';
|
||||
import { asAbsoluteWorkspaceUri, focusNote } from './services/editor';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
|
||||
@@ -7,7 +7,11 @@ import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import {
|
||||
getAttachmentsExtensions,
|
||||
getIgnoredFilesSetting,
|
||||
getNotesExtensions,
|
||||
} from './settings';
|
||||
import { AttachmentResourceProvider } from './core/services/attachment-provider';
|
||||
import { VsCodeWatcher } from './services/watcher';
|
||||
import { createMarkdownParser } from './core/services/markdown-parser';
|
||||
@@ -45,16 +49,32 @@ export async function activate(context: ExtensionContext) {
|
||||
const parserCache = new VsCodeBasedParserCache(context);
|
||||
const parser = createMarkdownParser([], parserCache);
|
||||
|
||||
const markdownProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
const attachmentProvider = new AttachmentResourceProvider();
|
||||
const { notesExtensions, defaultExtension } = getNotesExtensions();
|
||||
|
||||
const foamPromise = bootstrap(matcher, watcher, dataStore, parser, [
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
]);
|
||||
const markdownProvider = new MarkdownResourceProvider(
|
||||
dataStore,
|
||||
parser,
|
||||
notesExtensions
|
||||
);
|
||||
|
||||
const attachmentExtConfig = getAttachmentsExtensions();
|
||||
const attachmentProvider = new AttachmentResourceProvider(
|
||||
attachmentExtConfig
|
||||
);
|
||||
|
||||
const foamPromise = bootstrap(
|
||||
matcher,
|
||||
watcher,
|
||||
dataStore,
|
||||
parser,
|
||||
[markdownProvider, attachmentProvider],
|
||||
defaultExtension
|
||||
);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(feature => feature(context, foamPromise));
|
||||
const featuresPromises = features.map(feature =>
|
||||
feature(context, foamPromise)
|
||||
);
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
|
||||
@@ -66,17 +86,32 @@ export async function activate(context: ExtensionContext) {
|
||||
attachmentProvider,
|
||||
commands.registerCommand('foam-vscode.clear-cache', () =>
|
||||
parserCache.clear()
|
||||
)
|
||||
),
|
||||
workspace.onDidChangeConfiguration(e => {
|
||||
if (
|
||||
[
|
||||
'foam.files.ignore',
|
||||
'foam.files.attachmentExtensions',
|
||||
'foam.files.noteExtensions',
|
||||
'foam.files.defaultNoteExtension',
|
||||
].some(setting => e.affectsConfiguration(setting))
|
||||
) {
|
||||
window.showInformationMessage(
|
||||
'Foam: Reload the window to use the updated settings'
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
const feats = (await Promise.all(featuresPromises)).filter(r => r != null);
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) => {
|
||||
return res.reduce((acc: markdownit, r: any) => {
|
||||
return feats.reduce((acc: markdownit, r: any) => {
|
||||
return r.extendMarkdownIt ? r.extendMarkdownIt(acc) : acc;
|
||||
}, md);
|
||||
},
|
||||
foam,
|
||||
};
|
||||
} catch (e) {
|
||||
Logger.error('An error occurred while bootstrapping Foam', e);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { commands, ExtensionContext, window, workspace, Uri } from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { convertLinkFormat } from '../../core/janitor';
|
||||
import { isMdEditor } from '../../services/editor';
|
||||
|
||||
type LinkFormat = 'wikilink' | 'link';
|
||||
|
||||
enum ConvertOption {
|
||||
Wikilink2MDlink,
|
||||
MDlink2Wikilink,
|
||||
}
|
||||
|
||||
interface IConfig {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
const Config: { [key in ConvertOption]: IConfig } = {
|
||||
[ConvertOption.Wikilink2MDlink]: {
|
||||
from: 'wikilink',
|
||||
to: 'link',
|
||||
},
|
||||
[ConvertOption.MDlink2Wikilink]: {
|
||||
from: 'link',
|
||||
to: 'wikilink',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
/*
|
||||
commands:
|
||||
foam-vscode.convert-link-style-inplace
|
||||
foam-vscode.convert-link-style-incopy
|
||||
*/
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.convert-link-style-inplace', () => {
|
||||
return convertLinkAdapter(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher,
|
||||
true
|
||||
);
|
||||
}),
|
||||
commands.registerCommand('foam-vscode.convert-link-style-incopy', () => {
|
||||
return convertLinkAdapter(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher,
|
||||
false
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function convertLinkAdapter(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
isInPlace: boolean
|
||||
) {
|
||||
const convertOption = await pickConvertStrategy();
|
||||
if (!convertOption) {
|
||||
window.showInformationMessage('Convert canceled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPlace) {
|
||||
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
|
||||
} else {
|
||||
await convertLinkInCopy(fWorkspace, fParser, fMatcher, convertOption);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickConvertStrategy(): Promise<IConfig | undefined> {
|
||||
const options = {
|
||||
'to wikilink': ConvertOption.MDlink2Wikilink,
|
||||
'to markdown link': ConvertOption.Wikilink2MDlink,
|
||||
};
|
||||
return window.showQuickPick(Object.keys(options)).then(name => {
|
||||
if (name) {
|
||||
return Config[options[name]];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert links based on its workspace and the note containing it.
|
||||
* Changes happen in-place
|
||||
* @param fWorkspace
|
||||
* @param fParser
|
||||
* @param fMatcher
|
||||
* @param convertOption
|
||||
* @returns void
|
||||
*/
|
||||
async function convertLinkInPlace(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
convertOption: IConfig
|
||||
) {
|
||||
const editor = window.activeTextEditor;
|
||||
const doc = editor.document;
|
||||
|
||||
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
|
||||
return;
|
||||
}
|
||||
// const eol = getEditorEOL();
|
||||
let text = doc.getText();
|
||||
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
|
||||
const textReplaceArr = resource.links
|
||||
.filter(link => link.type === convertOption.from)
|
||||
.map(link =>
|
||||
convertLinkFormat(
|
||||
link,
|
||||
convertOption.to as LinkFormat,
|
||||
fWorkspace,
|
||||
resource
|
||||
)
|
||||
)
|
||||
/* transform .range property into vscode range */
|
||||
.map(linkReplace => ({
|
||||
...linkReplace,
|
||||
range: toVsCodeRange(linkReplace.range),
|
||||
}));
|
||||
|
||||
/* reorder the array such that the later range comes first */
|
||||
textReplaceArr.sort((a, b) => b.range.start.compareTo(a.range.start));
|
||||
|
||||
await editor.edit(editorBuilder => {
|
||||
textReplaceArr.forEach(edit => {
|
||||
editorBuilder.replace(edit.range, edit.newText);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert links based on its workspace and the note containing it.
|
||||
* Changes happen in a copy
|
||||
* 1. prepare a copy file, and makt it the activeTextEditor
|
||||
* 2. call to convertLinkInPlace
|
||||
* @param fWorkspace
|
||||
* @param fParser
|
||||
* @param fMatcher
|
||||
* @param convertOption
|
||||
* @returns void
|
||||
*/
|
||||
async function convertLinkInCopy(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
convertOption: IConfig
|
||||
) {
|
||||
const editor = window.activeTextEditor;
|
||||
const doc = editor.document;
|
||||
|
||||
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
|
||||
return;
|
||||
}
|
||||
// const eol = getEditorEOL();
|
||||
let text = doc.getText();
|
||||
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
const basePath = doc.uri.path.split('/').slice(0, -1).join('/');
|
||||
|
||||
const fileUri = Uri.file(
|
||||
`${
|
||||
basePath ? basePath + '/' : ''
|
||||
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`
|
||||
);
|
||||
const encoder = new TextEncoder();
|
||||
await workspace.fs.writeFile(fileUri, encoder.encode(text));
|
||||
await window.showTextDocument(fileUri);
|
||||
|
||||
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import { createFile, showInEditor } from '../../test/test-utils-vscode';
|
||||
import { removeBrackets, toTitleCase } from './copy-without-brackets';
|
||||
|
||||
describe('copy-without-brackets command', () => {
|
||||
it('should get the input from the active editor selection', async () => {
|
||||
@@ -11,3 +12,61 @@ describe('copy-without-brackets command', () => {
|
||||
expect(value).toEqual('This is my Test Content.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeBrackets', () => {
|
||||
it('removes the brackets', () => {
|
||||
const input = 'hello world [[this-is-it]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the md file extension', () => {
|
||||
const input = 'hello world [[this-is-it.md]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the mdx file extension', () => {
|
||||
const input = 'hello world [[this-is-it.mdx]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets and the markdown file extension', () => {
|
||||
const input = 'hello world [[this-is-it.markdown]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world This Is It';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes the brackets even with numbers', () => {
|
||||
const input = 'hello world [[2020-07-21.markdown]]';
|
||||
const actual = removeBrackets(input);
|
||||
const expected = 'hello world 2020 07 21';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('removes brackets for more than one word', () => {
|
||||
const input =
|
||||
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
|
||||
const actual = removeBrackets(input);
|
||||
const expected =
|
||||
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTitleCase', () => {
|
||||
it('title cases a word', () => {
|
||||
const input =
|
||||
'look at this really long sentence but I am calling it a word';
|
||||
const actual = toTitleCase(input);
|
||||
const expected =
|
||||
'Look At This Really Long Sentence But I Am Calling It A Word';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('works on one word', () => {
|
||||
const input = 'word';
|
||||
const actual = toTitleCase(input);
|
||||
const expected = 'Word';
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { window, env, ExtensionContext, commands } from 'vscode';
|
||||
import { removeBrackets } from '../../utils';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
@@ -31,3 +30,46 @@ async function copyWithoutBrackets() {
|
||||
window.showInformationMessage('Successfully copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the "Copy to Clipboard Without Brackets" command
|
||||
*
|
||||
*/
|
||||
export function removeBrackets(s: string): string {
|
||||
// take in the string, split on space
|
||||
const stringSplitBySpace = s.split(' ');
|
||||
|
||||
// loop through words
|
||||
const modifiedWords = stringSplitBySpace.map(currentWord => {
|
||||
if (currentWord.includes('[[')) {
|
||||
// all of these transformations will turn this "[[you-are-awesome]]"
|
||||
// to this "you are awesome"
|
||||
let word = currentWord.replace(/(\[\[)/g, '');
|
||||
word = word.replace(/(\]\])/g, '');
|
||||
word = word.replace(/(.mdx|.md|.markdown)/g, '');
|
||||
word = word.replace(/[-]/g, ' ');
|
||||
|
||||
// then we titlecase the word so "you are awesome"
|
||||
// becomes "You Are Awesome"
|
||||
const titleCasedWord = toTitleCase(word);
|
||||
|
||||
return titleCasedWord;
|
||||
}
|
||||
|
||||
return currentWord;
|
||||
});
|
||||
|
||||
return modifiedWords.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a string and returns it titlecased
|
||||
*
|
||||
* @example toTitleCase("hello world") -> "Hello World"
|
||||
*/
|
||||
export function toTitleCase(word: string): string {
|
||||
return word
|
||||
.split(' ')
|
||||
.map(word => word[0].toUpperCase() + word.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
showInEditor,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { CREATE_NOTE_COMMAND } from './create-note';
|
||||
import { CREATE_NOTE_COMMAND, createNote } from './create-note';
|
||||
import { Location } from '../../core/model/location';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { ResourceLink } from '../../core/model/note';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
|
||||
describe('create-note command', () => {
|
||||
afterEach(() => {
|
||||
@@ -194,12 +198,57 @@ describe('factories', () => {
|
||||
describe('forPlaceholder', () => {
|
||||
it('adds the .md extension to notes created for placeholders', async () => {
|
||||
await closeEditors();
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder('my-placeholder');
|
||||
const link: ResourceLink = {
|
||||
type: 'wikilink',
|
||||
rawText: '[[my-placeholder]]',
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
isEmbed: false,
|
||||
};
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
Location.forObjectWithRange(URI.file(''), link),
|
||||
'.md'
|
||||
);
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
const doc = window.activeTextEditor.document;
|
||||
expect(doc.uri.path).toMatch(/my-placeholder.md$/);
|
||||
expect(doc.getText()).toMatch(/^# my-placeholder/);
|
||||
});
|
||||
|
||||
it('replaces the original placeholder based on the new note identifier (#1327)', async () => {
|
||||
await closeEditors();
|
||||
const templateA = await createFile(
|
||||
`---
|
||||
foam_template:
|
||||
name: 'Example Template'
|
||||
description: 'An example for reproducing a bug'
|
||||
filepath: '$FOAM_SLUG-world.md'
|
||||
---`,
|
||||
['.foam', 'templates', 'template-a.md']
|
||||
);
|
||||
|
||||
const noteA = await createFile(`this is my [[hello]]`);
|
||||
|
||||
const parser = createMarkdownParser();
|
||||
const res = parser.parse(noteA.uri, noteA.content);
|
||||
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
Location.forObjectWithRange(noteA.uri, res.links[0]),
|
||||
'.md',
|
||||
{
|
||||
templatePath: templateA.uri.path,
|
||||
}
|
||||
);
|
||||
const results: Awaited<ReturnType<typeof createNote>> =
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
expect(results.didCreateFile).toBeTruthy();
|
||||
expect(results.uri.path.endsWith('hello-world.md')).toBeTruthy();
|
||||
|
||||
const newNoteDoc = window.activeTextEditor.document;
|
||||
expect(newNoteDoc.uri.path).toMatch(/hello-world.md$/);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
expect(doc.getText()).toEqual(`this is my [[hello-world]]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,21 @@ import { Resolver } from '../../services/variable-resolver';
|
||||
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
|
||||
import { isSome } from '../../core/utils';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Location } from '../../core/model/location';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { ResourceLink } from '../../core/model/note';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
|
||||
export default async function activate(context: vscode.ExtensionContext) {
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, createNote)
|
||||
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, args =>
|
||||
createNote(args, foam)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +59,11 @@ interface CreateNoteArgs {
|
||||
* The title of the note (translates into the FOAM_TITLE variable)
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* The source link that triggered the creation of the note.
|
||||
* It will be updated with the appropriate identifier to the note, if necessary.
|
||||
*/
|
||||
sourceLink?: Location<ResourceLink>;
|
||||
/**
|
||||
* What to do in case the target file already exists
|
||||
*/
|
||||
@@ -66,7 +82,7 @@ const DEFAULT_NEW_NOTE_TEXT = `# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}`;
|
||||
|
||||
async function createNote(args: CreateNoteArgs) {
|
||||
export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
args = args ?? {};
|
||||
const date = isSome(args.date) ? new Date(Date.parse(args.date)) : new Date();
|
||||
const resolver = new Resolver(
|
||||
@@ -92,43 +108,71 @@ async function createNote(args: CreateNoteArgs) {
|
||||
: getDefaultTemplateUri();
|
||||
}
|
||||
|
||||
if (await fileExists(templateUri)) {
|
||||
return NoteFactory.createFromTemplate(
|
||||
templateUri,
|
||||
resolver,
|
||||
noteUri,
|
||||
text,
|
||||
args.onFileExists
|
||||
);
|
||||
} else {
|
||||
return NoteFactory.createNote(
|
||||
noteUri ?? (await getPathFromTitle(resolver)),
|
||||
text,
|
||||
resolver,
|
||||
args.onFileExists,
|
||||
args.onRelativeNotePath
|
||||
);
|
||||
const createdNote = (await fileExists(templateUri))
|
||||
? await NoteFactory.createFromTemplate(
|
||||
templateUri,
|
||||
resolver,
|
||||
noteUri,
|
||||
text,
|
||||
args.onFileExists
|
||||
)
|
||||
: await NoteFactory.createNote(
|
||||
noteUri ?? (await getPathFromTitle(resolver)),
|
||||
text,
|
||||
resolver,
|
||||
args.onFileExists,
|
||||
args.onRelativeNotePath
|
||||
);
|
||||
|
||||
if (args.sourceLink) {
|
||||
const identifier = foam.workspace.getIdentifier(createdNote.uri);
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
|
||||
target: identifier,
|
||||
});
|
||||
if (edit.newText !== args.sourceLink.data.rawText) {
|
||||
const updateLink = new vscode.WorkspaceEdit();
|
||||
const uri = toVsCodeUri(args.sourceLink.uri);
|
||||
updateLink.replace(
|
||||
uri,
|
||||
toVsCodeRange(args.sourceLink.range),
|
||||
edit.newText
|
||||
);
|
||||
await vscode.workspace.applyEdit(updateLink);
|
||||
}
|
||||
}
|
||||
return createdNote;
|
||||
}
|
||||
|
||||
export const CREATE_NOTE_COMMAND = {
|
||||
command: 'foam-vscode.create-note',
|
||||
|
||||
/**
|
||||
* Creates a command descriptor to create a note from the given placeholder.
|
||||
*
|
||||
* @param placeholder the placeholder
|
||||
* @param defaultExtension the default extension (e.g. '.md')
|
||||
* @param extra extra command arguments
|
||||
* @returns the command descriptor
|
||||
*/
|
||||
forPlaceholder: (
|
||||
placeholder: string,
|
||||
sourceLink: Location<ResourceLink>,
|
||||
defaultExtension: string,
|
||||
extra: Partial<CreateNoteArgs> = {}
|
||||
): CommandDescriptor<CreateNoteArgs> => {
|
||||
const title = placeholder.endsWith('.md')
|
||||
? placeholder.replace(/\.md$/, '')
|
||||
const endsWithDefaultExtension = new RegExp(defaultExtension + '$');
|
||||
const { target: placeholder } = MarkdownLink.analyzeLink(sourceLink.data);
|
||||
const title = placeholder.endsWith(defaultExtension)
|
||||
? placeholder.replace(endsWithDefaultExtension, '')
|
||||
: placeholder;
|
||||
const notePath = placeholder.endsWith('.md')
|
||||
const notePath = placeholder.endsWith(defaultExtension)
|
||||
? placeholder
|
||||
: placeholder + '.md';
|
||||
: placeholder + defaultExtension;
|
||||
return {
|
||||
name: CREATE_NOTE_COMMAND.command,
|
||||
params: {
|
||||
title,
|
||||
notePath,
|
||||
sourceLink,
|
||||
...extra,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,3 +10,4 @@ export { default as openResource } from './open-resource';
|
||||
export { default as updateGraphCommand } from './update-graph';
|
||||
export { default as updateWikilinksCommand } from './update-wikilinks';
|
||||
export { default as createNote } from './create-note';
|
||||
export { default as generateStandaloneNote } from './convert-links-format-in-note';
|
||||
|
||||
@@ -5,18 +5,18 @@ import {
|
||||
commands,
|
||||
ProgressLocation,
|
||||
} from 'vscode';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import {
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import { getWikilinkDefinitionSetting } from './update-wikilinks';
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { focusNote } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { focusNote } from '../../services/editor';
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
workspace,
|
||||
Position,
|
||||
} from 'vscode';
|
||||
import { isMdEditor, mdDocSelector } from '../../utils';
|
||||
import { isMdEditor, mdDocSelector } from '../../services/editor';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
|
||||
import { getEditorEOL } from '../../services/editor';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
|
||||
export default async function activate(
|
||||
@@ -58,6 +57,15 @@ export default async function activate(
|
||||
);
|
||||
}
|
||||
|
||||
export function getWikilinkDefinitionSetting():
|
||||
| 'withExtensions'
|
||||
| 'withoutExtensions'
|
||||
| 'off' {
|
||||
return workspace
|
||||
.getConfiguration('foam.edit')
|
||||
.get('linkReferenceDefinitions', 'withoutExtensions');
|
||||
}
|
||||
|
||||
async function updateWikilinkDefinitions(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
|
||||
@@ -38,21 +38,42 @@ const daysOfWeek = [
|
||||
];
|
||||
|
||||
const generateDayOfWeekSnippets = (): DateSnippet[] => {
|
||||
const getTarget = (day: number) => {
|
||||
const getFutureTarget = (day: number) => {
|
||||
const target = new Date();
|
||||
const currentDay = target.getDay();
|
||||
const distance = (day + 7 - currentDay) % 7;
|
||||
target.setDate(target.getDate() + distance);
|
||||
return target;
|
||||
};
|
||||
// needs work
|
||||
const getPastTarget = (day: number) => {
|
||||
const target = new Date();
|
||||
const currentDay = target.getDay();
|
||||
const distance = currentDay === day ? 7 : (7 + currentDay - day) % 7;
|
||||
target.setDate(target.getDate() - distance);
|
||||
return target;
|
||||
};
|
||||
|
||||
const snippets = daysOfWeek.map(({ day, index }) => {
|
||||
const target = getTarget(index);
|
||||
const target = getFutureTarget(index);
|
||||
return {
|
||||
date: target,
|
||||
detail: `Get a daily note link for ${day}`,
|
||||
snippet: `/${day}`,
|
||||
};
|
||||
});
|
||||
|
||||
// append snippets previous days
|
||||
snippets.push(
|
||||
...daysOfWeek.map(({ day, index }) => {
|
||||
const target = getPastTarget(index);
|
||||
return {
|
||||
date: target,
|
||||
detail: `Get a daily note link for last ${day}`,
|
||||
snippet: `/-${day}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
return snippets;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
@@ -14,6 +13,9 @@ import { FoamGraph } from '../core/model/graph';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
import { isSome } from '../core/utils';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
|
||||
@@ -106,12 +108,16 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
: this.workspace.get(targetUri).title;
|
||||
}
|
||||
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(targetUri.path, {
|
||||
askForTemplate: true,
|
||||
onFileExists: 'open',
|
||||
});
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
Location.forObjectWithRange(documentUri, targetLink),
|
||||
this.workspace.defaultExtension,
|
||||
{
|
||||
askForTemplate: true,
|
||||
onFileExists: 'open',
|
||||
}
|
||||
);
|
||||
const newNoteFromTemplate = new vscode.MarkdownString(
|
||||
`[Create note from template for '${targetUri.getName()}'](${commandAsURI(
|
||||
`[Create note from template for '${targetUri.getBasename()}'](${commandAsURI(
|
||||
command
|
||||
).toString()})`
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Resource } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
|
||||
export const aliasCommitCharacters = ['#'];
|
||||
export const linkCommitCharacters = ['#', '|'];
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { Location } from '../core/model/location';
|
||||
|
||||
describe('Document navigation', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
@@ -71,9 +72,8 @@ describe('Document navigation', () => {
|
||||
|
||||
it('should create links for placeholders', async () => {
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
const ws = createTestWorkspace().set(
|
||||
parser.parse(fileA.uri, fileA.content)
|
||||
);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
@@ -83,9 +83,13 @@ describe('Document navigation', () => {
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
commandAsURI(
|
||||
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', {
|
||||
onFileExists: 'open',
|
||||
})
|
||||
CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
Location.forObjectWithRange(noteA.uri, noteA.links[0]),
|
||||
'.md',
|
||||
{
|
||||
onFileExists: 'open',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));
|
||||
@@ -227,6 +231,10 @@ describe('Document navigation', () => {
|
||||
doc,
|
||||
new vscode.Position(0, 26)
|
||||
);
|
||||
|
||||
// Make sure the references are sorted by position, so we match the right expectation
|
||||
refs.sort((a, b) => a.range.start.character - b.range.start.character);
|
||||
|
||||
expect(refs.length).toEqual(2);
|
||||
expect(refs[0]).toEqual({
|
||||
uri: toVsCodeUri(fileB.uri),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
@@ -10,6 +9,8 @@ import { FoamGraph } from '../core/model/graph';
|
||||
import { Position } from '../core/model/position';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -146,10 +147,8 @@ export class NavigationProvider
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
const documentUri = fromVsCodeUri(document.uri);
|
||||
const resource = this.parser.parse(documentUri, document.getText());
|
||||
|
||||
const targets: { link: ResourceLink; target: URI }[] = resource.links.map(
|
||||
link => ({
|
||||
@@ -161,9 +160,13 @@ export class NavigationProvider
|
||||
return targets
|
||||
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
|
||||
.map(o => {
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(o.target.path, {
|
||||
onFileExists: 'open',
|
||||
});
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
Location.forObjectWithRange(documentUri, o.link),
|
||||
this.workspace.defaultExtension,
|
||||
{
|
||||
onFileExists: 'open',
|
||||
}
|
||||
);
|
||||
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createNote,
|
||||
getUriInWorkspace,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { ConnectionsTreeDataProvider } from './backlinks';
|
||||
import { ConnectionsTreeDataProvider } from './connections';
|
||||
import { MapBasedMemento, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import {
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { isNone } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Connection, FoamGraph } from '../../core/model/graph';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
createConnectionItemsForResource,
|
||||
} from './utils/tree-view-utils';
|
||||
import { BaseTreeProvider } from './utils/base-tree-provider';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -30,7 +30,6 @@ export default async function activate(
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
|
||||
const updateTreeView = async () => {
|
||||
provider.target = vscode.window.activeTextEditor
|
||||
@@ -53,12 +52,7 @@ export default async function activate(
|
||||
}
|
||||
|
||||
export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeItem> {
|
||||
public show = new ContextMemento<'connections' | 'backlinks' | 'links'>(
|
||||
this.state,
|
||||
`foam-vscode.views.connections.show`,
|
||||
'connections',
|
||||
true
|
||||
);
|
||||
public show: ContextMemento<'all links' | 'backlinks' | 'forward links'>;
|
||||
public target?: URI = undefined;
|
||||
public nValues = 0;
|
||||
private connectionItems: ResourceRangeTreeItem[] = [];
|
||||
@@ -70,14 +64,20 @@ export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeIte
|
||||
registerCommands = true // for testing. don't love it, but will do for now
|
||||
) {
|
||||
super();
|
||||
this.show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
|
||||
this.state,
|
||||
`foam-vscode.views.connections.show`,
|
||||
'all links',
|
||||
true
|
||||
);
|
||||
if (!registerCommands) {
|
||||
return;
|
||||
}
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.connections.show:connections`,
|
||||
`foam-vscode.views.connections.show:all-links`,
|
||||
() => {
|
||||
this.show.update('connections');
|
||||
this.show.update('all links');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
@@ -89,9 +89,9 @@ export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeIte
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.connections.show:links`,
|
||||
`foam-vscode.views.connections.show:forward-links`,
|
||||
() => {
|
||||
this.show.update('links');
|
||||
this.show.update('forward links');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
@@ -113,9 +113,9 @@ export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeIte
|
||||
.asPlain()
|
||||
.isEqual(this.target);
|
||||
return (
|
||||
this.show.get() === 'connections' ||
|
||||
this.show.get() === 'all links' ||
|
||||
(isBacklink && this.show.get() === 'backlinks') ||
|
||||
(!isBacklink && this.show.get() === 'links')
|
||||
(!isBacklink && this.show.get() === 'forward links')
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../../settings';
|
||||
import { isSome } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { isSome } from '../../core/utils';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -104,7 +102,9 @@ function generateGraphData(foam: Foam) {
|
||||
}
|
||||
|
||||
function cutTitle(title: string): string {
|
||||
const maxLen = getTitleMaxLength();
|
||||
const maxLen = vscode.workspace
|
||||
.getConfiguration('foam.graph')
|
||||
.get('titleMaxLength', 24);
|
||||
if (maxLen > 0 && title.length > maxLen) {
|
||||
return title.substring(0, maxLen).concat('...');
|
||||
}
|
||||
@@ -166,28 +166,37 @@ async function getWebviewContent(
|
||||
context: vscode.ExtensionContext,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const datavizPath = vscode.Uri.joinPath(
|
||||
vscode.Uri.file(context.extensionPath),
|
||||
const datavizUri = vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
'static',
|
||||
'dataviz'
|
||||
);
|
||||
|
||||
const getWebviewUri = (fileName: string) =>
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizPath, fileName));
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizUri, fileName));
|
||||
|
||||
const indexHtml = await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.joinPath(datavizPath, 'index.html')
|
||||
);
|
||||
const indexHtml =
|
||||
vscode.env.uiKind === vscode.UIKind.Desktop
|
||||
? new TextDecoder('utf-8').decode(
|
||||
await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.joinPath(datavizUri, 'index.html')
|
||||
)
|
||||
)
|
||||
: await fetch(getWebviewUri('index.html').toString()).then(r => r.text());
|
||||
|
||||
// Replace the script paths with the appropriate webview URI.
|
||||
const filled = new TextDecoder('utf-8')
|
||||
.decode(indexHtml)
|
||||
.replace(/data-replace (src|href)="[^"]+"/g, match => {
|
||||
const filled = indexHtml.replace(
|
||||
/data-replace (src|href)="[^"]+"/g,
|
||||
match => {
|
||||
const i = match.indexOf(' ');
|
||||
const j = match.indexOf('=');
|
||||
const uri = getWebviewUri(match.slice(j + 2, -1).trim());
|
||||
return match.slice(i + 1, j) + '="' + uri.toString() + '"';
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
function getGraphStyle(): object {
|
||||
return vscode.workspace.getConfiguration('foam.graph').get('style');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as backlinks } from './backlinks';
|
||||
export { default as backlinks } from './connections';
|
||||
export { default as dataviz } from './dataviz';
|
||||
export { default as orphans } from './orphans';
|
||||
export { default as placeholders } from './placeholders';
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
createBacklinkItemsForResource as createBacklinkTreeItemsForResource,
|
||||
expandAll,
|
||||
} from './utils/tree-view-utils';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
@@ -43,6 +44,7 @@ export default async function activate(
|
||||
// cause the tree view to take the focus from the item when
|
||||
// browsing the notes explorer
|
||||
if (
|
||||
item &&
|
||||
!treeView.selection.find(
|
||||
i => i.resourceUri?.path === item.resourceUri.path
|
||||
)
|
||||
@@ -59,6 +61,11 @@ export default async function activate(
|
||||
foam.graph.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.expand-all`,
|
||||
(...args) =>
|
||||
expandAll(treeView, provider, node => node.contextValue === 'folder')
|
||||
),
|
||||
vscode.window.onDidChangeActiveTextEditor(revealTextEditorItem),
|
||||
treeView.onDidChangeVisibility(revealTextEditorItem)
|
||||
);
|
||||
@@ -84,11 +91,7 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
NotesTreeItems,
|
||||
Resource
|
||||
> {
|
||||
public show = new ContextMemento<'all' | 'notes-only'>(
|
||||
this.state,
|
||||
`foam-vscode.views.notes-explorer.show`,
|
||||
'all'
|
||||
);
|
||||
public show: ContextMemento<'all' | 'notes-only'>;
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
@@ -96,6 +99,12 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
private state: vscode.Memento
|
||||
) {
|
||||
super();
|
||||
this.show = new ContextMemento<'all' | 'notes-only'>(
|
||||
this.state,
|
||||
`foam-vscode.views.notes-explorer.show`,
|
||||
'all'
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.show:all`,
|
||||
@@ -133,26 +142,23 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
return parts;
|
||||
}
|
||||
|
||||
isValueType(value: Resource): value is Resource {
|
||||
return value.uri != null;
|
||||
}
|
||||
|
||||
createValueTreeItem(
|
||||
value: Resource,
|
||||
parent: FolderTreeItem<Resource>
|
||||
): NotesTreeItems {
|
||||
const res = new ResourceTreeItem(value, this.workspace, {
|
||||
const item = new ResourceTreeItem(value, this.workspace, {
|
||||
parent,
|
||||
collapsibleState:
|
||||
this.graph.getBacklinks(value.uri).length > 0
|
||||
? vscode.TreeItemCollapsibleState.Collapsed
|
||||
: vscode.TreeItemCollapsibleState.None,
|
||||
});
|
||||
res.getChildren = async () => {
|
||||
item.id = value.uri.toString();
|
||||
item.getChildren = async () => {
|
||||
const backlinks = await createBacklinkTreeItemsForResource(
|
||||
this.workspace,
|
||||
this.graph,
|
||||
res.uri
|
||||
item.uri
|
||||
);
|
||||
backlinks.forEach(item => {
|
||||
item.description = item.label;
|
||||
@@ -160,6 +166,11 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
});
|
||||
return backlinks;
|
||||
};
|
||||
return res;
|
||||
item.description =
|
||||
value.uri.getName().toLocaleLowerCase() ===
|
||||
value.title.toLocaleLowerCase()
|
||||
? undefined
|
||||
: value.uri.getBasename();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getOrphansConfig } from '../../settings';
|
||||
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
|
||||
import { getAttachmentsExtensions } from '../../settings';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
} from './utils/grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from './utils/tree-view-utils';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { imageExtensions } from '../../core/services/attachment-provider';
|
||||
|
||||
const EXCLUDE_TYPES = ['image', 'attachment'];
|
||||
export default async function activate(
|
||||
@@ -44,6 +49,13 @@ export default async function activate(
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieve the orphans configuration */
|
||||
export function getOrphansConfig(): GroupedResourcesConfig {
|
||||
const orphansConfig = vscode.workspace.getConfiguration('foam.orphans');
|
||||
const exclude: string[] = orphansConfig.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
|
||||
constructor(
|
||||
state: vscode.Memento,
|
||||
@@ -66,6 +78,13 @@ export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
|
||||
.filter(
|
||||
uri =>
|
||||
!EXCLUDE_TYPES.includes(this.workspace.find(uri)?.type) &&
|
||||
this.graph.getConnections(uri).length === 0
|
||||
this.graph.getBacklinks(uri).length === 0 &&
|
||||
this.graph.getLinks(uri).filter(c => !isAttachment(c.target))
|
||||
.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
function isAttachment(uri: URI) {
|
||||
const ext = [...getAttachmentsExtensions(), ...imageExtensions];
|
||||
return ext.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getPlaceholdersConfig } from '../../settings';
|
||||
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
} from './utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
UriTreeItem,
|
||||
createBacklinkItemsForResource,
|
||||
expandAll,
|
||||
groupRangesByResource,
|
||||
} from './utils/tree-view-utils';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
@@ -15,6 +18,13 @@ import { URI } from '../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FolderTreeItem } from './utils/folder-tree-provider';
|
||||
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
const placeholderCfg = vscode.workspace.getConfiguration('foam.placeholders');
|
||||
const exclude: string[] = placeholderCfg.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
@@ -47,6 +57,17 @@ export default async function activate(
|
||||
provider.onDidChangeTreeData(() => {
|
||||
treeView.title = baseTitle + ` (${provider.nValues})`;
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.placeholders.expand-all`,
|
||||
() =>
|
||||
expandAll(
|
||||
treeView,
|
||||
provider,
|
||||
node =>
|
||||
node.contextValue === 'placeholder' ||
|
||||
node.contextValue === 'folder'
|
||||
)
|
||||
),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
if (provider.show.get() === 'for-current-file') {
|
||||
provider.refresh();
|
||||
@@ -92,6 +113,8 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
|
||||
parent,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
});
|
||||
item.contextValue = 'placeholder';
|
||||
item.id = uri.toString();
|
||||
item.getChildren = async () => {
|
||||
return groupRangesByResource(
|
||||
this.workspace,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const treeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -43,12 +43,12 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
@@ -57,7 +57,7 @@ describe('Tags tree panel', () => {
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).toEqual('child');
|
||||
expect(child.label).toEqual('child');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -81,7 +81,7 @@ describe('Tags tree panel', () => {
|
||||
item => item instanceof TagItem
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).toEqual('parent');
|
||||
expect(parentTreeItems).toHaveLength(1);
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
@@ -91,12 +91,12 @@ describe('Tags tree panel', () => {
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(['child', 'subchild']).toContain(child.title);
|
||||
expect(['child', 'subchild']).toContain(child.label);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).not.toEqual('parent');
|
||||
expect(child.label).not.toEqual('parent');
|
||||
}
|
||||
});
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles a parent and child tag in the same note', async () => {
|
||||
@@ -107,7 +107,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteC);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Tags tree panel', () => {
|
||||
item => item instanceof TagItem
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('main');
|
||||
expect(parentTagItem.label).toEqual('main');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
@@ -132,10 +132,10 @@ describe('Tags tree panel', () => {
|
||||
.filter(item => item instanceof TagItem)
|
||||
.forEach(item => {
|
||||
expect(['main/subtopic']).toContain(item.tag);
|
||||
expect(item.title).toEqual('subtopic');
|
||||
expect(item.label).toEqual('subtopic');
|
||||
});
|
||||
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles a tag with multiple levels of hierarchy - #1134', async () => {
|
||||
@@ -145,28 +145,26 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
)) as TagItem[];
|
||||
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
expect(childTreeItems[0].label).toMatch(/^Search.*/);
|
||||
expect(childTreeItems[1].label).toEqual('child');
|
||||
expect(childTreeItems).toHaveLength(1);
|
||||
expect(childTreeItems[0].label).toEqual('child');
|
||||
|
||||
const grandchildTreeItems = (await provider.getChildren(
|
||||
childTreeItems[1]
|
||||
childTreeItems[0]
|
||||
)) as TagItem[];
|
||||
|
||||
expect(grandchildTreeItems).toHaveLength(2);
|
||||
expect(grandchildTreeItems[0].label).toMatch(/^Search.*/);
|
||||
expect(grandchildTreeItems[1].label).toEqual('second');
|
||||
expect(grandchildTreeItems).toHaveLength(1);
|
||||
expect(grandchildTreeItems[0].label).toEqual('second');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,20 @@ import { FoamTags } from '../../core/model/tags';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
expandAll,
|
||||
groupRangesByResource,
|
||||
} from './utils/tree-view-utils';
|
||||
import {
|
||||
Folder,
|
||||
FolderTreeItem,
|
||||
FolderTreeProvider,
|
||||
walk,
|
||||
} from './utils/folder-tree-provider';
|
||||
import {
|
||||
ContextMemento,
|
||||
MapBasedMemento,
|
||||
fromVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
|
||||
const TAG_SEPARATOR = '/';
|
||||
export default async function activate(
|
||||
@@ -20,52 +32,136 @@ export default async function activate(
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
provider.refresh();
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
foam.tags.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
})
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
if (provider.show.get() === 'for-current-file') {
|
||||
provider.refresh();
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${provider.providerId}.expand-all`,
|
||||
() =>
|
||||
expandAll(
|
||||
treeView,
|
||||
provider,
|
||||
node => node.contextValue === 'tag' || node.contextValue === 'folder'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<
|
||||
TagTreeItem | undefined | void
|
||||
> = new vscode.EventEmitter<TagTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> =
|
||||
this._onDidChangeTreeData.event;
|
||||
export class TagsProvider extends FolderTreeProvider<TagTreeItem, string> {
|
||||
public providerId = 'tags-explorer';
|
||||
public show = new ContextMemento<'all' | 'for-current-file'>(
|
||||
new MapBasedMemento(),
|
||||
`foam-vscode.views.${this.providerId}.show`,
|
||||
'all'
|
||||
);
|
||||
public groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
new MapBasedMemento(),
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
|
||||
private tags: {
|
||||
tag: string;
|
||||
notes: URI[];
|
||||
}[];
|
||||
|
||||
private foamTags: FoamTags;
|
||||
|
||||
constructor(tags: FoamTags, private workspace: FoamWorkspace) {
|
||||
this.foamTags = tags;
|
||||
this.computeTags();
|
||||
constructor(
|
||||
private foamTags: FoamTags,
|
||||
private workspace: FoamWorkspace,
|
||||
registerCommands: boolean = true
|
||||
) {
|
||||
super();
|
||||
if (!registerCommands) {
|
||||
return;
|
||||
}
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.show:all`,
|
||||
() => {
|
||||
this.show.update('all');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.show:for-current-file`,
|
||||
() => {
|
||||
this.show.update('for-current-file');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:folder`,
|
||||
() => {
|
||||
this.groupBy.update('folder');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:off`,
|
||||
() => {
|
||||
this.groupBy.update('off');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.computeTags();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private computeTags() {
|
||||
this.tags = [...this.foamTags.tags]
|
||||
.map(([tag, notes]) => ({ tag, notes }))
|
||||
.sort((a, b) => a.tag.localeCompare(b.tag));
|
||||
super.refresh();
|
||||
}
|
||||
|
||||
getTreeItem(element: TagTreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
getValues(): string[] {
|
||||
if (this.show.get() === 'for-current-file') {
|
||||
const uriInEditor = vscode.window.activeTextEditor?.document.uri;
|
||||
const currentResource = this.workspace.find(fromVsCodeUri(uriInEditor));
|
||||
return currentResource?.tags.map(t => t.label) ?? [];
|
||||
}
|
||||
return Array.from(this.tags.values()).map(tag => tag.tag);
|
||||
}
|
||||
|
||||
valueToPath(value: string) {
|
||||
return this.groupBy.get() === 'off' ? [value] : value.split(TAG_SEPARATOR);
|
||||
}
|
||||
|
||||
private countResourcesInSubtree(node: Folder<string>) {
|
||||
const nChildren = walk(
|
||||
node,
|
||||
tag => this.foamTags.tags.get(tag)?.length ?? 0
|
||||
).reduce((acc, nResources) => acc + nResources, 0);
|
||||
return nChildren;
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
node: Folder<string>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<string>
|
||||
): FolderTreeItem<string> {
|
||||
const nChildren = this.countResourcesInSubtree(node);
|
||||
return new TagItem(node, nChildren, [], parent);
|
||||
}
|
||||
|
||||
createValueTreeItem(
|
||||
value: string,
|
||||
parent: FolderTreeItem<string>,
|
||||
node: Folder<string>
|
||||
): TagItem {
|
||||
const nChildren = this.countResourcesInSubtree(node);
|
||||
const resources = this.foamTags.tags.get(value) ?? [];
|
||||
return new TagItem(node, nChildren, resources, parent);
|
||||
}
|
||||
|
||||
async getChildren(element?: TagItem): Promise<TagTreeItem[]> {
|
||||
@@ -73,37 +169,11 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
const children = await (element as any).getChildren();
|
||||
return children;
|
||||
}
|
||||
const parentTag = element ? element.tag : '';
|
||||
const parentPrefix = element ? parentTag + TAG_SEPARATOR : '';
|
||||
|
||||
const tagsAtThisLevel = this.tags
|
||||
.filter(({ tag }) => tag.startsWith(parentPrefix))
|
||||
.map(({ tag }) => {
|
||||
const nextSeparator = tag.indexOf(TAG_SEPARATOR, parentPrefix.length);
|
||||
const label =
|
||||
nextSeparator > -1
|
||||
? tag.substring(parentPrefix.length, nextSeparator)
|
||||
: tag.substring(parentPrefix.length);
|
||||
const tagId = parentPrefix + label;
|
||||
return { label, tagId, tag };
|
||||
})
|
||||
.reduce((acc, { label, tagId, tag }) => {
|
||||
const existing = acc.has(label);
|
||||
const nResources = this.foamTags.tags.get(tag).length ?? 0;
|
||||
if (!existing) {
|
||||
acc.set(label, { label, tagId, nResources: 0 });
|
||||
}
|
||||
acc.get(label).nResources += nResources;
|
||||
return acc;
|
||||
}, new Map() as Map<string, { label: string; tagId: string; nResources: number }>);
|
||||
|
||||
const subtags = Array.from(tagsAtThisLevel.values())
|
||||
.map(({ label, tagId, nResources }) => {
|
||||
const resources = this.foamTags.tags.get(tagId) ?? [];
|
||||
return new TagItem(tagId, label, nResources, resources);
|
||||
})
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
// Subtags are managed by the FolderTreeProvider
|
||||
const subtags = await super.getChildren(element);
|
||||
|
||||
// Compute the resources children
|
||||
const resourceTags: ResourceRangeTreeItem[] = (element?.notes ?? [])
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.reduce((acc, note) => {
|
||||
@@ -118,41 +188,31 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
);
|
||||
return [...acc, ...items];
|
||||
}, []);
|
||||
const resources = (
|
||||
await groupRangesByResource(this.workspace, resourceTags)
|
||||
).map(item => {
|
||||
item.id = element.tag + ' / ' + item.uri.toString();
|
||||
return item;
|
||||
});
|
||||
|
||||
const resources = await groupRangesByResource(this.workspace, resourceTags);
|
||||
|
||||
return Promise.resolve(
|
||||
[element && new TagSearch(element.tag), ...subtags, ...resources].filter(
|
||||
Boolean
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
|
||||
if (
|
||||
item instanceof ResourceTreeItem ||
|
||||
item instanceof ResourceRangeTreeItem
|
||||
) {
|
||||
return item.resolveTreeItem();
|
||||
}
|
||||
return Promise.resolve(item);
|
||||
return [...subtags, ...resources];
|
||||
}
|
||||
}
|
||||
|
||||
type TagTreeItem =
|
||||
| TagItem
|
||||
| TagSearch
|
||||
| ResourceTreeItem
|
||||
| ResourceRangeTreeItem;
|
||||
type TagTreeItem = TagItem | ResourceTreeItem | ResourceRangeTreeItem;
|
||||
|
||||
export class TagItem extends FolderTreeItem<string> {
|
||||
public readonly tag: string;
|
||||
|
||||
export class TagItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly tag: string,
|
||||
public readonly title: string,
|
||||
public readonly node: Folder<string>,
|
||||
public readonly nResourcesInSubtree: number,
|
||||
public readonly notes: URI[]
|
||||
public readonly notes: URI[],
|
||||
public readonly parentElement?: FolderTreeItem<string>
|
||||
) {
|
||||
super(title, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
super(node, node.path.slice(-1)[0], parentElement);
|
||||
this.tag = node.path.join(TAG_SEPARATOR);
|
||||
this.id = this.tag;
|
||||
this.description = `${nResourcesInSubtree} reference${
|
||||
nResourcesInSubtree !== 1 ? 's' : ''
|
||||
}`;
|
||||
@@ -161,27 +221,3 @@ export class TagItem extends vscode.TreeItem {
|
||||
iconPath = new vscode.ThemeIcon('symbol-number');
|
||||
contextValue = 'tag';
|
||||
}
|
||||
|
||||
export class TagSearch extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: string) {
|
||||
super(`Search #${tag}`, vscode.TreeItemCollapsibleState.None);
|
||||
const searchString = `#${tag}`;
|
||||
this.tooltip = `Search ${searchString} in workspace`;
|
||||
this.command = {
|
||||
command: 'workbench.action.findInFiles',
|
||||
arguments: [
|
||||
{
|
||||
query: searchString,
|
||||
triggerSearch: true,
|
||||
matchWholeWord: true,
|
||||
isCaseSensitive: true,
|
||||
},
|
||||
],
|
||||
title: 'Search',
|
||||
};
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon('search');
|
||||
contextValue = 'tag-search';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import { BaseTreeItem, ResourceTreeItem } from './tree-view-utils';
|
||||
* A folder is a map of basenames to either folders or values (e.g. resources).
|
||||
*/
|
||||
export interface Folder<T> {
|
||||
[basename: string]: Folder<T> | T;
|
||||
children: {
|
||||
[basename: string]: Folder<T>;
|
||||
};
|
||||
value?: T;
|
||||
path: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +22,7 @@ export class FolderTreeItem<T> extends vscode.TreeItem {
|
||||
iconPath = new vscode.ThemeIcon('folder');
|
||||
|
||||
constructor(
|
||||
public parent: Folder<T>,
|
||||
public node: Folder<T>,
|
||||
public name: string,
|
||||
public parentElement?: FolderTreeItem<T>
|
||||
) {
|
||||
@@ -52,11 +56,11 @@ export abstract class FolderTreeProvider<I, T> extends BaseTreeProvider<I> {
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
value: Folder<T>,
|
||||
node: Folder<T>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<T>
|
||||
) {
|
||||
return new FolderTreeItem<T>(value, name, parent);
|
||||
return new FolderTreeItem<T>(node, name, parent);
|
||||
}
|
||||
|
||||
async getChildren(item?: I): Promise<I[]> {
|
||||
@@ -64,42 +68,52 @@ export abstract class FolderTreeProvider<I, T> extends BaseTreeProvider<I> {
|
||||
return item.getChildren() as Promise<I[]>;
|
||||
}
|
||||
|
||||
const parent = (item as any)?.parent ?? this.root;
|
||||
const parent: Folder<T> = (item as any)?.node ?? this.root;
|
||||
|
||||
const children: vscode.TreeItem[] = Object.keys(parent).map(name => {
|
||||
const value = parent[name];
|
||||
if (this.isValueType(value)) {
|
||||
return this.createValueTreeItem(value, undefined);
|
||||
} else {
|
||||
return this.createFolderTreeItem(
|
||||
value,
|
||||
name,
|
||||
item as unknown as FolderTreeItem<T>
|
||||
);
|
||||
const children: vscode.TreeItem[] = Object.keys(parent?.children ?? []).map(
|
||||
name => {
|
||||
const node = parent.children[name];
|
||||
if (node.value != null) {
|
||||
return this.createValueTreeItem(node.value, undefined, node);
|
||||
} else {
|
||||
return this.createFolderTreeItem(
|
||||
node,
|
||||
name,
|
||||
item as unknown as FolderTreeItem<T>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return children.sort((a, b) => sortFolderTreeItems(a, b)) as any;
|
||||
}
|
||||
|
||||
createTree(values: T[], filterFn: (value: T) => boolean): Folder<T> {
|
||||
const root: Folder<T> = {};
|
||||
const root: Folder<T> = {
|
||||
children: {},
|
||||
path: [],
|
||||
};
|
||||
|
||||
for (const r of values) {
|
||||
const parts = this.valueToPath(r);
|
||||
let currentNode: Folder<T> = root;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!currentNode[part]) {
|
||||
if (!currentNode.children[part]) {
|
||||
if (index < parts.length - 1) {
|
||||
currentNode[part] = {};
|
||||
} else {
|
||||
if (filterFn(r)) {
|
||||
currentNode[part] = r;
|
||||
}
|
||||
currentNode.children[part] = {
|
||||
children: {},
|
||||
path: parts.slice(0, index + 1),
|
||||
};
|
||||
} else if (filterFn(r)) {
|
||||
currentNode.children[part] = {
|
||||
children: {},
|
||||
path: parts.slice(0, index + 1),
|
||||
value: r,
|
||||
};
|
||||
}
|
||||
}
|
||||
currentNode = currentNode[part] as Folder<T>;
|
||||
currentNode = currentNode.children[part];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,24 +123,25 @@ export abstract class FolderTreeProvider<I, T> extends BaseTreeProvider<I> {
|
||||
|
||||
getTreeItemsHierarchy(path: string[]): vscode.TreeItem[] {
|
||||
const treeItemsHierarchy: vscode.TreeItem[] = [];
|
||||
let currentNode: Folder<T> | T = this.root;
|
||||
let currentNode: Folder<T> = this.root;
|
||||
|
||||
for (const part of path) {
|
||||
if (currentNode[part] !== undefined) {
|
||||
currentNode = currentNode[part] as Folder<T> | T;
|
||||
if (this.isValueType(currentNode as T)) {
|
||||
if (currentNode.children[part] !== undefined) {
|
||||
currentNode = currentNode.children[part] as Folder<T>;
|
||||
if (currentNode.value) {
|
||||
treeItemsHierarchy.push(
|
||||
this.createValueTreeItem(
|
||||
currentNode as T,
|
||||
currentNode.value,
|
||||
treeItemsHierarchy[
|
||||
treeItemsHierarchy.length - 1
|
||||
] as FolderTreeItem<T>
|
||||
] as FolderTreeItem<T>,
|
||||
currentNode
|
||||
)
|
||||
);
|
||||
} else {
|
||||
treeItemsHierarchy.push(
|
||||
new FolderTreeItem(
|
||||
currentNode as Folder<T>,
|
||||
currentNode,
|
||||
part,
|
||||
treeItemsHierarchy[
|
||||
treeItemsHierarchy.length - 1
|
||||
@@ -172,16 +187,36 @@ export abstract class FolderTreeProvider<I, T> extends BaseTreeProvider<I> {
|
||||
*/
|
||||
abstract getValues(): T[];
|
||||
|
||||
/**
|
||||
* Returns true if the given value is of the type that should be displayed
|
||||
* as a leaf in the tree. That is, not as a folder.
|
||||
*/
|
||||
abstract isValueType(value: T): value is T;
|
||||
|
||||
/**
|
||||
* Creates a tree item for the given value.
|
||||
*/
|
||||
abstract createValueTreeItem(value: T, parent: FolderTreeItem<T>): I;
|
||||
abstract createValueTreeItem(
|
||||
value: T,
|
||||
parent: FolderTreeItem<T>,
|
||||
node: Folder<T>
|
||||
): I;
|
||||
}
|
||||
|
||||
/**
|
||||
* walks the node and performs an action on each value
|
||||
* @returns
|
||||
*/
|
||||
export function walk<T, R>(node: Folder<T>, fn: (value: T) => R): R[] {
|
||||
const results: R[] = [];
|
||||
|
||||
function traverse(node: Folder<T>) {
|
||||
if (node.value) {
|
||||
results.push(fn(node.value));
|
||||
}
|
||||
|
||||
Object.values(node.children).forEach(child => {
|
||||
traverse(child);
|
||||
});
|
||||
}
|
||||
|
||||
traverse(node);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function sortFolderTreeItems(a: vscode.TreeItem, b: vscode.TreeItem): number {
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
Folder,
|
||||
} from './folder-tree-provider';
|
||||
|
||||
export interface GroupedResourcesConfig {
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
type GroupedResourceTreeItem = UriTreeItem | FolderTreeItem<URI>;
|
||||
|
||||
/**
|
||||
@@ -31,11 +35,7 @@ export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvide
|
||||
GroupedResourceTreeItem,
|
||||
URI
|
||||
> {
|
||||
public groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
public groupBy: ContextMemento<'off' | 'folder'>;
|
||||
|
||||
/**
|
||||
* Creates an instance of GroupedResourcesTreeDataProvider.
|
||||
@@ -57,6 +57,12 @@ export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvide
|
||||
private matcher: IMatcher
|
||||
) {
|
||||
super();
|
||||
this.groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:folder`,
|
||||
@@ -92,18 +98,14 @@ export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvide
|
||||
return uris.filter(uri => this.matcher.isMatch(uri));
|
||||
}
|
||||
|
||||
isValueType(value: URI): value is URI {
|
||||
return value instanceof URI;
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
value: Folder<URI>,
|
||||
node: Folder<URI>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<URI>
|
||||
) {
|
||||
const item = super.createFolderTreeItem(value, name, parent);
|
||||
const item = super.createFolderTreeItem(node, name, parent);
|
||||
item.label = item.label || '(Not Created)';
|
||||
item.description = `(${Object.keys(value).length})`;
|
||||
item.description = `(${Object.keys(node.children).length})`;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { toVsCodeUri } from '../../../utils/vsc-utils';
|
||||
import { Range } from '../../../core/model/range';
|
||||
import { URI } from '../../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../../core/model/workspace';
|
||||
import { getNoteTooltip } from '../../../utils';
|
||||
import { isSome } from '../../../core/utils';
|
||||
import { getBlockFor } from '../../../core/services/markdown-parser';
|
||||
import { Connection, FoamGraph } from '../../../core/model/graph';
|
||||
import { Logger } from '../../../core/utils/log';
|
||||
import { getNoteTooltip } from '../../../services/editor';
|
||||
|
||||
export class BaseTreeItem extends vscode.TreeItem {
|
||||
resolveTreeItem(): Promise<vscode.TreeItem> {
|
||||
@@ -227,3 +228,57 @@ export function createConnectionItemsForResource(
|
||||
});
|
||||
return Promise.all(backlinkItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a node and its children in a tree view that match a given predicate
|
||||
*
|
||||
* @param treeView - The tree view to expand nodes in
|
||||
* @param provider - The tree data provider for the view
|
||||
* @param element - The element to expand
|
||||
* @param when - A function that returns true if the node should be expanded
|
||||
*/
|
||||
export async function expandNode<T>(
|
||||
treeView: vscode.TreeView<any>,
|
||||
provider: vscode.TreeDataProvider<T>,
|
||||
element: T,
|
||||
when: (element: T) => boolean
|
||||
) {
|
||||
try {
|
||||
if (when(element)) {
|
||||
await treeView.reveal(element, {
|
||||
select: false,
|
||||
focus: false,
|
||||
expand: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const obj = element as any;
|
||||
const label = obj.label ?? obj.toString();
|
||||
Logger.warn(
|
||||
`Could not expand element: ${label}. Try setting the ID property of the TreeItem`
|
||||
);
|
||||
}
|
||||
|
||||
const children = await provider.getChildren(element);
|
||||
for (const child of children) {
|
||||
await expandNode(treeView, provider, child, when);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all items in a tree view that match a given predicate
|
||||
*
|
||||
* @param treeView - The tree view to expand items in
|
||||
* @param provider - The tree data provider for the view
|
||||
* @param when - A function that returns true if the node should be expanded
|
||||
*/
|
||||
export async function expandAll<T>(
|
||||
treeView: vscode.TreeView<T>,
|
||||
provider: vscode.TreeDataProvider<T>,
|
||||
when: (element: T) => boolean = () => true
|
||||
) {
|
||||
const elements = await provider.getChildren(undefined);
|
||||
for (const element of elements) {
|
||||
await expandNode(treeView, provider, element, when);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export const markdownItFoamTags = (
|
||||
md: markdownit,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace,
|
||||
parser: ResourceParser
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
return `
|
||||
<div style="padding: 0.25em; margin: 1.5em 0; text-align: center; border: 1px solid var(--vscode-editorLineNumber-foreground);">
|
||||
Embeds are not supported in web extension: <br/> ${wikilinkItem}
|
||||
</div>`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
@@ -8,21 +8,21 @@ import {
|
||||
} from '../../test/test-utils-vscode';
|
||||
import {
|
||||
default as markdownItWikilinkEmbed,
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
} from './wikilink-embed';
|
||||
|
||||
const parser = createMarkdownParser();
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
it('should render an included note in flat mode', async () => {
|
||||
it('should render an included note in full inline mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -40,16 +40,16 @@ describe('Displaying included notes in preview', () => {
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included note in container mode', async () => {
|
||||
it('should render an included note in full card mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('Displaying included notes in preview', () => {
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section', async () => {
|
||||
it('should render an included section in full inline mode', async () => {
|
||||
// here we use createFile as the test note doesn't fill in
|
||||
// all the metadata we need
|
||||
const note = await createFile(
|
||||
@@ -83,8 +83,8 @@ This is the third section of note E
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
@@ -102,7 +102,7 @@ This is the third section of note E
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section in container mode', async () => {
|
||||
it('should render an included section in full card mode', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
@@ -120,8 +120,8 @@ This is the third section of note E
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -138,6 +138,220 @@ This is the third section of note E
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the title of a note in content inline mode', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Title
|
||||
## Section 1
|
||||
|
||||
This is the first section of note E`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><h2>Section 1</h2>
|
||||
<p>This is the first section of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the title of a note in content card mode', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
## Section 1
|
||||
|
||||
This is the first section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-e.md]]`);
|
||||
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('Section 1');
|
||||
expect(res).toContain('This is the first section of note E');
|
||||
expect(res).not.toContain('Title');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the section title, but still render subsection titles in content inline mode', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
|
||||
|
||||
## Section 1
|
||||
This is the first section of note E
|
||||
|
||||
### Subsection a
|
||||
This is the first subsection of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 1]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the first section of note E</p>
|
||||
<h3>Subsection a</h3>
|
||||
<p>This is the first subsection of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the subsection title in content mode if you link to it and regardless of its level', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
## Section 1
|
||||
This is the first section of note E
|
||||
|
||||
### Subsection a
|
||||
This is the first subsection of note E`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Subsection a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the first subsection of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should allow a note embedding type to be overridden if a modifier is passed in', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note E
|
||||
|
||||
# Section 3
|
||||
This is the third section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
content![[note-e#Section 2]]
|
||||
|
||||
full![[note-e#Section 3]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the second section of note E</p>
|
||||
</p>
|
||||
<p><h1>Section 3</h1>
|
||||
<p>This is the third section of note E</p>
|
||||
</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should allow a note embedding type to be overridden if two modifiers are passed in', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const res = md.render(`This is the root node.
|
||||
|
||||
content-card![[note-e#Section 2]]`);
|
||||
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('This is the second section of note E');
|
||||
expect(res).not.toContain('Section 2');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWikilinkEmbed(
|
||||
MarkdownIt(),
|
||||
@@ -150,6 +364,27 @@ This is the third section of note E
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the bare text for an embedded note that is embedding a note that is not found', async () => {
|
||||
const note = await createFile(
|
||||
'This is the text of note A which includes ![[does-not-exist]]',
|
||||
['note.md']
|
||||
);
|
||||
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
expect(md.render(`This is the root node. ![[note]]`)).toMatch(
|
||||
`<p>This is the root node. <p>This is the text of note A which includes ![[does-not-exist]]</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should display a warning in case of cyclical inclusions', async () => {
|
||||
const noteA = await createFile(
|
||||
'This is the text of note A which includes ![[note-b]]',
|
||||
@@ -162,14 +397,21 @@ This is the third section of note E
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(noteA.uri, noteA.content))
|
||||
.set(parser.parse(noteB.uri, noteB.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink');
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
deleteFile(noteA);
|
||||
deleteFile(noteB);
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(noteA);
|
||||
await deleteFile(noteB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
WIKILINK_EMBED_REGEX,
|
||||
WIKILINK_EMBED_REGEX_GROUPS,
|
||||
retrieveNoteConfig,
|
||||
} from './wikilink-embed';
|
||||
import * as config from '../../services/config';
|
||||
|
||||
describe('Wikilink Note Embedding', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Wikilink Parsing', () => {
|
||||
it('should match a wikilink item including a modifier and wikilink', () => {
|
||||
// no configuration
|
||||
expect('![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
|
||||
// one of the configurations
|
||||
expect('full![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
|
||||
// any combination of configurations
|
||||
expect('full-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('full-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
});
|
||||
|
||||
it('should only match the wikilink if there are unrecognized keywords', () => {
|
||||
const match1 = 'random-word![[note-a]]'.match(WIKILINK_EMBED_REGEX);
|
||||
expect(match1[0]).toEqual('![[note-a]]');
|
||||
expect(match1[1]).toEqual('![[note-a]]');
|
||||
|
||||
const match2 = 'foo![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX);
|
||||
expect(match2[0]).toEqual('![[note-a#section 1]]');
|
||||
expect(match2[1]).toEqual('![[note-a#section 1]]');
|
||||
});
|
||||
|
||||
it('should group the wikilink into modifier and wikilink', () => {
|
||||
const match1 = 'content![[note-a]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
|
||||
expect(match1[0]).toEqual('content![[note-a]]');
|
||||
expect(match1[1]).toEqual('content');
|
||||
expect(match1[2]).toEqual('note-a');
|
||||
|
||||
const match2 = 'full-inline![[note-a#section 1]]'.match(
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
expect(match2[0]).toEqual('full-inline![[note-a#section 1]]');
|
||||
expect(match2[1]).toEqual('full-inline');
|
||||
expect(match2[2]).toEqual('note-a#section 1');
|
||||
|
||||
const match3 = '![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
|
||||
expect(match3[0]).toEqual('![[note-a#section 1]]');
|
||||
expect(match3[1]).toEqual(undefined);
|
||||
expect(match3[2]).toEqual('note-a#section 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Parsing', () => {
|
||||
it('should use preview.embedNoteType if an explicit modifier is not passed in', () => {
|
||||
jest
|
||||
.spyOn(config, 'getFoamVsCodeConfig')
|
||||
.mockReturnValueOnce('full-card');
|
||||
|
||||
const { noteScope, noteStyle } = retrieveNoteConfig(undefined);
|
||||
expect(noteScope).toEqual('full');
|
||||
expect(noteStyle).toEqual('card');
|
||||
});
|
||||
|
||||
it('should use explicit modifier over user settings if passed in', () => {
|
||||
jest
|
||||
.spyOn(config, 'getFoamVsCodeConfig')
|
||||
.mockReturnValueOnce('full-inline')
|
||||
.mockReturnValueOnce('full-inline')
|
||||
.mockReturnValueOnce('full-inline');
|
||||
|
||||
let { noteScope, noteStyle } = retrieveNoteConfig('content-card');
|
||||
expect(noteScope).toEqual('content');
|
||||
expect(noteStyle).toEqual('card');
|
||||
|
||||
({ noteScope, noteStyle } = retrieveNoteConfig('content'));
|
||||
expect(noteScope).toEqual('content');
|
||||
expect(noteStyle).toEqual('inline');
|
||||
|
||||
({ noteScope, noteStyle } = retrieveNoteConfig('card'));
|
||||
expect(noteScope).toEqual('full');
|
||||
expect(noteStyle).toEqual('card');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { workspace as vsWorkspace } from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isSome } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { Resource, ResourceParser } from '../../core/model/note';
|
||||
@@ -13,8 +12,16 @@ import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Position } from '../../core/model/position';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import { isNone, isSome } from '../../core/utils';
|
||||
|
||||
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
// we need another regex because md.use(regex, replace) only permits capturing one group
|
||||
// so we capture the entire possible wikilink item (ex. content-card![[note]]) using WIKILINK_EMBED_REGEX and then
|
||||
// use WIKILINK_EMBED_REGEX_GROUPER to parse it into the modifier(content-card) and the wikilink(note)
|
||||
export const WIKILINK_EMBED_REGEX_GROUPS =
|
||||
/((?:\w+)|(?:(?:\w+)-(?:\w+)))?!\[\[([^[\]]+?)\]\]/;
|
||||
export const CONFIG_EMBED_NOTE_TYPE = 'preview.embedNoteType';
|
||||
const refsStack: string[] = [];
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
@@ -24,9 +31,13 @@ export const markdownItWikilinkEmbed = (
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
regex: /!\[\[([^[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
try {
|
||||
const [, noteEmbedModifier, wikilink] = wikilinkItem.match(
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
|
||||
const includedNote = workspace.find(wikilink);
|
||||
|
||||
if (!includedNote) {
|
||||
@@ -45,27 +56,29 @@ export const markdownItWikilinkEmbed = (
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
}
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
let html: string;
|
||||
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
let noteText = readFileSync(includedNote.uri.toFsPath()).toString();
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = noteText.split('\n');
|
||||
noteText = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(
|
||||
noteText,
|
||||
parser,
|
||||
workspace
|
||||
);
|
||||
content = getFoamVsCodeConfig(CONFIG_EMBED_NOTE_IN_CONTAINER)
|
||||
? `<div class="embed-container-note">${md.render(noteText)}</div>`
|
||||
: noteText;
|
||||
const { noteScope, noteStyle } =
|
||||
retrieveNoteConfig(noteEmbedModifier);
|
||||
|
||||
const extractor: EmbedNoteExtractor =
|
||||
noteScope === 'full'
|
||||
? fullExtractor
|
||||
: noteScope === 'content'
|
||||
? contentExtractor
|
||||
: fullExtractor;
|
||||
|
||||
const formatter: EmbedNoteFormatter =
|
||||
noteStyle === 'card'
|
||||
? cardFormatter
|
||||
: noteStyle === 'inline'
|
||||
? inlineFormatter
|
||||
: cardFormatter;
|
||||
|
||||
content = extractor(includedNote, parser, workspace);
|
||||
html = formatter(content, md);
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
@@ -74,19 +87,20 @@ export const markdownItWikilinkEmbed = (
|
||||
${md.renderInline('[[' + wikilink + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`})`
|
||||
)}</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
`Error while including ${wikilinkItem} into the current document of the Preview panel`,
|
||||
e
|
||||
);
|
||||
return '';
|
||||
@@ -108,6 +122,11 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
.map(link => {
|
||||
const info = MarkdownLink.analyzeLink(link);
|
||||
const resource = workspace.find(info.target);
|
||||
// embedded notes that aren't created are still collected
|
||||
// return null so it can be filtered in the next step
|
||||
if (isNone(resource)) {
|
||||
return null;
|
||||
}
|
||||
const pathFromRoot = vsWorkspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
);
|
||||
@@ -115,6 +134,7 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
target: pathFromRoot,
|
||||
});
|
||||
})
|
||||
.filter(linkEdits => !isNone(linkEdits))
|
||||
.sort((a, b) => Position.compareTo(b.range.start, a.range.start));
|
||||
const text = edits.reduce(
|
||||
(text, edit) => TextEdit.apply(text, edit),
|
||||
@@ -123,4 +143,91 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
return text;
|
||||
}
|
||||
|
||||
export function retrieveNoteConfig(explicitModifier: string | undefined): {
|
||||
noteScope: string;
|
||||
noteStyle: string;
|
||||
} {
|
||||
let config = getFoamVsCodeConfig<string>(CONFIG_EMBED_NOTE_TYPE); // ex. full-inline
|
||||
let [noteScope, noteStyle] = config.split('-');
|
||||
|
||||
// an explicit modifier will always override corresponding user setting
|
||||
if (explicitModifier !== undefined) {
|
||||
if (['full', 'content'].includes(explicitModifier)) {
|
||||
noteScope = explicitModifier;
|
||||
} else if (['card', 'inline'].includes(explicitModifier)) {
|
||||
noteStyle = explicitModifier;
|
||||
} else {
|
||||
[noteScope, noteStyle] = explicitModifier.split('-');
|
||||
}
|
||||
}
|
||||
return { noteScope, noteStyle };
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of function that gets the desired content of the note
|
||||
*/
|
||||
export type EmbedNoteExtractor = (
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => string;
|
||||
|
||||
function fullExtractor(
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
): string {
|
||||
let noteText = readFileSync(note.uri.toFsPath()).toString();
|
||||
const section = Resource.findSection(note, note.uri.fragment);
|
||||
if (isSome(section)) {
|
||||
const rows = noteText.split('\n');
|
||||
noteText = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
function contentExtractor(
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
): string {
|
||||
let noteText = readFileSync(note.uri.toFsPath()).toString();
|
||||
let section = Resource.findSection(note, note.uri.fragment);
|
||||
if (!note.uri.fragment) {
|
||||
// if there's no fragment(section), the wikilink is linking to the entire note,
|
||||
// in which case we need to remove the title. We could just use rows.shift()
|
||||
// but should the note start with blank lines, it will only remove the first blank line
|
||||
// leaving the title
|
||||
// A better way is to find where the actual title starts by assuming it's at section[0]
|
||||
// then we treat it as the same case as link to a section
|
||||
section = note.sections.length ? note.sections[0] : null;
|
||||
}
|
||||
let rows = noteText.split('\n');
|
||||
if (isSome(section)) {
|
||||
rows = rows.slice(section.range.start.line, section.range.end.line);
|
||||
}
|
||||
rows.shift();
|
||||
noteText = rows.join('\n');
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of function that renders note content with the desired style in html
|
||||
*/
|
||||
export type EmbedNoteFormatter = (content: string, md: markdownit) => string;
|
||||
|
||||
function cardFormatter(content: string, md: markdownit): string {
|
||||
return md.render(
|
||||
`<div class="embed-container-note">${md.render(content)}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function inlineFormatter(content: string, md: markdownit): string {
|
||||
return md.render(content);
|
||||
}
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
|
||||
@@ -26,9 +26,9 @@ describe('Link generation in preview', () => {
|
||||
markdownItRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, ws), MarkdownIt());
|
||||
|
||||
it('generates a link to a note', () => {
|
||||
it('generates a link to a note using the note title as link', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a></p>\n`
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>${noteA.title}</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,13 +48,22 @@ describe('Link generation in preview', () => {
|
||||
const note = `[[note-a]]
|
||||
[note-a]: <note-a.md> "Note A"`;
|
||||
expect(md.render(note)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a>\n[note-a]: <note-a.md> "Note A"</p>\n`
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>${noteA.title}</a>\n[note-a]: <note-a.md> "Note A"</p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a link to a section within the note', () => {
|
||||
expect(md.render(`[[#sec]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='sec' href='#sec' data-href='#sec'>#sec</a></p>\n`
|
||||
);
|
||||
expect(md.render(`[[#Section Name]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='Section Name' href='#section-name' data-href='#section-name'>#Section Name</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a link to a note with a specific section', () => {
|
||||
expect(md.render(`[[note-b#sec2]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='My second note#sec2' href='/path2/to/note-b.md#sec2' data-href='/path2/to/note-b.md#sec2'>note-b#sec2</a></p>\n`
|
||||
`<p><a class='foam-note-link' title='My second note#sec2' href='/path2/to/note-b.md#sec2' data-href='/path2/to/note-b.md#sec2'>${noteB.title}#sec2</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -66,7 +75,7 @@ describe('Link generation in preview', () => {
|
||||
|
||||
it('generates a link to a note if the note exists, but the section does not exist', () => {
|
||||
expect(md.render(`[[note-b#nonexistentsec]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='My second note#nonexistentsec' href='/path2/to/note-b.md#nonexistentsec' data-href='/path2/to/note-b.md#nonexistentsec'>note-b#nonexistentsec</a></p>\n`
|
||||
`<p><a class='foam-note-link' title='My second note#nonexistentsec' href='/path2/to/note-b.md#nonexistentsec' data-href='/path2/to/note-b.md#nonexistentsec'>${noteB.title}#nonexistentsec</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { toSlug } from '../../utils/slug';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export const markdownItWikilinkNavigation = (
|
||||
md: markdownit,
|
||||
@@ -26,18 +27,33 @@ export const markdownItWikilinkNavigation = (
|
||||
isEmbed: false,
|
||||
});
|
||||
const formattedSection = section ? `#${section}` : '';
|
||||
const linkSection = section ? `#${toSlug(section)}` : '';
|
||||
const label = isEmpty(alias) ? `${target}${formattedSection}` : alias;
|
||||
|
||||
// [[#section]] links
|
||||
if (target.length === 0) {
|
||||
// we don't have a good way to check if the section exists within the
|
||||
// open file, so we just create a regular link for it
|
||||
return getResourceLink(section, linkSection, label);
|
||||
}
|
||||
|
||||
const resource = workspace.find(target);
|
||||
if (isNone(resource)) {
|
||||
return getPlaceholderLink(label);
|
||||
}
|
||||
|
||||
const link = `${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
)}${formattedSection}`;
|
||||
const title = `${resource.title}${formattedSection}`;
|
||||
return `<a class='foam-note-link' title='${title}' href='/${link}' data-href='/${link}'>${label}</a>`;
|
||||
const resourceLabel = isEmpty(alias)
|
||||
? `${resource.title}${formattedSection}`
|
||||
: alias;
|
||||
const resourceLink = `/${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri),
|
||||
false
|
||||
)}`;
|
||||
return getResourceLink(
|
||||
`${resource.title}${formattedSection}`,
|
||||
`${resourceLink}${linkSection}`,
|
||||
resourceLabel
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
@@ -52,4 +68,7 @@ export const markdownItWikilinkNavigation = (
|
||||
const getPlaceholderLink = (content: string) =>
|
||||
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
|
||||
|
||||
const getResourceLink = (title: string, link: string, label: string) =>
|
||||
`<a class='foam-note-link' title='${title}' href='${link}' data-href='${link}'>${label}</a>`;
|
||||
|
||||
export default markdownItWikilinkNavigation;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine, mdDocSelector } from '../utils';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine } from '../core/utils/md';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
|
||||
// this regex is different from HASHTAG_REGEX in that it does not look for a
|
||||
// #+character. It uses a negative look-ahead for `# `
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Resource, ResourceParser } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { isNone } from '../utils';
|
||||
import {
|
||||
fromVsCodeUri,
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../utils/vsc-utils';
|
||||
import { isNone } from '../core/utils';
|
||||
|
||||
const AMBIGUOUS_IDENTIFIER_CODE = 'ambiguous-identifier';
|
||||
const UNKNOWN_SECTION_CODE = 'unknown-section';
|
||||
@@ -24,20 +24,21 @@ interface FoamCommand<T> {
|
||||
interface FindIdentifierCommandArgs {
|
||||
range: vscode.Range;
|
||||
target: vscode.Uri;
|
||||
defaultExtension: string;
|
||||
amongst: vscode.Uri[];
|
||||
}
|
||||
|
||||
const FIND_IDENTIFIER_COMMAND: FoamCommand<FindIdentifierCommandArgs> = {
|
||||
name: 'foam:compute-identifier',
|
||||
execute: async ({ target, amongst, range }) => {
|
||||
execute: async ({ target, amongst, range, defaultExtension }) => {
|
||||
if (vscode.window.activeTextEditor) {
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
target.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
|
||||
identifier = identifier.endsWith('.md')
|
||||
? identifier.slice(0, -3)
|
||||
identifier = identifier.endsWith(defaultExtension)
|
||||
? identifier.slice(0, defaultExtension.length * -1)
|
||||
: identifier;
|
||||
|
||||
await vscode.window.activeTextEditor.edit(builder => {
|
||||
@@ -97,7 +98,7 @@ export default async function activate(
|
||||
}),
|
||||
vscode.languages.registerCodeActionsProvider(
|
||||
'markdown',
|
||||
new IdentifierResolver(),
|
||||
new IdentifierResolver(foam.workspace.defaultExtension),
|
||||
{
|
||||
providedCodeActionKinds: IdentifierResolver.providedCodeActionKinds,
|
||||
}
|
||||
@@ -193,6 +194,8 @@ export class IdentifierResolver implements vscode.CodeActionProvider {
|
||||
vscode.CodeActionKind.QuickFix,
|
||||
];
|
||||
|
||||
constructor(private defaultExtension: string) {}
|
||||
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
@@ -207,7 +210,12 @@ export class IdentifierResolver implements vscode.CodeActionProvider {
|
||||
);
|
||||
for (const item of diagnostic.relatedInformation) {
|
||||
res.push(
|
||||
createFindIdentifierCommand(diagnostic, item.location.uri, uris)
|
||||
createFindIdentifierCommand(
|
||||
diagnostic,
|
||||
item.location.uri,
|
||||
this.defaultExtension,
|
||||
uris
|
||||
)
|
||||
);
|
||||
}
|
||||
return [...acc, ...res];
|
||||
@@ -232,12 +240,12 @@ const createReplaceSectionCommand = (
|
||||
section: string
|
||||
): vscode.CodeAction => {
|
||||
const action = new vscode.CodeAction(
|
||||
`Use ${section}`,
|
||||
`${section}`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
command: REPLACE_TEXT_COMMAND.name,
|
||||
title: `Use section ${section}`,
|
||||
title: `Use section "${section}"`,
|
||||
arguments: [
|
||||
{
|
||||
value: section,
|
||||
@@ -257,10 +265,11 @@ const createReplaceSectionCommand = (
|
||||
const createFindIdentifierCommand = (
|
||||
diagnostic: vscode.Diagnostic,
|
||||
target: vscode.Uri,
|
||||
defaultExtension: string,
|
||||
possibleTargets: vscode.Uri[]
|
||||
): vscode.CodeAction => {
|
||||
const action = new vscode.CodeAction(
|
||||
`Use ${vscode.workspace.asRelativePath(target)}`,
|
||||
`${vscode.workspace.asRelativePath(target)}`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
@@ -270,6 +279,7 @@ const createFindIdentifierCommand = (
|
||||
{
|
||||
target: target,
|
||||
amongst: possibleTargets,
|
||||
defaultExtension: defaultExtension,
|
||||
range: new vscode.Range(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character + 2,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
EndOfLine,
|
||||
FileType,
|
||||
@@ -8,15 +6,18 @@ import {
|
||||
Selection,
|
||||
SnippetString,
|
||||
TextDocument,
|
||||
TextEditor,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
WorkspaceEdit,
|
||||
MarkdownString,
|
||||
} from 'vscode';
|
||||
import { focusNote } from '../utils';
|
||||
import { getExcerpt, stripFrontMatter, stripImages } from '../core/utils/md';
|
||||
import { isSome } from '../core/utils/core';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { isSome } from '../core/utils';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import {
|
||||
AlwaysIncludeMatcher,
|
||||
FileListBasedMatcher,
|
||||
@@ -31,6 +32,36 @@ interface SelectionInfo {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MarkdownString of the note content
|
||||
* @param note A Foam Note
|
||||
*/
|
||||
export function getNoteTooltip(content: string): string {
|
||||
const strippedContent = stripFrontMatter(stripImages(content));
|
||||
return formatMarkdownTooltip(strippedContent) as any;
|
||||
}
|
||||
|
||||
export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
const LINES_LIMIT = 16;
|
||||
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
|
||||
const totalLines = content.split('\n').length;
|
||||
const diffLines = totalLines - lines;
|
||||
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
|
||||
const md = new MarkdownString(`${excerpt}${ellipsis}`);
|
||||
md.isTrusted = true;
|
||||
return md;
|
||||
}
|
||||
|
||||
export const mdDocSelector = [
|
||||
{ language: 'markdown', scheme: 'file' },
|
||||
{ language: 'markdown', scheme: 'vscode-vfs' },
|
||||
{ language: 'markdown', scheme: 'untitled' },
|
||||
];
|
||||
|
||||
export function isMdEditor(editor: TextEditor) {
|
||||
return editor && editor.document && editor.document.languageId === 'markdown';
|
||||
}
|
||||
|
||||
export function findSelectionContent(): SelectionInfo | undefined {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
@@ -51,6 +82,24 @@ export function findSelectionContent(): SelectionInfo | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
export async function focusNote(
|
||||
notePath: URI,
|
||||
moveCursorToEnd: boolean,
|
||||
viewColumn: ViewColumn = ViewColumn.Active
|
||||
) {
|
||||
const document = await workspace.openTextDocument(toVsCodeUri(notePath));
|
||||
const editor = await window.showTextDocument(document, viewColumn);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (moveCursorToEnd) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
|
||||
return { document, editor };
|
||||
}
|
||||
|
||||
export async function createDocAndFocus(
|
||||
text: SnippetString,
|
||||
filepath: URI,
|
||||
@@ -171,9 +220,9 @@ export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
let files: Uri[] = [];
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
const uris = await workspace.findFiles(
|
||||
new RelativePattern(folder.uri.path, '**/*'),
|
||||
new RelativePattern(folder.uri, '**/*'),
|
||||
new RelativePattern(
|
||||
folder.uri.path,
|
||||
folder.uri,
|
||||
`{${excludePatterns.get(folder.name).join(',')}}`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { window, commands, ExtensionContext } from 'vscode';
|
||||
import { workspace, window, commands, ExtensionContext } from 'vscode';
|
||||
import { IDisposable } from '../core/common/lifecycle';
|
||||
import { BaseLogger, ILogger, LogLevel } from '../core/utils/log';
|
||||
import { getFoamLoggerLevel } from '../settings';
|
||||
|
||||
function getFoamLoggerLevel(): LogLevel {
|
||||
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
|
||||
}
|
||||
|
||||
export interface VsCodeLogger extends ILogger, IDisposable {
|
||||
show();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
SnippetString,
|
||||
ViewColumn,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { focusNote, isNone } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { UserCancelledOperation } from './errors';
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
deleteFile,
|
||||
fileExists,
|
||||
findSelectionContent,
|
||||
focusNote,
|
||||
getCurrentEditorDirectory,
|
||||
readFile,
|
||||
replaceSelection,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
import { Resolver } from './variable-resolver';
|
||||
import dateFormat from 'dateformat';
|
||||
import { getFoamVsCodeConfig } from './config';
|
||||
import { isNone } from '../core/utils';
|
||||
|
||||
/**
|
||||
* The templates directory
|
||||
@@ -475,30 +475,7 @@ async function askUserForFilepathConfirmation(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Common chars that is better to avoid in file names.
|
||||
* Inspired by:
|
||||
* https://www.mtu.edu/umc/services/websites/writing/characters-avoid/
|
||||
* https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
|
||||
* Even if some might be allowed in Win or Linux, to keep things more compatible and less error prone
|
||||
* we don't allow them
|
||||
* Also see https://github.com/foambubble/foam/issues/1042
|
||||
*/
|
||||
const UNALLOWED_CHARS = '/\\#%&{}<>?*$!\'":@+`|=';
|
||||
|
||||
/**
|
||||
* Uses the title to generate a file path.
|
||||
* It sanitizes the title to remove special characters and spaces.
|
||||
*
|
||||
* @param resolver the resolver to use
|
||||
* @returns the string path of the new note
|
||||
*/
|
||||
export const getPathFromTitle = async (resolver: Resolver) => {
|
||||
let defaultName = await resolver.resolveFromName('FOAM_TITLE');
|
||||
UNALLOWED_CHARS.split('').forEach(char => {
|
||||
defaultName = defaultName.split(char).join('');
|
||||
});
|
||||
|
||||
const defaultFilepath = URI.file(`${defaultName}.md`);
|
||||
return defaultFilepath;
|
||||
const defaultName = await resolver.resolveFromName('FOAM_TITLE_SAFE');
|
||||
return URI.file(`${defaultName}.md`);
|
||||
};
|
||||
|
||||
@@ -102,6 +102,23 @@ describe('variable-resolver, variable resolution', () => {
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_TITLE_SAFE', async () => {
|
||||
const foamTitle = 'My/note#title';
|
||||
const variables = [
|
||||
new Variable('FOAM_TITLE'),
|
||||
new Variable('FOAM_TITLE_SAFE'),
|
||||
];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
expected.set('FOAM_TITLE_SAFE', 'My-note-title');
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', foamTitle);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_DATE_* properties with current day by default', async () => {
|
||||
const variables = [
|
||||
new Variable('FOAM_DATE_YEAR'),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
const knownFoamVariables = new Set([
|
||||
'FOAM_TITLE',
|
||||
'FOAM_TITLE_SAFE',
|
||||
'FOAM_SLUG',
|
||||
'FOAM_SELECTED_TEXT',
|
||||
'FOAM_DATE_YEAR',
|
||||
@@ -132,6 +133,9 @@ export class Resolver implements VariableResolver {
|
||||
case 'FOAM_TITLE':
|
||||
value = resolveFoamTitle();
|
||||
break;
|
||||
case 'FOAM_TITLE_SAFE':
|
||||
value = resolveFoamTitleSafe(this);
|
||||
break;
|
||||
case 'FOAM_SLUG':
|
||||
value = toSlug(await this.resolve(new Variable('FOAM_TITLE')));
|
||||
break;
|
||||
@@ -242,3 +246,29 @@ async function resolveFoamTitle() {
|
||||
function resolveFoamSelectedText() {
|
||||
return findSelectionContent()?.content ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Common chars that is better to avoid in file names.
|
||||
* Inspired by:
|
||||
* https://www.mtu.edu/umc/services/websites/writing/characters-avoid/
|
||||
* https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
|
||||
* Even if some might be allowed in Win or Linux, to keep things more compatible and less error prone
|
||||
* we don't allow them
|
||||
* Also see https://github.com/foambubble/foam/issues/1042
|
||||
*/
|
||||
const UNALLOWED_CHARS = '/\\#%&{}<>?*$!\'":@+`|=';
|
||||
|
||||
/**
|
||||
* Uses the title to generate a file path.
|
||||
* It sanitizes the title to remove special characters and spaces.
|
||||
*
|
||||
* @param resolver the resolver to use
|
||||
* @returns the string path of the new note
|
||||
*/
|
||||
export const resolveFoamTitleSafe = async (resolver: Resolver) => {
|
||||
let safeTitle = await resolver.resolveFromName('FOAM_TITLE');
|
||||
UNALLOWED_CHARS.split('').forEach(char => {
|
||||
safeTitle = safeTitle.split(char).join('-');
|
||||
});
|
||||
return safeTitle;
|
||||
};
|
||||
|
||||
32
packages/foam-vscode/src/settings.spec.ts
Normal file
32
packages/foam-vscode/src/settings.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getNotesExtensions } from './settings';
|
||||
import { withModifiedFoamConfiguration } from './test/test-utils-vscode';
|
||||
|
||||
describe('Default note settings', () => {
|
||||
it('should default to .md', async () => {
|
||||
const config = getNotesExtensions();
|
||||
expect(config.defaultExtension).toEqual('.md');
|
||||
expect(config.notesExtensions).toEqual(['.md']);
|
||||
});
|
||||
|
||||
it('should always include the default note extension in the list of notes extensions', async () => {
|
||||
withModifiedFoamConfiguration(
|
||||
'files.defaultNoteExtension',
|
||||
'mdxx',
|
||||
async () => {
|
||||
const { notesExtensions } = getNotesExtensions();
|
||||
expect(notesExtensions).toEqual(['.mdxx']);
|
||||
|
||||
withModifiedFoamConfiguration(
|
||||
'files.notesExtensions',
|
||||
'md markdown',
|
||||
async () => {
|
||||
const { notesExtensions } = getNotesExtensions();
|
||||
expect(notesExtensions).toEqual(
|
||||
expect.arrayContaining(['.mdxx', '.md', '.markdown'])
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,41 @@
|
||||
import { workspace, GlobPattern } from 'vscode';
|
||||
import { LogLevel } from './core/utils/log';
|
||||
import { uniq } from 'lodash';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
|
||||
export function getWikilinkDefinitionSetting():
|
||||
| 'withExtensions'
|
||||
| 'withoutExtensions'
|
||||
| 'off' {
|
||||
return workspace
|
||||
.getConfiguration('foam.edit')
|
||||
.get('linkReferenceDefinitions', 'withoutExtensions');
|
||||
/**
|
||||
* Gets the notes extensions and default extension from the config.
|
||||
*
|
||||
* @returns {notesExtensions: string[], defaultExtension: string}
|
||||
*/
|
||||
export function getNotesExtensions() {
|
||||
const notesExtensionsFromSetting = getFoamVsCodeConfig(
|
||||
'files.notesExtensions',
|
||||
''
|
||||
)
|
||||
.split(' ')
|
||||
.filter(ext => ext.trim() !== '')
|
||||
.map(ext => '.' + ext.trim());
|
||||
const defaultExtension =
|
||||
'.' +
|
||||
(getFoamVsCodeConfig('files.defaultNoteExtension', 'md') ?? 'md').trim();
|
||||
|
||||
// we make sure that the default extension is always included in the list of extensions
|
||||
const notesExtensions = uniq(
|
||||
notesExtensionsFromSetting.concat(defaultExtension)
|
||||
);
|
||||
|
||||
return { notesExtensions, defaultExtension };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment extensions from the config.
|
||||
*
|
||||
* @returns string[]
|
||||
*/
|
||||
export function getAttachmentsExtensions() {
|
||||
return getFoamVsCodeConfig('files.attachmentExtensions', '')
|
||||
.split(' ')
|
||||
.map(ext => '.' + ext.trim());
|
||||
}
|
||||
|
||||
/** Retrieve the list of file ignoring globs. */
|
||||
@@ -18,35 +46,3 @@ export function getIgnoredFilesSetting(): GlobPattern[] {
|
||||
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
|
||||
];
|
||||
}
|
||||
|
||||
/** Retrieves the maximum length for a Graph node title. */
|
||||
export function getTitleMaxLength(): number {
|
||||
return workspace.getConfiguration('foam.graph').get('titleMaxLength');
|
||||
}
|
||||
|
||||
/** Retrieve the graph's style object */
|
||||
export function getGraphStyle(): object {
|
||||
return workspace.getConfiguration('foam.graph').get('style');
|
||||
}
|
||||
|
||||
export function getFoamLoggerLevel(): LogLevel {
|
||||
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
|
||||
}
|
||||
|
||||
/** Retrieve the orphans configuration */
|
||||
export function getOrphansConfig(): GroupedResourcesConfig {
|
||||
const orphansConfig = workspace.getConfiguration('foam.orphans');
|
||||
const exclude: string[] = orphansConfig.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
const placeholderCfg = workspace.getConfiguration('foam.placeholders');
|
||||
const exclude: string[] = placeholderCfg.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export interface GroupedResourcesConfig {
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
@@ -34,12 +34,17 @@ async function main() {
|
||||
// Passed to --extensionTestsPath
|
||||
const extensionTestsPath = path.join(__dirname, 'suite');
|
||||
|
||||
const testWorkspace = path.join(
|
||||
extensionDevelopmentPath,
|
||||
'.test-workspace'
|
||||
);
|
||||
|
||||
// Download VS Code, unzip it and run the integration test
|
||||
await runTests({
|
||||
extensionDevelopmentPath,
|
||||
extensionTestsPath,
|
||||
launchArgs: [
|
||||
path.join(extensionDevelopmentPath, '.test-workspace'),
|
||||
testWorkspace,
|
||||
'--disable-extensions',
|
||||
'--disable-workspace-trust',
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user