Compare commits
69 Commits
v0.15.3
...
fix-link-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aff88827f | ||
|
|
4195797024 | ||
|
|
fa405f5f65 | ||
|
|
4fd573b9e4 | ||
|
|
f613e1b9e2 | ||
|
|
0ada7d8e2c | ||
|
|
8b39bcdf16 | ||
|
|
6073dc246d | ||
|
|
5b671d59a8 | ||
|
|
8abea48b5c | ||
|
|
2eeb2e156b | ||
|
|
dc76660a63 | ||
|
|
e8eeffa4ca | ||
|
|
7d4f5e1532 | ||
|
|
e7749cd52b | ||
|
|
c6a4eab744 | ||
|
|
c88bd6f2f0 | ||
|
|
304a803310 | ||
|
|
632c41ac5f | ||
|
|
ec636809d8 | ||
|
|
af43a31ae8 | ||
|
|
7235af70dd | ||
|
|
de84541692 | ||
|
|
84fab168ce | ||
|
|
4f116cfc88 | ||
|
|
fd71dbe557 | ||
|
|
df4bf5a5cb | ||
|
|
122db20695 | ||
|
|
3b40e26a83 | ||
|
|
bbe44ea21b | ||
|
|
59bb2eb38f | ||
|
|
97f87692b6 | ||
|
|
4f76a6b24a | ||
|
|
c822589733 | ||
|
|
b748629c68 | ||
|
|
b1aa182fac | ||
|
|
c7155d3956 | ||
|
|
91385fc937 | ||
|
|
9f42893d61 | ||
|
|
65497ba6d3 | ||
|
|
f5ad5245b4 | ||
|
|
d1a6412cb7 | ||
|
|
e03fcf5dfa | ||
|
|
f174aa7162 | ||
|
|
2d9e1f5903 | ||
|
|
cf5daa4d22 | ||
|
|
e9eb3032e8 | ||
|
|
a8a418824f | ||
|
|
dd06d0b805 | ||
|
|
11af331694 | ||
|
|
5da1012fab | ||
|
|
8015a35f39 | ||
|
|
587466a210 | ||
|
|
52bc1ba13d | ||
|
|
8f045a3ff4 | ||
|
|
b2be5a7311 | ||
|
|
87e2400070 | ||
|
|
78e946c177 | ||
|
|
80e46f7898 | ||
|
|
5f89a59b07 | ||
|
|
f921c095aa | ||
|
|
a51e0613ea | ||
|
|
9df71adb64 | ||
|
|
17c216736b | ||
|
|
66a8c3bd49 | ||
|
|
5f7b3b7c02 | ||
|
|
9ed0d6e18e | ||
|
|
0140748550 | ||
|
|
356dcc5579 |
@@ -788,6 +788,24 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "iam-yan",
|
||||
"name": "Yan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48427014?v=4",
|
||||
"profile": "https://github.com/iam-yan",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jimt",
|
||||
"name": "Jim Tittsler",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/180326?v=4",
|
||||
"profile": "https://WikiEducator.org/User:JimTittsler",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
1
.gitignore
vendored
@@ -9,3 +9,4 @@ dist
|
||||
docs/_site
|
||||
docs/.sass-cache
|
||||
docs/.jekyll-metadata
|
||||
.test-workspace
|
||||
|
||||
38
.vscode/launch.json
vendored
@@ -6,15 +6,20 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "Debug Jest Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["workspace", "foam-vscode", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"args": ["--runInBand"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/foam-vscode/.test-workspace",
|
||||
"--disable-extensions",
|
||||
"--disable-workspace-trust",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode",
|
||||
"--extensionTestsPath=${workspaceFolder}/packages/foam-vscode/out/test/suite"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"name": "Run VSCode Extension",
|
||||
@@ -24,8 +29,25 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/packages/foam-vscode/out/**/*.js"],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"cwd": "${workspaceFolder}/packages/foam-vscode",
|
||||
"runtimeExecutable": "yarn",
|
||||
"args": [
|
||||
"jest",
|
||||
"--runInBand",
|
||||
"--watchAll=false"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
6
.vscode/settings.json
vendored
@@ -24,9 +24,9 @@
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"jest.autoRun": "off",
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn jest",
|
||||
"gitdoc.enabled": false,
|
||||
"jest.autoEnable": false,
|
||||
"jest.runAllTestsFirst": false,
|
||||
"search.mode": "reuseEditor"
|
||||
}
|
||||
|
||||
BIN
assets/screenshots/feature-backlinks-panel.gif
Normal file
|
After Width: | Height: | Size: 821 KiB |
BIN
assets/screenshots/feature-daily-note.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/screenshots/feature-definition-references.gif
Normal file
|
After Width: | Height: | Size: 859 KiB |
BIN
assets/screenshots/feature-definitions-generation.gif
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
assets/screenshots/feature-link-autocompletion.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/screenshots/feature-navigation.gif
Normal file
|
After Width: | Height: | Size: 935 KiB |
BIN
assets/screenshots/feature-note-embed.gif
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
assets/screenshots/feature-placeholder-orphan-panel.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/screenshots/feature-preview-navigation.gif
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
assets/screenshots/feature-show-graph.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/screenshots/feature-syntax-highlight.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
assets/screenshots/feature-tags-panel.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/screenshots/feature-templates.gif
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/screenshots/feature-unique-wikilink-completion.gif
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
assets/screenshots/feature-wikilink-diagnostics.gif
Normal file
|
After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 593 KiB After Width: | Height: | Size: 593 KiB |
27
docs/dev/releasing-foam.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Releasing Foam
|
||||
|
||||
1. Get to the latest code
|
||||
- `git checkout master && git fetch && git rebase`
|
||||
2. Sanity checks
|
||||
- `yarn reset`
|
||||
- `yarn test`
|
||||
3. Update change log
|
||||
- `./packages/foam-vscode/CHANGELOG.md`
|
||||
- `git add *`
|
||||
- `git commit -m"Preparation for next release"`
|
||||
4. Update version
|
||||
- `$ cd packages/foam-vscode`
|
||||
- `foam-vscode$ yarn lerna version <version>` (where `version` is `patch/minor/major`)
|
||||
- `cd ../..`
|
||||
5. Package extension
|
||||
- `$ yarn vscode:package-extension`
|
||||
6. Publish extension
|
||||
- `$ yarn vscode:publish-extension`
|
||||
7. Update the release notes in GitHub
|
||||
- in GitHub, top right, click on "releases"
|
||||
- select "tags" in top left
|
||||
- select the tag that was just released, click "edit" and copy release information from changelog
|
||||
- publish (no need to attach artifacts)
|
||||
8. Annouce on Discord
|
||||
|
||||
Steps 1 to 6 should really be replaced by a GitHub action...
|
||||
@@ -218,6 +218,8 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
93
docs/proposals/wikilinks-in-foam.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Wikilinks in Foam
|
||||
|
||||
Foam supports standard wikilinks in the format `[[wikilink]]`.
|
||||
|
||||
Wikilinks can refer to any note or attachment in the repo: `[[note.md]]`, `[[doc.pdf]]`, `[[image.jpg]]`.
|
||||
|
||||
The usual wikilink syntax without extension refers to notes: `[[wikilink]]` and `[[wikilink.md]]` are equivalent.
|
||||
|
||||
The goal of wikilinks is to uniquely identify a file in a repo, no matter in which directory it lives.
|
||||
|
||||
Sometimes in a repo you can have files with the same name in different directories.
|
||||
Foam allows you to identify those files using the minimum effort needed to disambiguate them.
|
||||
|
||||
This is achieved by adding as many directories above the file needed to uniquely identify the link, e.g. `[[house/todo]]`.
|
||||
|
||||
See below for more details.
|
||||
|
||||
## Goals for wikilinks in Foam
|
||||
|
||||
Wikilinks in Foam are meant to satisfy the following:
|
||||
- make it easy for users to identify a resource
|
||||
- make it interoperable with other similar note taking systems (Obsidian, Dendron, ...)
|
||||
- be easy to get started with, but satisfy growing needs
|
||||
|
||||
## Types of wikilinks supported in Foam
|
||||
|
||||
Foam supports two types of keys inside a wikilink: a **path** reference and an **identifier** reference:
|
||||
|
||||
- `[[./file]]` and `[[../to/another/file]]` are **path** links to a resource, relative _from the source_
|
||||
- `[[/path/to/file]]` is a **path** link to a resource, relative _from the repo root_
|
||||
- `[[file]]` is an **identifier** of a resource (based on the filename)
|
||||
- `[[path/to/file]]` is an **identifier** of a resource (based on the path), the same is true for `[[to/file]]`
|
||||
|
||||
It's important to note that sometimes identifier keys can't uniquely locale a resource.
|
||||
|
||||
A more concrete example will help:
|
||||
|
||||
```
|
||||
/
|
||||
projects/
|
||||
house/
|
||||
todo.md
|
||||
buy-car/
|
||||
todo.md
|
||||
cars.md
|
||||
work/
|
||||
todo.md
|
||||
notes.md
|
||||
```
|
||||
|
||||
In the above repo:
|
||||
|
||||
- `[[cars]]` is a unique identifier of a resource - it can be used anywhere in the repo
|
||||
- `[[todo]]` is an non-unique identifier as it can refer to multiple resources
|
||||
- `[[house/todo]]` is a unique identifier of a resource - it can be used anywhere in the repo
|
||||
- `[[projects/house/todo]]` is a unique identifier of a resource - it can be used anywhere in the repo
|
||||
- `[[/projects/house/todo]]` is a path reference to a resource
|
||||
- `[[./todo]]` is a path reference to a resource (e.g. from `/projects/buy-car/cars.md`)
|
||||
|
||||
Basically we could say as a rule:
|
||||
|
||||
- if the link starts with `/` or `.` we consider it a **path** reference, in the first case from the repo root, otherwise from the source note
|
||||
- if a link doesn't start with `/` or `.` it is an **identifier**
|
||||
- generally speaking we use the shortest identifier available to identify a resource, **but all are valid**
|
||||
- `[[projects/buy-car/cars]]`, `[[buy-car/cars]]`, `[[cars]]` are all unique identifier to the same resource, and are all valid in a document
|
||||
- the same can be said for `[[projects/house/todo]]` and `[[house/todo]]` - but not for `[[todo]]`, because it can refer to more than one resource
|
||||
|
||||
## Compatibility with other apps
|
||||
|
||||
| Scenario | Obsidian | Foam |
|
||||
| --------------------------- | ------------------------------- | ------------------------------- |
|
||||
| 1 `[[notes]]` | ✔ unique identifier in repo | ✔ unique identifier in repo |
|
||||
| 2 `[[/work/notes]]` | ✔ valid path from repo root | ✔ valid path from repo root |
|
||||
| 3 `[[work/notes]]` | ✔ valid path from repo root | ✔ valid identifier in repo |
|
||||
| 4 `[[project/house/todo]]` | ✔ valid path from repo root | ✔ valid unique identifier |
|
||||
| 5 `[[/project/house/todo]]` | ✔ valid path from repo root | ✔ valid path from repo root |
|
||||
| 6 `[[house/todo]]` | ✔ valid unique identifier | ✔ valid unique identifier |
|
||||
| 7 `[[todo]]` | ✘ ambiguous identifier | ✘ ambiguous identifier |
|
||||
| 8 `[[/house/todo]]` | ✘ incorrect path from repo root | ✘ incorrect path from repo root |
|
||||
|
||||
## Non-unique identifiers
|
||||
|
||||
We can't prevent non-unique identifiers from occurring in Foam (first and foremost because a file could be edited with another editor) but we can flag them.
|
||||
|
||||
Therefore Foam follows the following strategy instead:
|
||||
|
||||
1. there is a clear resolution mechanism (alphabetic) so that if nothing changes a non-unique identifier will always return the same note. Resolution has to be deterministic
|
||||
2. a diagnostic entry (warning or error) is showed to the user for non-unique identifiers, so she knows that she's using a "risky" identifier
|
||||
1. The quick resolution for this item will show the available unique identifiers matching the non-unique one
|
||||
|
||||
## Thanks
|
||||
|
||||
Thanks to [@memplex](https://github.com/memeplex) for helping with the thinking around this proposal.
|
||||
@@ -10,6 +10,10 @@ Foam enables you to Link pages together using `[[file-name]]` annotations (i.e.
|
||||
|
||||
> If the `F12` shortcut feels unnatural you can rebind it at File > Preferences > Keyboard Shortcuts by searching for `editor.action.revealDefinition`.
|
||||
|
||||
## Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
|
||||
## Markdown compatibility
|
||||
|
||||
The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) extension automatically generates [[link-reference-definitions]] at the bottom of the file to make wikilinks compatible with Markdown tools and parsers.
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.15.3"
|
||||
"version": "0.17.2"
|
||||
}
|
||||
|
||||
0
packages/foam-vscode/.test-workspace/.keep
Normal file
@@ -4,6 +4,98 @@ 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.17.2] - 2021-12-22
|
||||
|
||||
Fixes and Improvements:
|
||||
- Improved support for wikilinks in titles (#878)
|
||||
- Use syntax injection for wikilinks (#876 - thanks @memeplex)
|
||||
- Fix when applying text edits in last line
|
||||
|
||||
Internal:
|
||||
- DX: Clean up of testing setup (#881 - thanks @memeplex)
|
||||
|
||||
## [0.17.1] - 2021-12-16
|
||||
|
||||
Fixes and Improvements:
|
||||
- Decorate markdown files only (#857)
|
||||
- Fix template placeholders issue (#859)
|
||||
- Improved replacement range for link completion
|
||||
|
||||
Internal:
|
||||
- Major URI/path handling refactoring (#858 - thanks @memeplex)
|
||||
|
||||
## [0.17.0] - 2021-12-08
|
||||
|
||||
Features:
|
||||
|
||||
- Added first class support for sections (#856)
|
||||
- Sections can be referred to in wikilinks
|
||||
- Sections can be embedded
|
||||
- Autocompletion for sections
|
||||
- Diagnostic for sections
|
||||
- Embed sections
|
||||
|
||||
## [0.16.1] - 2021-11-30
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed diagnostic bug triggered when file had same suffix (#851)
|
||||
|
||||
## [0.16.0] - 2021-11-24
|
||||
|
||||
Features:
|
||||
|
||||
- Added support for unique wikilink identifiers (#841)
|
||||
- This change allows files that have the same name to be uniquely referenced as wikilinks
|
||||
- BREAKING CHANGE: wikilinks to attachments must now include the extension
|
||||
- Added diagnostics for ambiguous wikilinks, with quick fixes available (#844)
|
||||
- Added support for unique wikilinks in autocompletion (#845)
|
||||
|
||||
## [0.15.9] - 2021-11-23
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed filepath retrieval when creating note from template (#843)
|
||||
|
||||
## [0.15.8] - 2021-11-22
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Re-enable link navigation for wikilinks (#840)
|
||||
|
||||
## [0.15.7] - 2021-11-21
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed template listing (#831)
|
||||
- Fixed note creation from template (#834)
|
||||
|
||||
## [0.15.6] - 2021-11-18
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Link Reference Generation is now OFF by default
|
||||
- Fixed preview navigation (#830)
|
||||
|
||||
|
||||
## [0.15.5] - 2021-11-15
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Major improvement in navigation. Use link definitions and link references (#821)
|
||||
- Fixed bug showing in hover reference the same more than once when it had multiple links to another (#822)
|
||||
|
||||
Internal:
|
||||
|
||||
- Foam URI refactoring (#820)
|
||||
- Template service refactoring (#825)
|
||||
|
||||
## [0.15.4] - 2021-11-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Detached Foam URI from VS Code URI. This should improve several path related issues in Windows. Given how core this change is, the release is just about this refactoring to easily detect possible side effects.
|
||||
|
||||
## [0.15.3] - 2021-11-08
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -5,15 +5,127 @@
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
|
||||
> ⚠️ This is an early stage software. Use at your own peril.
|
||||
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
|
||||
|
||||
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VsCode, which means you can pair it with your favorite extensions for a great editing experience.
|
||||
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VS Code, which means you can pair it with your favorite extensions for a great editing experience.
|
||||
|
||||
Foam is open source, and allows you to create a local first, markdown based, personal knowledge base. You can also use it to publish your notes.
|
||||
|
||||
Foam is also meant to be extensible, so you can integrate with its internals to customize your knowledge base.
|
||||
|
||||
## Features
|
||||
|
||||
### Graph Visualization
|
||||
|
||||
See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command.
|
||||
|
||||

|
||||
|
||||
### Link Autocompletion
|
||||
|
||||
Foam helps you create the connections between your notes, and your placeholders as well.
|
||||
|
||||

|
||||
|
||||
### Unique identifiers across directories
|
||||
|
||||
Foam supports files with the same name in multiple directories.
|
||||
It will use the minimum identifier required, and even report and help you fix existing ambiguous wikilinks.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Link Preview and Navigation
|
||||
|
||||

|
||||
|
||||
### Go to definition, Peek References
|
||||
|
||||
See where a note is being referenced in your knowledge base.
|
||||
|
||||

|
||||
|
||||
### Navigation in Preview
|
||||
|
||||
Navigate your rendered notes in the VS Code preview panel.
|
||||
|
||||

|
||||
|
||||
### Note embed
|
||||
|
||||
Embed the content from other notes.
|
||||
|
||||

|
||||
|
||||
### Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections.
|
||||
Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
|
||||
### Link Alias
|
||||
|
||||
Foam supports link aliasing, so you can have a `[[wikilink]]`, or a `[[wikilink|alias]]`.
|
||||
|
||||
### Templates
|
||||
|
||||
Use [custom templates](https://foambubble.github.io/foam/features/note-templates) to have avoid repetitve work on your notes.
|
||||
|
||||

|
||||
|
||||
### Backlinks Panel
|
||||
|
||||
Quickly check which notes are referencing the currently active note.
|
||||
See for each occurrence the context in which it lives, as well as a preview of the note.
|
||||
|
||||

|
||||
|
||||
### Tag Explorer Panel
|
||||
|
||||
Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags).
|
||||
Foam also supports hierarchical tags.
|
||||
|
||||

|
||||
|
||||
### Orphans and Placeholder Panels
|
||||
|
||||
Orphans are note that have no inbound nor outbound links.
|
||||
Placeholders are dangling links, or notes without content.
|
||||
Keep them under control, and your knowledge base in better state, by using this panel.
|
||||
|
||||

|
||||
|
||||
### Syntax highlight
|
||||
|
||||
Foam highlights wikilinks and placeholder differently, to help you visualize your knowledge base.
|
||||
|
||||

|
||||
|
||||
### Daily note
|
||||
|
||||
Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes).
|
||||
|
||||

|
||||
|
||||
### Generate references for your wikilinks
|
||||
|
||||
Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wikilinks]]`, to use your notes in a non-Foam workspace.
|
||||
With references you can also make your notes navigable both in GitHub UI as well as GitHub Pages.
|
||||
|
||||

|
||||
|
||||
### Commands
|
||||
|
||||
- Explore your knowledge base with the `Foam: Open Random Note` command
|
||||
- Access your daily note with the `Foam: Open Daily Note` command
|
||||
- Create a new note with the `Foam: Create New Note` command
|
||||
- This becomes very powerful when combined with [note templates](https://foambubble.github.io/foam/features/note-templates) and the `Foam: Create New Note from Template` command
|
||||
- See your workspace as a connected graph with the `Foam: Show Graph` command
|
||||
|
||||
## Recipes
|
||||
|
||||
People use Foam in different ways for different use cases, check out the [recipes](https://foambubble.github.io/foam/recipes/recipes) page for inspiration!
|
||||
|
||||
## Getting started
|
||||
|
||||
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
|
||||
@@ -22,24 +134,13 @@ You really, _really_, **really** should read [Foam documentation](https://foambu
|
||||
2. Clone the repository and open it in VS Code.
|
||||
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).
|
||||
|
||||
This will also install `Foam for VSCode`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
|
||||
|
||||
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
|
||||
|
||||
## Features
|
||||
|
||||
- Connect your notes using [`[[wikilinks]]`](https://foambubble.github.io/foam/features/backlinking)
|
||||
- Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wikilinks]]`, to use your notes in a non-foam workspace
|
||||
- See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command
|
||||
- Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags)
|
||||
- Make your notes navigable both in GitHub UI as well as GitHub Pages
|
||||
- Use [custom templates](https://foambubble.github.io/foam/features/note-templates) for your notes
|
||||
- Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes)
|
||||
- Explore your knowledge base with the `Foam: Open Random Note` command
|
||||
This will also install `Foam`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
|
||||
|
||||
## Requirements
|
||||
|
||||
High tolerance for alpha-grade software.
|
||||
Foam is still a Work in Progress.
|
||||
Rest assured it will never lock you in, nor compromise your files, but sometimes some features might break ;)
|
||||
|
||||
## Known Issues
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 604 KiB After Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 821 KiB |
BIN
packages/foam-vscode/assets/screenshots/feature-daily-note.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 621 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
BIN
packages/foam-vscode/assets/screenshots/feature-navigation.gif
Normal file
|
After Width: | Height: | Size: 935 KiB |
BIN
packages/foam-vscode/assets/screenshots/feature-note-embed.gif
Normal file
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 369 KiB |
BIN
packages/foam-vscode/assets/screenshots/feature-show-graph.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 394 KiB |
BIN
packages/foam-vscode/assets/screenshots/feature-tags-panel.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
packages/foam-vscode/assets/screenshots/feature-templates.gif
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 593 KiB |
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
plugins: [["@babel/plugin-transform-runtime", { helpers: false }]]
|
||||
};
|
||||
@@ -82,7 +82,7 @@ module.exports = {
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
modulePathIgnorePatterns: ['.vscode-test'],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
@@ -91,7 +91,7 @@ module.exports = {
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
preset: 'ts-jest',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
@@ -126,13 +126,13 @@ module.exports = {
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node"
|
||||
testEnvironment: 'node',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
@@ -152,7 +152,10 @@ module.exports = {
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
// This is overridden in every runCLI invocation but it's here as the default
|
||||
// for vscode-jest. We only want unit tests in the test explorer (sidebar),
|
||||
// since spec tests require the entire extension host to be launched before.
|
||||
testRegex: ['\\.test\\.ts$'],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "foam-vscode",
|
||||
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
|
||||
"description": "Generate markdown reference lists from wikilinks in a workspace",
|
||||
"displayName": "Foam",
|
||||
"description": "VS Code + Markdown + Wikilinks for your note taking and knowledge base",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"url": "https://github.com/foambubble/foam",
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.15.3",
|
||||
"version": "0.17.2",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
"vscode": "^1.47.1"
|
||||
},
|
||||
"icon": "icon/FOAM_ICON_256.png",
|
||||
"icon": "assets/icon/FOAM_ICON_256.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
@@ -37,6 +37,26 @@
|
||||
"markdown.previewStyles": [
|
||||
"./static/preview/style.css"
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"path": "./syntaxes/injection.json",
|
||||
"scopeName": "foam.wikilink.injection",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colors": [
|
||||
{
|
||||
"id": "foam.placeholder",
|
||||
"description": "Color of foam placeholders.",
|
||||
"defaults": {
|
||||
"dark": "editorWarning.foreground",
|
||||
"light": "editorWarning.foreground",
|
||||
"highContrast": "editorWarning.foreground"
|
||||
}
|
||||
}
|
||||
],
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
@@ -223,7 +243,7 @@
|
||||
},
|
||||
"foam.edit.linkReferenceDefinitions": {
|
||||
"type": "string",
|
||||
"default": "withoutExtensions",
|
||||
"default": "off",
|
||||
"enum": [
|
||||
"withExtensions",
|
||||
"withoutExtensions",
|
||||
@@ -235,11 +255,6 @@
|
||||
"Disable wikilink definitions generation"
|
||||
]
|
||||
},
|
||||
"foam.links.navigation.enable": {
|
||||
"description": "Enable navigation through links",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"foam.links.hover.enable": {
|
||||
"description": "Enable displaying note content on hover links",
|
||||
"type": "boolean",
|
||||
@@ -361,7 +376,9 @@
|
||||
"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",
|
||||
"lint": "tsdx lint src",
|
||||
"clean": "rimraf out",
|
||||
@@ -382,7 +399,6 @@
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
@@ -393,12 +409,10 @@
|
||||
"@types/vscode": "^1.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"babel-jest": "^26.2.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.2.2",
|
||||
"jest-environment-vscode": "^1.0.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"markdown-it": "^12.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -412,7 +426,6 @@
|
||||
"dateformat": "^3.0.3",
|
||||
"detect-newline": "^3.1.0",
|
||||
"fast-array-diff": "^1.0.1",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { createConfigFromFolders } from './config';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { TEST_DATA_DIR } from '../test/test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = URI.joinPath(TEST_DATA_DIR, 'test-config');
|
||||
|
||||
describe('Foam configuration', () => {
|
||||
it('can read settings from config.json', () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(testFolder, 'folder1'),
|
||||
]);
|
||||
expect(config.get('feature1.setting1.value')).toBeTruthy();
|
||||
expect(config.get('feature2.value')).toEqual(12);
|
||||
|
||||
const section = config.get<{ value: boolean }>('feature1.setting1');
|
||||
expect(section!.value).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can merge settings from multiple foam folders', () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(testFolder, 'folder1'),
|
||||
URI.joinPath(testFolder, 'folder2'),
|
||||
]);
|
||||
|
||||
// override value
|
||||
expect(config.get('feature1.setting1.value')).toBe(false);
|
||||
// this was not overridden
|
||||
expect(config.get('feature1.setting1.extraValue')).toEqual('go foam');
|
||||
// new value from second config file
|
||||
expect(config.get('feature1.setting1.value2')).toBe('hello');
|
||||
|
||||
// this whole section doesn't exist in second file
|
||||
expect(config.get('feature2.value')).toEqual(12);
|
||||
});
|
||||
|
||||
it('cannot activate local plugins from workspace config', () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(testFolder, 'enable-plugins'),
|
||||
]);
|
||||
expect(config.get('experimental.localPlugins.enabled')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export interface FoamConfig {
|
||||
workspaceFolders: URI[];
|
||||
includeGlobs: string[];
|
||||
ignoreGlobs: string[];
|
||||
get<T>(path: string): T | undefined;
|
||||
get<T>(path: string, defaultValue: T): T;
|
||||
}
|
||||
|
||||
const DEFAULT_INCLUDES = ['**/*'];
|
||||
|
||||
const DEFAULT_IGNORES = ['**/node_modules/**'];
|
||||
|
||||
export const createConfigFromObject = (
|
||||
workspaceFolders: URI[],
|
||||
include: string[],
|
||||
ignore: string[],
|
||||
settings: any
|
||||
) => {
|
||||
const config: FoamConfig = {
|
||||
workspaceFolders: workspaceFolders,
|
||||
includeGlobs: include,
|
||||
ignoreGlobs: ignore,
|
||||
get: <T>(path: string, defaultValue?: T) => {
|
||||
const tokens = path.split('.');
|
||||
const value = tokens.reduce((acc, t) => acc?.[t], settings);
|
||||
return value ?? defaultValue;
|
||||
},
|
||||
};
|
||||
return config;
|
||||
};
|
||||
|
||||
export const createConfigFromFolders = (
|
||||
workspaceFolders: URI[] | URI,
|
||||
options: {
|
||||
include?: string[];
|
||||
ignore?: string[];
|
||||
} = {}
|
||||
): FoamConfig => {
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [workspaceFolders];
|
||||
}
|
||||
const workspaceConfig: any = workspaceFolders.reduce(
|
||||
(acc, f) => merge(acc, parseConfig(URI.joinPath(f, 'config.json'))),
|
||||
{}
|
||||
);
|
||||
// For security reasons local plugins can only be
|
||||
// activated via user config
|
||||
if ('experimental' in workspaceConfig) {
|
||||
delete workspaceConfig['experimental']['localPlugins'];
|
||||
}
|
||||
|
||||
const userConfig = parseConfig(URI.file(`~/.foam/config.json`));
|
||||
|
||||
const settings = merge(workspaceConfig, userConfig);
|
||||
|
||||
return createConfigFromObject(
|
||||
workspaceFolders,
|
||||
options.include ?? DEFAULT_INCLUDES,
|
||||
options.ignore ?? DEFAULT_IGNORES,
|
||||
settings
|
||||
);
|
||||
};
|
||||
|
||||
const parseConfig = (path: URI) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(URI.toFsPath(path), 'utf8'));
|
||||
} catch {
|
||||
Logger.debug('Could not read configuration from ' + URI.toString(path));
|
||||
}
|
||||
};
|
||||
@@ -34,5 +34,5 @@ const getOffset = (
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i].length);
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { generateHeading } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { createConfigFromFolders } from '../config';
|
||||
import { MarkdownResourceProvider } from '../markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
@@ -17,21 +15,13 @@ describe('generateHeadings', () => {
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(TEST_DATA_DIR, '__scaffold__'),
|
||||
]);
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { generateLinkReferences } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { createConfigFromFolders } from '../config';
|
||||
import { MarkdownResourceProvider } from '../markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
@@ -14,24 +12,17 @@ Logger.setLevel('error');
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
// TODO slug must be reserved for actual slugs, not file names
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(TEST_DATA_DIR, '__scaffold__'),
|
||||
]);
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
@@ -7,13 +6,10 @@ import {
|
||||
} from '../markdown-provider';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
@@ -168,7 +164,7 @@ export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
|
||||
return {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
uriToSlug(note.uri)
|
||||
note.uri.getName()
|
||||
)}${paddingEnd}`,
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
@@ -176,14 +172,3 @@ export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileName
|
||||
* @returns null if file name is already in kebab case otherise returns
|
||||
* the kebab cased file name
|
||||
*/
|
||||
export const getKebabCaseFileName = (fileName: string) => {
|
||||
const kebabCasedFileName = slugger.slug(fileName);
|
||||
return kebabCasedFileName === fileName ? null : kebabCasedFileName;
|
||||
};
|
||||
|
||||
@@ -5,11 +5,10 @@ import {
|
||||
} from './markdown-provider';
|
||||
import { DirectLink, WikiLink } from './model/note';
|
||||
import { Logger } from './utils/log';
|
||||
import { uriToSlug } from './utils/slug';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamGraph } from './model/graph';
|
||||
import { Range } from './model/range';
|
||||
import { createTestWorkspace } from '../test/test-utils';
|
||||
import { createTestWorkspace, getRandomURI } from '../test/test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -40,50 +39,45 @@ const pageE = `
|
||||
# Page E
|
||||
`;
|
||||
|
||||
const createNoteFromMarkdown = (path: string, content: string) =>
|
||||
createMarkdownParser([]).parse(URI.file(path), content);
|
||||
const createNoteFromMarkdown = (content: string, path?: string) =>
|
||||
createMarkdownParser([]).parse(
|
||||
path ? URI.file(path) : getRandomURI(),
|
||||
content
|
||||
);
|
||||
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
workspace.set(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
workspace.set(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
workspace.set(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
workspace.set(createNoteFromMarkdown('/page-d.md', pageD));
|
||||
workspace.set(createNoteFromMarkdown('/page-e.md', pageE));
|
||||
workspace.set(createNoteFromMarkdown(pageA, '/page-a.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageB, '/page-b.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageC, '/page-c.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageD, '/page-d.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageE, '/page-e.md'));
|
||||
|
||||
expect(
|
||||
workspace
|
||||
.list()
|
||||
.map(n => n.uri)
|
||||
.map(uriToSlug)
|
||||
.map(n => n.uri.getName())
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
|
||||
it('Ingores external links', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to google](https://www.google.com)
|
||||
`
|
||||
`this is a [link to google](https://www.google.com)`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Ignores references to sections in the same file', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to intro](#introduction)
|
||||
`
|
||||
`this is a [link to intro](#introduction)`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Parses internal links correctly', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is a [link to page b](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
@@ -95,7 +89,6 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses links that have formatting in label', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [**link** with __formatting__](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
@@ -107,11 +100,11 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/page-a.md', pageA);
|
||||
const noteB = createNoteFromMarkdown('/page-b.md', pageB);
|
||||
const noteC = createNoteFromMarkdown('/page-c.md', pageC);
|
||||
const noteD = createNoteFromMarkdown('/Page D.md', pageD);
|
||||
const noteE = createNoteFromMarkdown('/page e.md', pageE);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/page-a.md');
|
||||
const noteB = createNoteFromMarkdown(pageB, '/page-b.md');
|
||||
const noteC = createNoteFromMarkdown(pageC, '/page-c.md');
|
||||
const noteD = createNoteFromMarkdown(pageD, '/Page D.md');
|
||||
const noteE = createNoteFromMarkdown(pageE, '/page e.md');
|
||||
|
||||
workspace
|
||||
.set(noteA)
|
||||
@@ -134,7 +127,6 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses backlinks with an alias', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
|
||||
);
|
||||
expect(note.links.length).toEqual(2);
|
||||
@@ -151,9 +143,7 @@ this is a [link to intro](#introduction)
|
||||
});
|
||||
|
||||
it('Skips wikilinks in codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some text with our [[first-wikilink]].
|
||||
|
||||
\`\`\`
|
||||
@@ -161,8 +151,7 @@ this is inside a [[codeblock]]
|
||||
\`\`\`
|
||||
|
||||
this is some text with our [[second-wikilink]].
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.links.map(l => l.label)).toEqual([
|
||||
'first-wikilink',
|
||||
'second-wikilink',
|
||||
@@ -170,16 +159,13 @@ this is some text with our [[second-wikilink]].
|
||||
});
|
||||
|
||||
it('Skips wikilinks in inlined codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some text with our [[first-wikilink]].
|
||||
|
||||
this is \`inside a [[codeblock]]\`
|
||||
|
||||
this is some text with our [[second-wikilink]].
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.links.map(l => l.label)).toEqual([
|
||||
'first-wikilink',
|
||||
'second-wikilink',
|
||||
@@ -189,71 +175,71 @@ this is some text with our [[second-wikilink]].
|
||||
|
||||
describe('Note Title', () => {
|
||||
it('should initialize note title if heading exists', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-a.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Page A
|
||||
this note has a title
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(note.title).toBe('Page A');
|
||||
});
|
||||
|
||||
it('should support wikilinks and urls in title', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Page A with [[wikilink]] and a [url](https://google.com)
|
||||
this note has a title
|
||||
`);
|
||||
expect(note.title).toBe('Page A with wikilink and a url');
|
||||
});
|
||||
|
||||
it('should default to file name if heading does not exist', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-d.md',
|
||||
`
|
||||
This file has no heading.
|
||||
`
|
||||
`This file has no heading.`,
|
||||
'/page-d.md'
|
||||
);
|
||||
|
||||
expect(note.title).toEqual('page-d');
|
||||
});
|
||||
|
||||
it('should give precedence to frontmatter title over other headings', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.title).toBe('Note Title');
|
||||
});
|
||||
|
||||
it('should support numbers', () => {
|
||||
const note1 = createNoteFromMarkdown('/157.md', `hello`);
|
||||
const note1 = createNoteFromMarkdown(`hello`, '/157.md');
|
||||
expect(note1.title).toBe('157');
|
||||
|
||||
const note2 = createNoteFromMarkdown('/157.md', `# 158`);
|
||||
const note2 = createNoteFromMarkdown(`# 158`, '/157.md');
|
||||
expect(note2.title).toBe('158');
|
||||
|
||||
const note3 = createNoteFromMarkdown(
|
||||
'/157.md',
|
||||
`
|
||||
---
|
||||
title: 159
|
||||
---
|
||||
|
||||
# 158
|
||||
`
|
||||
`,
|
||||
'/157.md'
|
||||
);
|
||||
expect(note3.title).toBe('159');
|
||||
});
|
||||
|
||||
it('should not break on empty titles (see #276)', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/Hello Page.md',
|
||||
`
|
||||
#
|
||||
|
||||
this note has an empty title line
|
||||
`
|
||||
`,
|
||||
'/Hello Page.md'
|
||||
);
|
||||
expect(note.title).toEqual('Hello Page');
|
||||
});
|
||||
@@ -261,47 +247,38 @@ this note has an empty title line
|
||||
|
||||
describe('frontmatter', () => {
|
||||
it('should parse yaml frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title`
|
||||
);
|
||||
# Other Note Title`);
|
||||
|
||||
expect(note.properties.title).toBe('Note Title');
|
||||
expect(note.properties.date).toBe('20-12-12');
|
||||
});
|
||||
|
||||
it('should parse empty frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
---
|
||||
|
||||
# Empty Frontmatter
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
|
||||
it('should not fail when there are issues with parsing frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: - one
|
||||
- two
|
||||
- #
|
||||
---
|
||||
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
@@ -310,11 +287,11 @@ title: - one
|
||||
describe('wikilinks definitions', () => {
|
||||
it('can generate links without file extension when includeExtension = false', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
|
||||
|
||||
const noExtRefs = createMarkdownReferences(workspace, noteA.uri, false);
|
||||
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
|
||||
@@ -322,11 +299,11 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
it('can generate links with file extension when includeExtension = true', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
|
||||
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
|
||||
@@ -334,11 +311,11 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
it('use relative paths', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir2/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir3/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir3/page-c.md'));
|
||||
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual([
|
||||
@@ -350,13 +327,10 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
describe('tags plugin', () => {
|
||||
it('can find tags in the text of the note', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
# this is a #heading
|
||||
#this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags).toEqual([
|
||||
{ label: 'heading', range: Range.create(1, 12, 1, 20) },
|
||||
{ label: 'this', range: Range.create(2, 0, 2, 5) },
|
||||
@@ -367,16 +341,13 @@ describe('tags plugin', () => {
|
||||
});
|
||||
|
||||
it('will skip tags in codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some #text that includes #tags we #care-about.
|
||||
|
||||
\`\`\`
|
||||
this is a #codeblock
|
||||
\`\`\`
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'text',
|
||||
'tags',
|
||||
@@ -385,13 +356,9 @@ this is a #codeblock
|
||||
});
|
||||
|
||||
it('will skip tags in inlined codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some #text that includes #tags we #care-about.
|
||||
this is a \`inlined #codeblock\`
|
||||
`
|
||||
);
|
||||
this is a \`inlined #codeblock\` `);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'text',
|
||||
'tags',
|
||||
@@ -399,16 +366,13 @@ this is a \`inlined #codeblock\`
|
||||
]);
|
||||
});
|
||||
it('can find tags as text in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: hello, world this_is_good
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'hello',
|
||||
'world',
|
||||
@@ -420,16 +384,13 @@ this is some #text that includes #tags we #care-about.
|
||||
});
|
||||
|
||||
it('can find tags as array in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: [hello, world, this_is_good]
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'hello',
|
||||
'world',
|
||||
@@ -444,16 +405,13 @@ this is some #text that includes #tags we #care-about.
|
||||
// For now it's enough to just get the YAML block range
|
||||
// in the future we might want to be more specific
|
||||
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: [hello, world, this_is_good]
|
||||
---
|
||||
# this is a heading
|
||||
this is some text
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags[0]).toEqual({
|
||||
label: 'hello',
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
@@ -461,6 +419,45 @@ this is some text
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sections plugin', () => {
|
||||
it('should find sections within the note', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Section 1
|
||||
|
||||
This is the content of section 1.
|
||||
|
||||
## Section 1.1
|
||||
|
||||
This is the content of section 1.1.
|
||||
|
||||
# Section 2
|
||||
|
||||
This is the content of section 2.
|
||||
`);
|
||||
expect(note.sections).toHaveLength(3);
|
||||
expect(note.sections[0].label).toEqual('Section 1');
|
||||
expect(note.sections[0].range).toEqual(Range.create(1, 0, 9, 0));
|
||||
expect(note.sections[1].label).toEqual('Section 1.1');
|
||||
expect(note.sections[1].range).toEqual(Range.create(5, 0, 9, 0));
|
||||
expect(note.sections[2].label).toEqual('Section 2');
|
||||
expect(note.sections[2].range).toEqual(Range.create(9, 0, 13, 0));
|
||||
});
|
||||
|
||||
it('should support wikilinks and links in the section label', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Section with [[wikilink]]
|
||||
|
||||
This is the content of section with wikilink
|
||||
|
||||
## Section with [url](https://google.com)
|
||||
|
||||
This is the content of section with url`);
|
||||
expect(note.sections).toHaveLength(2);
|
||||
expect(note.sections[0].label).toEqual('Section with wikilink');
|
||||
expect(note.sections[1].label).toEqual('Section with url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser plugins', () => {
|
||||
const testPlugin: ParserPlugin = {
|
||||
visit: (node, note) => {
|
||||
|
||||
@@ -17,13 +17,7 @@ import {
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
extractHashtags,
|
||||
extractTagsFromProp,
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { extractHashtags, extractTagsFromProp, isNone, isSome } from './utils';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
@@ -69,7 +63,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + URI.toString(uri));
|
||||
Logger.info('Found: ' + uri.toString());
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
@@ -100,15 +94,26 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return URI.isMarkdownFile(uri);
|
||||
return uri.isMarkdown();
|
||||
}
|
||||
|
||||
read(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
let content = await this.dataStore.read(uri);
|
||||
if (isSome(content) && uri.fragment) {
|
||||
const resource = this.parser.parse(uri, content);
|
||||
const section = Resource.findSection(resource, uri.fragment);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
@@ -128,21 +133,32 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
def => def.label === link.target
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, resource.uri);
|
||||
const definedUri = resource.uri.resolve(definitionUri);
|
||||
targetUri =
|
||||
workspace.find(definedUri, resource.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
const [target, section] = link.target.split('#');
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
target === ''
|
||||
? resource.uri
|
||||
: workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
|
||||
if (section) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
const [target, section] = link.target.split('#');
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(URI.resolve(link.target, resource.uri).path);
|
||||
workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(resource.uri.resolve(link.target).path);
|
||||
if (section && !targetUri.isPlaceholder()) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return targetUri;
|
||||
@@ -161,9 +177,9 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
*/
|
||||
const getTextFromChildren = (root: Node): string => {
|
||||
let text = '';
|
||||
visit(root, 'text', node => {
|
||||
if (node.type === 'text') {
|
||||
text = text + (node as any).value;
|
||||
visit(root, node => {
|
||||
if (node.type === 'text' || node.type === 'wikiLink') {
|
||||
text = text + ((node as any).value || '');
|
||||
}
|
||||
});
|
||||
return text;
|
||||
@@ -201,6 +217,53 @@ const tagsPlugin: ParserPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
let sectionStack: Array<{ label: string; level: number; start: Position }> = [];
|
||||
const sectionsPlugin: ParserPlugin = {
|
||||
name: 'section',
|
||||
onWillVisitTree: () => {
|
||||
sectionStack = [];
|
||||
},
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'heading') {
|
||||
const level = (node as any).depth;
|
||||
const label = getTextFromChildren(node);
|
||||
if (!label || !level) {
|
||||
return;
|
||||
}
|
||||
const start = astPositionToFoamRange(node.position!).start;
|
||||
|
||||
// Close all the sections that are not parents of the current section
|
||||
while (
|
||||
sectionStack.length > 0 &&
|
||||
sectionStack[sectionStack.length - 1].level >= level
|
||||
) {
|
||||
const section = sectionStack.pop();
|
||||
note.sections.push({
|
||||
label: section.label,
|
||||
range: Range.createFromPosition(section.start, start),
|
||||
});
|
||||
}
|
||||
|
||||
// Add the new section to the stack
|
||||
sectionStack.push({ label, level, start });
|
||||
}
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
const end = Position.create(note.source.end.line + 1, 0);
|
||||
// Close all the remainig sections
|
||||
while (sectionStack.length > 0) {
|
||||
const section = sectionStack.pop();
|
||||
note.sections.push({
|
||||
label: section.label,
|
||||
range: { start: section.start, end },
|
||||
});
|
||||
}
|
||||
note.sections.sort((a, b) =>
|
||||
Position.compareTo(a.range.start, b.range.start)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const titlePlugin: ParserPlugin = {
|
||||
name: 'title',
|
||||
visit: (node, note) => {
|
||||
@@ -209,8 +272,8 @@ const titlePlugin: ParserPlugin = {
|
||||
node.type === 'heading' &&
|
||||
(node as any).depth === 1
|
||||
) {
|
||||
note.title =
|
||||
((node as Parent)!.children?.[0] as any)?.value || note.title;
|
||||
const title = getTextFromChildren(node);
|
||||
note.title = title.length > 0 ? title : note.title;
|
||||
}
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
@@ -219,7 +282,7 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title === '') {
|
||||
note.title = URI.getBasename(note.uri);
|
||||
note.title = note.uri.getName();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -254,7 +317,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = URI.resolve(targetUri, note.uri);
|
||||
const uri = note.uri.resolve(targetUri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
@@ -295,7 +358,7 @@ const handleError = (
|
||||
const name = plugin.name || '';
|
||||
Logger.warn(
|
||||
`Error while executing [${fnName}] in plugin [${name}]. ${
|
||||
uri ? 'for file [' + URI.toString(uri) : ']'
|
||||
uri ? 'for file [' + uri.toString() : ']'
|
||||
}.`,
|
||||
e
|
||||
);
|
||||
@@ -314,6 +377,7 @@ export function createMarkdownParser(
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
sectionsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
|
||||
@@ -327,7 +391,7 @@ export function createMarkdownParser(
|
||||
|
||||
const foamParser: ResourceParser = {
|
||||
parse: (uri: URI, markdown: string): Resource => {
|
||||
Logger.debug('Parsing:', URI.toString(uri));
|
||||
Logger.debug('Parsing:', uri.toString());
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
try {
|
||||
return plugin.onWillParseMarkdown?.(acc) || acc;
|
||||
@@ -344,6 +408,7 @@ export function createMarkdownParser(
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: '',
|
||||
sections: [],
|
||||
tags: [],
|
||||
links: [],
|
||||
definitions: [],
|
||||
@@ -384,10 +449,7 @@ export function createMarkdownParser(
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn(
|
||||
`Error while parsing YAML for [${URI.toString(uri)}]`,
|
||||
e
|
||||
);
|
||||
Logger.warn(`Error while parsing YAML for [${uri.toString()}]`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +521,8 @@ export function createMarkdownReferences(
|
||||
// Should never occur since we're already in a file,
|
||||
if (source?.type !== 'note') {
|
||||
console.warn(
|
||||
`Note ${URI.toString(
|
||||
noteUri
|
||||
)} note found in workspace when attempting to generate markdown reference list`
|
||||
`Note ${noteUri.toString()} note found in workspace when attempting \
|
||||
to generate markdown reference list`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -473,9 +534,7 @@ export function createMarkdownReferences(
|
||||
const target = workspace.find(targetUri);
|
||||
if (isNone(target)) {
|
||||
Logger.warn(
|
||||
`Link ${URI.toString(targetUri)} in ${URI.toString(
|
||||
noteUri
|
||||
)} is not valid.`
|
||||
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -484,10 +543,10 @@ export function createMarkdownReferences(
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = URI.relativePath(noteUri, target.uri);
|
||||
const pathToNote = includeExtension
|
||||
? relativePath
|
||||
: dropExtension(relativePath);
|
||||
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
|
||||
if (!includeExtension) {
|
||||
relativeUri = relativeUri.changeExtension('*', '');
|
||||
}
|
||||
|
||||
// [wikilink-text]: path/to/file.md "Page title"
|
||||
return {
|
||||
@@ -495,7 +554,7 @@ export function createMarkdownReferences(
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText || link.label,
|
||||
url: pathToNote,
|
||||
url: relativeUri.path,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore, IMatcher, Matcher } from '../services/datastore';
|
||||
import { FoamConfig } from '../config';
|
||||
import { IDataStore, IMatcher } from '../services/datastore';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { ResourceParser } from './note';
|
||||
@@ -18,21 +17,15 @@ export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
graph: FoamGraph;
|
||||
config: FoamConfig;
|
||||
tags: FoamTags;
|
||||
}
|
||||
|
||||
export const bootstrap = async (
|
||||
config: FoamConfig,
|
||||
matcher: IMatcher,
|
||||
dataStore: IDataStore,
|
||||
initialProviders: ResourceProvider[]
|
||||
) => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
const workspace = new FoamWorkspace();
|
||||
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
|
||||
|
||||
@@ -43,7 +36,6 @@ export const bootstrap = async (
|
||||
workspace,
|
||||
graph,
|
||||
tags,
|
||||
config,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { FoamWorkspace, uriToResourceName } from './workspace';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { Range } from './range';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
|
||||
@@ -99,16 +99,19 @@ export class FoamGraph implements IDisposable {
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
const placeholder = this.placeholders.get(name);
|
||||
if (placeholder) {
|
||||
this.placeholders.delete(name);
|
||||
const resourcesToUpdate = this.backlinks.get(placeholder.path) ?? [];
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res.source))
|
||||
);
|
||||
let resourcesToUpdate: URI[] = [];
|
||||
for (const placeholderId of this.placeholders.keys()) {
|
||||
// quick and dirty check for affected resources
|
||||
if (resource.uri.path.endsWith(placeholderId + '.md')) {
|
||||
resourcesToUpdate.push(
|
||||
...this.backlinks.get(placeholderId).map(c => c.source)
|
||||
);
|
||||
// resourcesToUpdate.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res))
|
||||
);
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
}
|
||||
@@ -170,7 +173,7 @@ export class FoamGraph implements IDisposable {
|
||||
|
||||
this.backlinks.get(target.path)?.push(connection);
|
||||
|
||||
if (URI.isPlaceholder(target)) {
|
||||
if (target.isPlaceholder()) {
|
||||
this.placeholders.set(uriToPlaceholderId(target), target);
|
||||
}
|
||||
return this;
|
||||
@@ -190,7 +193,7 @@ export class FoamGraph implements IDisposable {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
!source.isEqual(c.source) || !target.isEqual(c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links.set(
|
||||
@@ -206,7 +209,7 @@ export class FoamGraph implements IDisposable {
|
||||
);
|
||||
if (this.backlinks.get(target.path)?.length === 0) {
|
||||
this.backlinks.delete(target.path);
|
||||
if (URI.isPlaceholder(target)) {
|
||||
if (target.isPlaceholder()) {
|
||||
this.placeholders.delete(uriToPlaceholderId(target));
|
||||
}
|
||||
}
|
||||
@@ -232,8 +235,8 @@ export class FoamGraph implements IDisposable {
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
URI.isEqual(a.source, b.source) &&
|
||||
URI.isEqual(a.target, b.target) &&
|
||||
a.source.isEqual(b.source) &&
|
||||
a.target.isEqual(b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
|
||||
@@ -38,12 +38,17 @@ export interface Tag {
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
label: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
uri: URI;
|
||||
type: string;
|
||||
title: string;
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
sections: Section[];
|
||||
tags: Tag[];
|
||||
links: ResourceLink[];
|
||||
|
||||
@@ -66,7 +71,7 @@ export abstract class Resource {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
URI.isUri((thing as Resource).uri) &&
|
||||
(thing as Resource).uri instanceof URI &&
|
||||
typeof (thing as Resource).title === 'string' &&
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
@@ -74,4 +79,11 @@ export abstract class Resource {
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
public static findSection(resource: Resource, label: string): Section | null {
|
||||
if (label) {
|
||||
return resource.sections.find(s => s.label === label) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class FoamTags implements IDisposable {
|
||||
if (this.tags.has(tag)) {
|
||||
const remainingLocations = this.tags
|
||||
.get(tag)
|
||||
?.filter(uri => !URI.isEqual(uri, resource.uri));
|
||||
?.filter(uri => !uri.isEqual(resource.uri));
|
||||
|
||||
if (remainingLocations && remainingLocations.length > 0) {
|
||||
this.tags.set(tag, remainingLocations);
|
||||
|
||||
@@ -1,51 +1,75 @@
|
||||
import { Logger } from '../utils/log';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
import { URI } from './uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('Foam URIs', () => {
|
||||
describe('Foam URI', () => {
|
||||
describe('URI parsing', () => {
|
||||
const base = URI.file('/path/to/file.md');
|
||||
test.each([
|
||||
['https://www.google.com', URI.parse('https://www.google.com')],
|
||||
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
|
||||
['../relative/file.md', URI.file('/path/relative/file.md')],
|
||||
['#section', URI.create({ ...base, fragment: 'section' })],
|
||||
['#section', base.withFragment('section')],
|
||||
[
|
||||
'../relative/file.md#section',
|
||||
URI.parse('file:/path/relative/file.md#section'),
|
||||
],
|
||||
])('URI Parsing (%s)', (input, exp) => {
|
||||
const result = URI.resolve(input, base);
|
||||
const result = base.resolve(input);
|
||||
expect(result.scheme).toEqual(exp.scheme);
|
||||
expect(result.authority).toEqual(exp.authority);
|
||||
expect(result.path).toEqual(exp.path);
|
||||
expect(result.query).toEqual(exp.query);
|
||||
expect(result.fragment).toEqual(exp.fragment);
|
||||
});
|
||||
});
|
||||
it('supports various cases', () => {
|
||||
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
|
||||
'no-directory'
|
||||
);
|
||||
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
|
||||
'manydotsname'
|
||||
);
|
||||
|
||||
it('normalizes the Windows drive letter to upper case', () => {
|
||||
const upperCase = URI.parse('file:///C:/this/is/a/Path');
|
||||
const lowerCase = URI.parse('file:///c:/this/is/a/Path');
|
||||
expect(upperCase.path).toEqual('/C:/this/is/a/Path');
|
||||
expect(lowerCase.path).toEqual('/C:/this/is/a/Path');
|
||||
expect(upperCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
|
||||
expect(lowerCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
|
||||
});
|
||||
|
||||
it('consistently parses file paths', () => {
|
||||
const win1 = URI.file('c:\\this\\is\\a\\path');
|
||||
const win2 = URI.parse('c:\\this\\is\\a\\path');
|
||||
expect(win1).toEqual(win2);
|
||||
|
||||
const unix1 = URI.file('/this/is/a/path');
|
||||
const unix2 = URI.parse('/this/is/a/path');
|
||||
expect(unix1).toEqual(unix2);
|
||||
});
|
||||
|
||||
it('correctly parses file paths', () => {
|
||||
const winUri = URI.file('c:\\this\\is\\a\\path');
|
||||
const unixUri = URI.file('/this/is/a/path');
|
||||
expect(winUri).toEqual(
|
||||
new URI({
|
||||
scheme: 'file',
|
||||
path: '/C:/this/is/a/path',
|
||||
})
|
||||
);
|
||||
expect(unixUri).toEqual(
|
||||
new URI({
|
||||
scheme: 'file',
|
||||
path: '/this/is/a/path',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('computes a relative uri using a slug', () => {
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.md'), '../hello.md')
|
||||
).toEqual(URI.file('/hello.md'));
|
||||
expect(URI.computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
|
||||
it('supports computing relative paths', () => {
|
||||
expect(URI.file('/my/file.md').resolve('../hello.md')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
|
||||
).toEqual(URI.file('/hello.markdown'));
|
||||
expect(URI.file('/my/file.md').resolve('../hello')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(URI.file('/my/file.markdown').resolve('../hello')).toEqual(
|
||||
URI.file('/hello.markdown')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
// Some code in this file comes from https://github.com/microsoft/vscode/main/src/vs/base/common/uri.ts
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { isWindows } from '../common/platform';
|
||||
import * as pathUtils from '../utils/path';
|
||||
|
||||
/**
|
||||
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
|
||||
@@ -24,240 +23,134 @@ import { isWindows } from '../common/platform';
|
||||
* urn:example:animal:ferret:nose
|
||||
* ```
|
||||
*/
|
||||
export interface URI {
|
||||
scheme: string;
|
||||
authority: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
const { posix } = paths;
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
export abstract class URI {
|
||||
static create(from: Partial<URI>): URI {
|
||||
return {
|
||||
scheme: from.scheme ?? _empty,
|
||||
authority: from.authority ?? _empty,
|
||||
path: from.path ?? _empty,
|
||||
query: from.query ?? _empty,
|
||||
fragment: from.fragment ?? _empty,
|
||||
};
|
||||
export class URI {
|
||||
readonly scheme: string;
|
||||
readonly authority: string;
|
||||
readonly path: string;
|
||||
readonly query: string;
|
||||
readonly fragment: string;
|
||||
|
||||
constructor(from: Partial<URI> = {}) {
|
||||
this.scheme = from.scheme ?? _empty;
|
||||
this.authority = from.authority ?? _empty;
|
||||
this.path = from.path ?? _empty; // We assume the path is already posix
|
||||
this.query = from.query ?? _empty;
|
||||
this.fragment = from.fragment ?? _empty;
|
||||
}
|
||||
|
||||
static parse(value: string): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return URI.create({});
|
||||
return new URI();
|
||||
}
|
||||
return URI.create({
|
||||
return new URI({
|
||||
scheme: match[2] || 'file',
|
||||
authority: percentDecode(match[4] ?? _empty),
|
||||
path: percentDecode(match[5] ?? _empty),
|
||||
path: pathUtils.fromFsPath(percentDecode(match[5] ?? _empty))[0],
|
||||
query: percentDecode(match[7] ?? _empty),
|
||||
fragment: percentDecode(match[9] ?? _empty),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URI from value, taking into consideration possible relative paths.
|
||||
*
|
||||
* @param reference the URI to use as reference in case value is a relative path
|
||||
* @param value the value to parse for a URI
|
||||
* @returns the URI from the given value. In case of a relative path, the URI will take into account
|
||||
* the reference from which it is computed
|
||||
*/
|
||||
static resolve(value: string, reference: URI): URI {
|
||||
let uri = URI.parse(value);
|
||||
if (uri.scheme === 'file' && !value.startsWith('/')) {
|
||||
const [path, fragment] = value.split('#');
|
||||
uri =
|
||||
path.length > 0 ? URI.computeRelativeURI(reference, path) : reference;
|
||||
if (fragment) {
|
||||
uri = URI.create({
|
||||
...uri,
|
||||
fragment: fragment,
|
||||
});
|
||||
static file(value: string): URI {
|
||||
let [path, authority] = pathUtils.fromFsPath(value);
|
||||
return new URI({ scheme: 'file', authority, path });
|
||||
}
|
||||
|
||||
static placeholder(path: string): URI {
|
||||
return new URI({ scheme: 'placeholder', path: path });
|
||||
}
|
||||
|
||||
resolve(value: string | URI, isDirectory = false): URI {
|
||||
const uri = value instanceof URI ? value : URI.parse(value);
|
||||
if (!uri.isAbsolute()) {
|
||||
if (uri.scheme === 'file' || uri.scheme === 'placeholder') {
|
||||
let newUri = this.withFragment(uri.fragment);
|
||||
if (uri.path) {
|
||||
newUri = (isDirectory ? newUri : newUri.getDirectory())
|
||||
.joinPath(uri.path)
|
||||
.changeExtension('', this.getExtension());
|
||||
}
|
||||
return newUri;
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
static computeRelativeURI(reference: URI, relativeSlug: string): URI {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${posix.extname(reference.path)}`;
|
||||
return URI.create({
|
||||
...reference,
|
||||
path: posix.join(posix.dirname(reference.path), slug),
|
||||
});
|
||||
isAbsolute(): boolean {
|
||||
return pathUtils.isAbsolute(this.path);
|
||||
}
|
||||
|
||||
static file(path: string): URI {
|
||||
let authority = _empty;
|
||||
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if (isWindows) {
|
||||
if (path.startsWith(_slash)) {
|
||||
path = `${path.replace(/\\/g, _slash)}`;
|
||||
} else {
|
||||
path = `/${path.replace(/\\/g, _slash)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// check for authority as used in UNC shares
|
||||
// or use the path as given
|
||||
if (path[0] === _slash && path[1] === _slash) {
|
||||
const idx = path.indexOf(_slash, 2);
|
||||
if (idx === -1) {
|
||||
authority = path.substring(2);
|
||||
path = _slash;
|
||||
} else {
|
||||
authority = path.substring(2, idx);
|
||||
path = path.substring(idx) || _slash;
|
||||
}
|
||||
}
|
||||
|
||||
return URI.create({ scheme: 'file', authority, path });
|
||||
getDirectory(): URI {
|
||||
const path = pathUtils.getDirectory(this.path);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
static placeholder(key: string): URI {
|
||||
return URI.create({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
getBasename(): string {
|
||||
return pathUtils.getBasename(this.path);
|
||||
}
|
||||
|
||||
static relativePath(source: URI, target: URI): string {
|
||||
const relativePath = posix.relative(
|
||||
posix.dirname(source.path),
|
||||
target.path
|
||||
getName(): string {
|
||||
return pathUtils.getName(this.path);
|
||||
}
|
||||
|
||||
getExtension(): string {
|
||||
return pathUtils.getExtension(this.path);
|
||||
}
|
||||
|
||||
changeExtension(from: string, to: string): URI {
|
||||
const path = pathUtils.changeExtension(this.path, from, to);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
joinPath(...paths: string[]) {
|
||||
const path = pathUtils.joinPath(this.path, ...paths);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
relativeTo(uri: URI) {
|
||||
const path = pathUtils.relativeTo(this.path, uri.path);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
withFragment(fragment: string): URI {
|
||||
return new URI({ ...this, fragment });
|
||||
}
|
||||
|
||||
isPlaceholder(): boolean {
|
||||
return this.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
toFsPath() {
|
||||
return pathUtils.toFsPath(
|
||||
this.path,
|
||||
this.scheme === 'file' ? this.authority : ''
|
||||
);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
static getBasename(uri: URI) {
|
||||
return posix.parse(uri.path).name;
|
||||
toString(): string {
|
||||
return encode(this, false);
|
||||
}
|
||||
|
||||
static getDir(uri: URI) {
|
||||
return URI.file(posix.dirname(uri.path));
|
||||
isMarkdown(): boolean {
|
||||
const ext = this.getExtension();
|
||||
return ext === '.md' || ext === '.markdown';
|
||||
}
|
||||
|
||||
static getFileNameWithoutExtension(uri: URI) {
|
||||
return URI.getBasename(uri).replace(/\.[^.]+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
static createResourceUriFromPlaceholder(
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param uri The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
static joinPath(uri: URI, ...pathFragment: string[]): URI {
|
||||
if (!uri.path) {
|
||||
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
|
||||
}
|
||||
let newPath: string;
|
||||
if (isWindows && uri.scheme === 'file') {
|
||||
newPath = URI.file(paths.win32.join(URI.toFsPath(uri), ...pathFragment))
|
||||
.path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return URI.create({ ...uri, path: newPath });
|
||||
}
|
||||
|
||||
static toFsPath(uri: URI, keepDriveLetterCasing = true): string {
|
||||
let value: string;
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`;
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A &&
|
||||
uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a &&
|
||||
uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2);
|
||||
} else {
|
||||
value = uri.path.substr(1);
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path;
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, '\\');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static toString(uri: URI): string {
|
||||
return encode(uri, false);
|
||||
}
|
||||
|
||||
// --- utility
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
isEqual(uri: URI): boolean {
|
||||
return (
|
||||
typeof (thing as URI).authority === 'string' &&
|
||||
typeof (thing as URI).fragment === 'string' &&
|
||||
typeof (thing as URI).path === 'string' &&
|
||||
typeof (thing as URI).query === 'string' &&
|
||||
typeof (thing as URI).scheme === 'string'
|
||||
this.authority === uri.authority &&
|
||||
this.scheme === uri.scheme &&
|
||||
this.path === uri.path &&
|
||||
this.fragment === uri.fragment &&
|
||||
this.query === uri.query
|
||||
);
|
||||
}
|
||||
|
||||
static isPlaceholder(uri: URI): boolean {
|
||||
return uri.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
static isEqual(a: URI, b: URI): boolean {
|
||||
return (
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path &&
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query
|
||||
);
|
||||
}
|
||||
static isMarkdownFile(uri: URI): boolean {
|
||||
return uri.path.endsWith('.md');
|
||||
}
|
||||
}
|
||||
|
||||
// --- encode / decode
|
||||
@@ -331,20 +224,20 @@ function encode(uri: URI, skipEncoding: boolean): string {
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
// lower-case windows drive letters in /C:/fff or C:/fff
|
||||
// upper-case windows drive letters in /c:/fff or c:/fff
|
||||
if (
|
||||
path.length >= 3 &&
|
||||
path.charCodeAt(0) === CharCode.Slash &&
|
||||
path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
const code = path.charCodeAt(1);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
|
||||
if (code >= CharCode.a && code <= CharCode.z) {
|
||||
path = `/${String.fromCharCode(code - 32)}:${path.substr(3)}`; // "/C:".length === 3
|
||||
}
|
||||
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
|
||||
const code = path.charCodeAt(0);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
|
||||
if (code >= CharCode.a && code <= CharCode.z) {
|
||||
path = `${String.fromCharCode(code - 32)}:${path.substr(2)}`; // "/C:".length === 3
|
||||
}
|
||||
}
|
||||
// encode the rest of the path
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getReferenceType } from './workspace';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from './uri';
|
||||
@@ -6,24 +6,6 @@ import { createTestNote, createTestWorkspace } from '../../test/test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('Reference types', () => {
|
||||
it('Detects absolute references', () => {
|
||||
expect(getReferenceType('/hello')).toEqual('absolute-path');
|
||||
expect(getReferenceType('/hello/there')).toEqual('absolute-path');
|
||||
});
|
||||
it('Detects relative references', () => {
|
||||
expect(getReferenceType('../hello')).toEqual('relative-path');
|
||||
expect(getReferenceType('./hello')).toEqual('relative-path');
|
||||
expect(getReferenceType('./hello/there')).toEqual('relative-path');
|
||||
});
|
||||
it('Detects key references', () => {
|
||||
expect(getReferenceType('hello')).toEqual('key');
|
||||
});
|
||||
it('Detects URIs', () => {
|
||||
expect(getReferenceType(URI.file('/path/to/file.md'))).toEqual('uri');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace resources', () => {
|
||||
it('Adds notes to workspace', () => {
|
||||
const ws = createTestWorkspace();
|
||||
@@ -71,6 +53,41 @@ describe('Workspace resources', () => {
|
||||
ws.set(noteA);
|
||||
expect(ws.list()).toEqual([noteA]);
|
||||
});
|
||||
|
||||
it('#851 - listing by ID should not return files with same suffix', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'test-file.md' }))
|
||||
.set(createTestNote({ uri: 'file.md' }));
|
||||
expect(ws.listByIdentifier('file').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('Support dendron-style names', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'note.pdf' }))
|
||||
.set(createTestNote({ uri: 'note.md' }))
|
||||
.set(createTestNote({ uri: 'note.yo.md' }))
|
||||
.set(createTestNote({ uri: 'note2.md' }));
|
||||
for (const [reference, path] of [
|
||||
['note', '/note.md'],
|
||||
['note.md', '/note.md'],
|
||||
['note.yo', '/note.yo.md'],
|
||||
['note.yo.md', '/note.yo.md'],
|
||||
['note.pdf', '/note.pdf'],
|
||||
['note2', '/note2.md'],
|
||||
]) {
|
||||
expect(ws.listByIdentifier(reference)[0].uri.path).toEqual(path);
|
||||
expect(ws.find(reference).uri.path).toEqual(path);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should include fragment when finding resource URI', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'test-file.md' }))
|
||||
.set(createTestNote({ uri: 'file.md' }));
|
||||
|
||||
const res = ws.find('test-file#my-section');
|
||||
expect(res.uri.fragment).toEqual('my-section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Graph', () => {
|
||||
@@ -146,6 +163,86 @@ describe('Graph', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Identifier computation', () => {
|
||||
it('should compute the minimum identifier to resolve a name clash', () => {
|
||||
const first = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
});
|
||||
const second = createTestNote({
|
||||
uri: '/another/way/for/page-a.md',
|
||||
});
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.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');
|
||||
expect(ws.getIdentifier(third.uri)).toEqual('path/for/page-a');
|
||||
});
|
||||
|
||||
it('should support sections in identifier computation', () => {
|
||||
const first = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
});
|
||||
const second = createTestNote({
|
||||
uri: '/another/way/for/page-a.md',
|
||||
});
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(first)
|
||||
.set(second)
|
||||
.set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
);
|
||||
});
|
||||
|
||||
const needle = '/project/car/todo';
|
||||
|
||||
test.each([
|
||||
[['/project/home/todo', '/other/todo', '/something/else'], 'car/todo'],
|
||||
[['/family/car/todo', '/other/todo'], 'project/car/todo'],
|
||||
[[], 'todo'],
|
||||
])('Find shortest identifier', (haystack, id) => {
|
||||
expect(FoamWorkspace.getShortestIdentifier(needle, haystack)).toEqual(id);
|
||||
});
|
||||
|
||||
it('should ignore same string in haystack', () => {
|
||||
const haystack = [
|
||||
needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
|
||||
expect(identifier).toEqual('car/todo');
|
||||
});
|
||||
|
||||
it('should return best guess when no solution is possible', () => {
|
||||
/**
|
||||
* In this case there is no way to uniquely identify the element,
|
||||
* our fallback is to just return the "least wrong" result, basically
|
||||
* a full identifier
|
||||
* This is an edge case that should never happen in a real repo
|
||||
*/
|
||||
const haystack = [
|
||||
'/parent/' + needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
|
||||
expect(identifier).toEqual('project/car/todo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wikilinks', () => {
|
||||
it('Can be defined with basename, relative path, absolute path, extension', () => {
|
||||
const noteA = createTestNote({
|
||||
@@ -313,15 +410,14 @@ describe('Wikilinks', () => {
|
||||
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
// Attachments require extension
|
||||
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([]);
|
||||
});
|
||||
|
||||
it('Resolves conflicts alphabetically - part 1', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'attachment-a' }],
|
||||
links: [{ slug: 'attachment-a.pdf' }],
|
||||
});
|
||||
const attachmentA = createTestNote({
|
||||
uri: '/path/to/more/attachment-a.pdf',
|
||||
@@ -343,7 +439,7 @@ describe('Wikilinks', () => {
|
||||
it('Resolves conflicts alphabetically - part 2', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'attachment-a' }],
|
||||
links: [{ slug: 'attachment-a.pdf' }],
|
||||
});
|
||||
const attachmentA = createTestNote({
|
||||
uri: '/path/to/more/attachment-a.pdf',
|
||||
@@ -362,21 +458,7 @@ describe('Wikilinks', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Allows for dendron-style wikilinks, including a dot', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'dendron.style' }],
|
||||
});
|
||||
const noteB1 = createTestNote({ uri: '/path/to/another/dendron.style.md' });
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB1);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB1.uri]);
|
||||
});
|
||||
|
||||
it('Handles capatalization of files and wikilinks correctly', () => {
|
||||
it('Handles capitalization of files and wikilinks correctly', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
@@ -408,7 +490,7 @@ describe('Wikilinks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown direct links', () => {
|
||||
describe('Markdown direct links', () => {
|
||||
it('Support absolute and relative path', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
|
||||
@@ -1,35 +1,11 @@
|
||||
import * as path from 'path';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { isSome, isNone } from '../utils';
|
||||
import { isAbsolute, getExtension, changeExtension } from '../utils/path';
|
||||
import { isSome } from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
|
||||
export function getReferenceType(
|
||||
reference: URI | string
|
||||
): 'uri' | 'absolute-path' | 'relative-path' | 'key' {
|
||||
if (URI.isUri(reference)) {
|
||||
return 'uri';
|
||||
}
|
||||
const isPath = reference.split('/').length > 1;
|
||||
if (!isPath) {
|
||||
return 'key';
|
||||
}
|
||||
const isAbsPath = isPath && reference.startsWith('/');
|
||||
return isAbsPath ? 'absolute-path' : 'relative-path';
|
||||
}
|
||||
|
||||
const pathToResourceId = (pathValue: string) => {
|
||||
const { ext } = path.parse(pathValue);
|
||||
return ext.length > 0 ? pathValue : pathValue + '.md';
|
||||
};
|
||||
const uriToResourceId = (uri: URI) => pathToResourceId(uri.path);
|
||||
|
||||
const pathToResourceName = (pathValue: string) =>
|
||||
path.parse(pathValue).name.toLowerCase();
|
||||
export const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
private onDidUpdateEmitter = new Emitter<{ old: Resource; new: Resource }>();
|
||||
@@ -41,11 +17,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
private providers: ResourceProvider[] = [];
|
||||
|
||||
/**
|
||||
* Resources by key / slug
|
||||
*/
|
||||
private resourcesByName: Map<string, string[]> = new Map();
|
||||
/**
|
||||
* Resources by URI
|
||||
* Resources by path
|
||||
*/
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
@@ -55,14 +27,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
set(resource: Resource) {
|
||||
const id = uriToResourceId(resource.uri);
|
||||
const old = this.find(resource.uri);
|
||||
const name = uriToResourceName(resource.uri);
|
||||
this.resources.set(id, resource);
|
||||
if (!this.resourcesByName.has(name)) {
|
||||
this.resourcesByName.set(name, []);
|
||||
}
|
||||
this.resourcesByName.get(name)?.push(id);
|
||||
this.resources.set(normalize(resource.uri.path), resource);
|
||||
isSome(old)
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
@@ -70,28 +36,15 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
delete(uri: URI) {
|
||||
const id = uriToResourceId(uri);
|
||||
const deleted = this.resources.get(id);
|
||||
this.resources.delete(id);
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
this.resourcesByName.set(
|
||||
name,
|
||||
this.resourcesByName.get(name)?.filter(resId => resId !== id) ?? []
|
||||
);
|
||||
if (this.resourcesByName.get(name)?.length === 0) {
|
||||
this.resourcesByName.delete(name);
|
||||
}
|
||||
const deleted = this.resources.get(normalize(uri.path));
|
||||
this.resources.delete(normalize(uri.path));
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
public exists(uri: URI): boolean {
|
||||
return (
|
||||
!URI.isPlaceholder(uri) &&
|
||||
isSome(this.resources.get(uriToResourceId(uri)))
|
||||
);
|
||||
return isSome(this.find(uri));
|
||||
}
|
||||
|
||||
public list(): Resource[] {
|
||||
@@ -107,48 +60,69 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public find(resourceId: URI | string, reference?: URI): Resource | null {
|
||||
const refType = getReferenceType(resourceId);
|
||||
switch (refType) {
|
||||
case 'uri':
|
||||
const uri = resourceId as URI;
|
||||
return this.exists(uri)
|
||||
? this.resources.get(uriToResourceId(uri)) ?? null
|
||||
: null;
|
||||
|
||||
case 'key':
|
||||
const name = pathToResourceName(resourceId as string);
|
||||
let paths = this.resourcesByName.get(name);
|
||||
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
paths = this.resourcesByName.get(resourceId as string);
|
||||
}
|
||||
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// prettier-ignore
|
||||
const sortedPaths = paths.length === 1
|
||||
? paths
|
||||
: paths.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return this.resources.get(sortedPaths[0]) ?? null;
|
||||
|
||||
case 'absolute-path':
|
||||
const resourceUri = URI.file(resourceId as string);
|
||||
return this.resources.get(uriToResourceId(resourceUri)) ?? null;
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
return null;
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = URI.computeRelativeURI(reference, relativePath);
|
||||
return this.resources.get(uriToResourceId(targetUri)) ?? null;
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected reference type: ' + refType);
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
let needle = normalize('/' + identifier);
|
||||
let mdNeedle = getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
let resources = [];
|
||||
for (const key of this.resources.keys()) {
|
||||
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
|
||||
resources.push(this.resources.get(normalize(key)));
|
||||
}
|
||||
}
|
||||
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimal identifier for the given resource
|
||||
*
|
||||
* @param forResource the resource to compute the identifier for
|
||||
*/
|
||||
public getIdentifier(forResource: URI): string {
|
||||
const amongst = [];
|
||||
const basename = forResource.getBasename();
|
||||
for (const res of this.resources.values()) {
|
||||
// Just a quick optimization to only add the elements that might match
|
||||
if (res.uri.path.endsWith(basename)) {
|
||||
if (!res.uri.isEqual(forResource)) {
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
identifier = changeExtension(identifier, '.md', '');
|
||||
if (forResource.fragment) {
|
||||
identifier += `#${forResource.fragment}`;
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this.resources.get(normalize((reference as URI).path)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
let [path, fragment] = (reference as string).split('#');
|
||||
if (FoamWorkspace.isIdentifier(path)) {
|
||||
resource = this.listByIdentifier(path)[0];
|
||||
} else {
|
||||
if (isAbsolute(path) || isSome(baseUri)) {
|
||||
if (getExtension(path) !== '.md') {
|
||||
const uri = baseUri.resolve(path + '.md');
|
||||
resource = uri ? this.resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
if (!resource) {
|
||||
const uri = baseUri.resolve(path);
|
||||
resource = uri ? this.resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource && fragment) {
|
||||
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
|
||||
}
|
||||
return resource ?? null;
|
||||
}
|
||||
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
@@ -175,4 +149,51 @@ export class FoamWorkspace implements IDisposable {
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
}
|
||||
|
||||
static isIdentifier(path: string): boolean {
|
||||
return !(
|
||||
path.startsWith('/') ||
|
||||
path.startsWith('./') ||
|
||||
path.startsWith('../')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimal identifier for the given string amongst others
|
||||
*
|
||||
* @param forPath the value to compute the identifier for
|
||||
* @param amongst the set of strings within which to find the identifier
|
||||
*/
|
||||
static getShortestIdentifier(forPath: string, amongst: string[]): string {
|
||||
const needleTokens = forPath.split('/').reverse();
|
||||
const haystack = amongst
|
||||
.filter(value => value !== forPath)
|
||||
.map(value => value.split('/').reverse());
|
||||
|
||||
let tokenIndex = 0;
|
||||
let res = needleTokens;
|
||||
while (tokenIndex < needleTokens.length) {
|
||||
for (let j = haystack.length - 1; j >= 0; j--) {
|
||||
if (
|
||||
haystack[j].length < tokenIndex ||
|
||||
needleTokens[tokenIndex] !== haystack[j][tokenIndex]
|
||||
) {
|
||||
haystack.splice(j, 1);
|
||||
}
|
||||
}
|
||||
if (haystack.length === 0) {
|
||||
res = needleTokens.splice(0, tokenIndex + 1);
|
||||
break;
|
||||
}
|
||||
tokenIndex++;
|
||||
}
|
||||
const identifier = res
|
||||
.filter(token => token.trim() !== '')
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
|
||||
const normalize = (v: string) => v.toLocaleLowerCase();
|
||||
|
||||
@@ -5,14 +5,14 @@ import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = URI.joinPath(TEST_DATA_DIR, 'test-datastore');
|
||||
const testFolder = TEST_DATA_DIR.joinPath('test-datastore');
|
||||
|
||||
describe('Matcher', () => {
|
||||
it('generates globs with the base dir provided', () => {
|
||||
const matcher = new Matcher([testFolder], ['*'], []);
|
||||
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '*')),
|
||||
toMatcherPathFormat(testFolder.joinPath('*')),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Matcher', () => {
|
||||
const matcher = new Matcher([testFolder]);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '**', '*')),
|
||||
toMatcherPathFormat(testFolder.joinPath('**', '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,32 +28,32 @@ describe('Matcher', () => {
|
||||
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g1')),
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g2')),
|
||||
toMatcherPathFormat(testFolder.joinPath('g1')),
|
||||
toMatcherPathFormat(testFolder.joinPath('g2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a match method to filter strings', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.match(files)).toEqual([
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a isMatch method to see whether a file is matched or not', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(true);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
@@ -72,10 +72,10 @@ describe('Matcher', () => {
|
||||
it('ignores files in the exclude list', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(false);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
|
||||
@@ -39,8 +39,8 @@ export interface IMatcher {
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => URI.toFsPath(uri).replace(/\\/g, '/')
|
||||
: (uri: URI) => URI.toFsPath(uri);
|
||||
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
|
||||
: (uri: URI) => uri.toFsPath();
|
||||
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
@@ -76,7 +76,7 @@ export class Matcher implements IMatcher {
|
||||
|
||||
match(files: URI[]) {
|
||||
const matches = micromatch(
|
||||
files.map(f => URI.toFsPath(f)),
|
||||
files.map(f => f.toFsPath()),
|
||||
this.include,
|
||||
{
|
||||
ignore: this.exclude,
|
||||
@@ -123,7 +123,7 @@ export class FileDataStore implements IDataStore {
|
||||
|
||||
async read(uri: URI) {
|
||||
try {
|
||||
return (await fs.promises.readFile(URI.toFsPath(uri))).toString();
|
||||
return (await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
|
||||
@@ -2,12 +2,6 @@ import { titleCase } from 'title-case';
|
||||
export { extractHashtags, extractTagsFromProp } from './hashtags';
|
||||
export * from './core';
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
const parts = path.split('.');
|
||||
parts.pop();
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filename
|
||||
|
||||
192
packages/foam-vscode/src/core/utils/path.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { posix } from 'path';
|
||||
import { promises, constants } from 'fs';
|
||||
|
||||
/**
|
||||
* Converts filesystem path to POSIX path. Supported inputs are:
|
||||
* - Windows path starting with a drive letter, e.g. C:\dir\file.ext
|
||||
* - UNC path for a shared file, e.g. \\server\share\path\file.ext
|
||||
* - POSIX path, e.g. /dir/file.ext
|
||||
*
|
||||
* @param path A supported filesystem path.
|
||||
* @returns [path, authority] where path is a POSIX representation for the
|
||||
* given input and authority is undefined except for UNC paths.
|
||||
*/
|
||||
export function fromFsPath(path: string): [string, string] {
|
||||
let authority: string;
|
||||
if (isUNCShare(path)) {
|
||||
[path, authority] = parseUNCShare(path);
|
||||
path = path.replace(/\\/g, '/');
|
||||
} else if (hasDrive(path)) {
|
||||
path = '/' + path[0].toUpperCase() + path.substr(1).replace(/\\/g, '/');
|
||||
} else if (path[0] === '/' && hasDrive(path, 1)) {
|
||||
// POSIX representation of a Windows path: just normalize drive letter case
|
||||
path = '/' + path[1].toUpperCase() + path.substr(2);
|
||||
}
|
||||
return [path, authority];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a POSIX path to a filesystem path.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @param authority An optional authority used to build UNC paths. This only
|
||||
* makes sense for the Windows platform.
|
||||
* @returns A platform-specific representation of the given POSIX path.
|
||||
*/
|
||||
export function toFsPath(path: string, authority?: string): string {
|
||||
if (path[0] === '/' && hasDrive(path, 1)) {
|
||||
path = path.substr(1).replace(/\//g, '\\');
|
||||
if (authority) {
|
||||
path = `\\\\${authority}${path}`;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the containing directory of a POSIX path, e.g.
|
||||
* - /d1/d2/f.ext -> /d1/d2
|
||||
* - /d1/d2 -> /d1
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns true if the path is absolute, false otherwise.
|
||||
*/
|
||||
export function isAbsolute(path: string): boolean {
|
||||
return posix.isAbsolute(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the containing directory of a POSIX path, e.g.
|
||||
* - /d1/d2/f.ext -> /d1/d2
|
||||
* - /d1/d2 -> /d1
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The containing directory of the given path.
|
||||
*/
|
||||
export function getDirectory(path: string): string {
|
||||
return posix.dirname(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the basename of a POSIX path, e.g. /d/f.ext -> f.ext.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The basename of the given path.
|
||||
*/
|
||||
export function getBasename(path: string): string {
|
||||
return posix.basename(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the name of a POSIX path, e.g. /d/f.ext -> f.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The name of the given path.
|
||||
*/
|
||||
export function getName(path: string): string {
|
||||
return changeExtension(getBasename(path), '*', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the extension of a POSIX path, e.g.
|
||||
* - /d/f.ext -> .ext
|
||||
* - /d/f.g.ext -> .ext
|
||||
* - /d/f -> ''
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The extension of the given path.
|
||||
*/
|
||||
export function getExtension(path: string): string {
|
||||
return posix.extname(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a POSIX path matching some extension to have another extension.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @param from The required current extension, or '*' to match any extension.
|
||||
* @param to The target extension.
|
||||
* @returns A POSIX path with its extension possibly changed.
|
||||
*/
|
||||
export function changeExtension(
|
||||
path: string,
|
||||
from: string,
|
||||
to: string
|
||||
): string {
|
||||
const old = getExtension(path);
|
||||
if ((from === '*' && old !== to) || old === from) {
|
||||
path = path.substring(0, path.length - old.length);
|
||||
return to ? path + to : path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a number of POSIX paths into a single POSIX path, e.g.
|
||||
* - /d1, d2, f.ext -> /d1/d2/f.ext
|
||||
* - /d1/d2, .., f.ext -> /d1/f.ext
|
||||
*
|
||||
* @param paths A variable number of POSIX paths.
|
||||
* @returns A POSIX path built from the given POSIX paths.
|
||||
*/
|
||||
export function joinPath(...paths: string[]): string {
|
||||
return posix.join(...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a POSIX path relative to another POSIX path, e.g.
|
||||
* - /d1/d2 relative to /d1 -> d2
|
||||
* - /d1/d2 relative to /d1/d3 -> ../d2
|
||||
*
|
||||
* @param path The POSIX path to be made relative.
|
||||
* @param basePath The POSIX base path.
|
||||
* @returns A POSIX path relative to the base path.
|
||||
*/
|
||||
export function relativeTo(path: string, basePath: string): string {
|
||||
return posix.relative(basePath, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if there is an accessible file for a path.
|
||||
*
|
||||
* @param fsPath A filesystem-specific path.
|
||||
* @returns true if an accesible file exists, false otherwise.
|
||||
*/
|
||||
export async function existsInFs(fsPath: string) {
|
||||
try {
|
||||
await promises.access(fsPath, constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDrive(path: string, idx = 0): boolean {
|
||||
if (path.length <= idx) {
|
||||
return false;
|
||||
}
|
||||
const c = path.charCodeAt(idx);
|
||||
return (
|
||||
((c >= CharCode.A && c <= CharCode.Z) ||
|
||||
(c >= CharCode.a && c <= CharCode.z)) &&
|
||||
path.charCodeAt(idx + 1) === CharCode.Colon
|
||||
);
|
||||
}
|
||||
|
||||
function isUNCShare(fsPath: string): boolean {
|
||||
return (
|
||||
fsPath.length >= 2 &&
|
||||
fsPath.charCodeAt(0) === CharCode.Backslash &&
|
||||
fsPath.charCodeAt(1) === CharCode.Backslash
|
||||
);
|
||||
}
|
||||
|
||||
function parseUNCShare(uncPath: string): [string, string] {
|
||||
const idx = uncPath.indexOf('\\', 2);
|
||||
if (idx === -1) {
|
||||
return [uncPath.substring(2), '\\'];
|
||||
} else {
|
||||
return [uncPath.substring(2, idx), uncPath.substring(idx) || '\\'];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export const uriToSlug = (uri: URI): string =>
|
||||
GithubSlugger.slug(URI.getBasename(uri));
|
||||
@@ -1,13 +1,13 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
|
||||
import { URI } from './core/model/uri';
|
||||
import { isWindows } from './utils';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
@@ -19,11 +19,9 @@ describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
config,
|
||||
`${isoDate}.md`
|
||||
);
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
const oldValue = await workspace
|
||||
.getConfiguration('foam')
|
||||
@@ -33,8 +31,8 @@ describe('getDailyNotePath', () => {
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toEqual(
|
||||
URI.toFsPath(expectedPath)
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toEqual(
|
||||
expectedPath.toFsPath()
|
||||
);
|
||||
|
||||
await workspace
|
||||
@@ -44,7 +42,7 @@ describe('getDailyNotePath', () => {
|
||||
|
||||
test('Uses absolute directories without modification', async () => {
|
||||
const config = isWindows
|
||||
? 'c:\\absolute_path\\journal'
|
||||
? 'C:\\absolute_path\\journal'
|
||||
: '/absolute_path/journal';
|
||||
const expectedPath = isWindows
|
||||
? `${config}\\${isoDate}.md`
|
||||
@@ -59,7 +57,7 @@ describe('getDailyNotePath', () => {
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toMatch(
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toMatch(
|
||||
expectedPath
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import { isAbsolute } from 'path';
|
||||
import { focusNote, pathExists } from './utils';
|
||||
import { existsInFs } from './core/utils/path';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { createNoteFromDailyNoteTemplate } from './features/create-from-template';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { NoteFactory } from './services/templates';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
@@ -24,7 +25,12 @@ export async function openDailyNoteFor(date?: Date) {
|
||||
dailyNotePath,
|
||||
currentDate
|
||||
);
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
// if a new file is created, the editor is automatically created
|
||||
// but forcing the focus will block the template placeholders from working
|
||||
// so we only explicitly focus on the note if the file already exists
|
||||
if (!isNew) {
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,16 +50,16 @@ export function getDailyNotePath(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
): URI {
|
||||
const dailyNoteDirectory: string =
|
||||
configuration.get('openDailyNote.directory') ?? '.';
|
||||
const dailyNoteDirectory = URI.file(
|
||||
configuration.get('openDailyNote.directory') ?? '.'
|
||||
);
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
|
||||
if (isAbsolute(dailyNoteDirectory)) {
|
||||
return URI.joinPath(URI.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
if (dailyNoteDirectory.isAbsolute()) {
|
||||
return dailyNoteDirectory.joinPath(dailyNoteFilename);
|
||||
} else {
|
||||
return URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
dailyNoteDirectory,
|
||||
return fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
|
||||
dailyNoteDirectory.path,
|
||||
dailyNoteFilename
|
||||
);
|
||||
}
|
||||
@@ -100,7 +106,7 @@ export async function createDailyNoteIfNotExists(
|
||||
dailyNotePath: URI,
|
||||
targetDate: Date
|
||||
) {
|
||||
if (await pathExists(dailyNotePath)) {
|
||||
if (await existsInFs(dailyNotePath.toFsPath())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -116,7 +122,7 @@ foam_template:
|
||||
# ${dateFormat(targetDate, titleFormat, false)}
|
||||
`;
|
||||
|
||||
await createNoteFromDailyNoteTemplate(
|
||||
await NoteFactory.createFromDailyNoteTemplate(
|
||||
dailyNotePath,
|
||||
templateFallbackText,
|
||||
targetDate
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import { FoamConfig } from './core/config';
|
||||
import { MarkdownResourceProvider } from './core/markdown-provider';
|
||||
import { bootstrap } from './core/model/foam';
|
||||
import { FileDataStore, Matcher } from './core/services/datastore';
|
||||
import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { getConfigFromVscode } from './services/config';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
|
||||
function createMarkdownProvider(config: FoamConfig): MarkdownResourceProvider {
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
const provider = new MarkdownResourceProvider(matcher, triggers => {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
return [
|
||||
watcher.onDidChange(triggers.onDidChange),
|
||||
watcher.onDidCreate(triggers.onDidCreate),
|
||||
watcher.onDidDelete(triggers.onDidDelete),
|
||||
watcher,
|
||||
];
|
||||
});
|
||||
return provider;
|
||||
}
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
@@ -36,10 +18,23 @@ export async function activate(context: ExtensionContext) {
|
||||
Logger.info('Starting Foam');
|
||||
|
||||
// Prepare Foam
|
||||
const config: FoamConfig = getConfigFromVscode();
|
||||
const dataStore = new FileDataStore();
|
||||
const markdownProvider = createMarkdownProvider(config);
|
||||
const foamPromise = bootstrap(config, dataStore, [markdownProvider]);
|
||||
const matcher = new Matcher(
|
||||
workspace.workspaceFolders.map(dir => fromVsCodeUri(dir.uri)),
|
||||
['**/*'],
|
||||
getIgnoredFilesSetting().map(g => g.toString())
|
||||
);
|
||||
const markdownProvider = new MarkdownResourceProvider(matcher, triggers => {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
return [
|
||||
watcher.onDidChange(uri => triggers.onDidChange(fromVsCodeUri(uri))),
|
||||
watcher.onDidCreate(uri => triggers.onDidCreate(fromVsCodeUri(uri))),
|
||||
watcher.onDidDelete(uri => triggers.onDidDelete(fromVsCodeUri(uri))),
|
||||
watcher,
|
||||
];
|
||||
});
|
||||
|
||||
const foamPromise = bootstrap(matcher, dataStore, [markdownProvider]);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createNote,
|
||||
getUriInWorkspace,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
@@ -25,7 +26,8 @@ describe('Backlinks panel', () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
const rootUri = workspace.workspaceFolders[0].uri;
|
||||
// TODO: this should really just be the workspace folder, use that once #806 is fixed
|
||||
const rootUri = getUriInWorkspace('just-a-ref.md');
|
||||
const ws = createTestWorkspace();
|
||||
|
||||
const noteA = createTestNote({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { Resource, ResourceLink } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -21,7 +22,9 @@ const feature: FoamFeature = {
|
||||
const provider = new BacklinksTreeDataProvider(foam.workspace, foam.graph);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
provider.target = vscode.window.activeTextEditor?.document.uri;
|
||||
provider.target = vscode.window.activeTextEditor
|
||||
? fromVsCodeUri(vscode.window.activeTextEditor?.document.uri)
|
||||
: undefined;
|
||||
await provider.refresh();
|
||||
});
|
||||
|
||||
@@ -60,7 +63,7 @@ export class BacklinksTreeDataProvider
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
URI.isEqual(this.workspace.resolveLink(resource, link), uri)
|
||||
this.workspace.resolveLink(resource, link).isEqual(uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
@@ -90,7 +93,7 @@ export class BacklinksTreeDataProvider
|
||||
}
|
||||
|
||||
const backlinksByResourcePath = groupBy(
|
||||
this.graph.getConnections(uri).filter(c => URI.isEqual(c.target, uri)),
|
||||
this.graph.getConnections(uri).filter(c => c.target.isEqual(uri)),
|
||||
b => b.source.path
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Uri } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import path from 'path';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { createFile } from '../test/test-utils-vscode';
|
||||
import * as editor from '../services/editor';
|
||||
|
||||
describe('createFromTemplate', () => {
|
||||
describe('Create from template commands', () => {
|
||||
describe('create-note-from-template', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -21,6 +23,71 @@ describe('createFromTemplate', () => {
|
||||
'No templates available. Would you like to create one instead?',
|
||||
});
|
||||
});
|
||||
|
||||
it('offers to pick which template to use', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
const templateB = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-b.md',
|
||||
]);
|
||||
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
[
|
||||
expect.objectContaining({ label: 'template-a.md' }),
|
||||
expect.objectContaining({ label: 'template-b.md' }),
|
||||
],
|
||||
{
|
||||
placeHolder: 'Select a template to use.',
|
||||
}
|
||||
);
|
||||
|
||||
await workspace.fs.delete(toVsCodeUri(templateA.uri));
|
||||
await workspace.fs.delete(toVsCodeUri(templateB.uri));
|
||||
});
|
||||
|
||||
it('Uses template metadata to improve dialog box', async () => {
|
||||
const templateA = await createFile(
|
||||
`---
|
||||
foam_template:
|
||||
name: My Template
|
||||
description: My Template description
|
||||
---
|
||||
|
||||
Template A
|
||||
`,
|
||||
['.foam', 'templates', 'template-a.md']
|
||||
);
|
||||
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
label: 'My Template',
|
||||
description: 'template-a.md',
|
||||
detail: 'My Template description',
|
||||
}),
|
||||
],
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
await workspace.fs.delete(toVsCodeUri(templateA.uri));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create-note-from-default-template', () => {
|
||||
@@ -33,7 +100,7 @@ describe('createFromTemplate', () => {
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const fileWriteSpy = jest.spyOn(workspace.fs, 'writeFile');
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
@@ -45,7 +112,7 @@ describe('createFromTemplate', () => {
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(fileWriteSpy).toHaveBeenCalledTimes(0);
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,12 +122,12 @@ describe('createFromTemplate', () => {
|
||||
});
|
||||
|
||||
it('should create a new template', async () => {
|
||||
const template = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'hello-world.md'
|
||||
);
|
||||
).fsPath;
|
||||
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(template);
|
||||
@@ -75,12 +142,12 @@ describe('createFromTemplate', () => {
|
||||
|
||||
it('can be cancelled', async () => {
|
||||
// This is the default template which would be created.
|
||||
const template = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'new-template.md'
|
||||
);
|
||||
).fsPath;
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
@@ -1,109 +1,14 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { existsSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isAbsolute } from 'path';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
commands,
|
||||
ExtensionContext,
|
||||
QuickPickItem,
|
||||
Selection,
|
||||
SnippetString,
|
||||
TextDocument,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
WorkspaceEdit,
|
||||
} from 'vscode';
|
||||
import { commands, ExtensionContext, QuickPickItem, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { focusNote } from '../utils';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
|
||||
const templatesDir = URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates'
|
||||
);
|
||||
|
||||
export class UserCancelledOperation extends Error {
|
||||
constructor(message?: string) {
|
||||
super('UserCancelledOperation');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FoamSelectionContent {
|
||||
document: TextDocument;
|
||||
selection: Selection;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const knownFoamVariables = new Set([
|
||||
'FOAM_TITLE',
|
||||
'FOAM_SELECTED_TEXT',
|
||||
'FOAM_DATE_YEAR',
|
||||
'FOAM_DATE_YEAR_SHORT',
|
||||
'FOAM_DATE_MONTH',
|
||||
'FOAM_DATE_MONTH_NAME',
|
||||
'FOAM_DATE_MONTH_NAME_SHORT',
|
||||
'FOAM_DATE_DATE',
|
||||
'FOAM_DATE_DAY_NAME',
|
||||
'FOAM_DATE_DAY_NAME_SHORT',
|
||||
'FOAM_DATE_HOUR',
|
||||
'FOAM_DATE_MINUTE',
|
||||
'FOAM_DATE_SECOND',
|
||||
'FOAM_DATE_SECONDS_UNIX',
|
||||
]);
|
||||
|
||||
const wikilinkDefaultTemplateText = `# $\{1:$FOAM_TITLE}\n\n$0`;
|
||||
const defaultTemplateDefaultText: string = `---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}
|
||||
`;
|
||||
const defaultTemplateUri = URI.joinPath(templatesDir, 'new-note.md');
|
||||
const dailyNoteTemplateUri = URI.joinPath(templatesDir, 'daily-note.md');
|
||||
|
||||
const templateContent = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
Welcome to Foam templates.
|
||||
|
||||
What you see in the heading is a placeholder
|
||||
- it allows you to quickly move through positions of the new note by pressing TAB, e.g. to easily fill fields
|
||||
- a placeholder optionally has a default value, which can be some text or, as in this case, a [variable](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables)
|
||||
- when landing on a placeholder, the default value is already selected so you can easily replace it
|
||||
- a placeholder can define a list of values, e.g.: \${2|one,two,three|}
|
||||
- you can use variables even outside of placeholders, here is today's date: \${CURRENT_YEAR}/\${CURRENT_MONTH}/\${CURRENT_DATE}
|
||||
|
||||
For a full list of features see [the VS Code snippets page](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax).
|
||||
|
||||
## To get started
|
||||
|
||||
1. edit this file to create the shape new notes from this template will look like
|
||||
2. create a note from this template by running the \`Foam: Create New Note From Template\` command
|
||||
`;
|
||||
|
||||
async function templateMetadata(
|
||||
templateUri: URI
|
||||
): Promise<Map<string, string>> {
|
||||
const contents = await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString());
|
||||
const [templateMetadata] = extractFoamTemplateFrontmatterMetadata(contents);
|
||||
return templateMetadata;
|
||||
}
|
||||
|
||||
async function getTemplates(): Promise<URI[]> {
|
||||
const templates = await workspace.findFiles('.foam/templates/**.md', null);
|
||||
return templates;
|
||||
}
|
||||
import {
|
||||
createTemplate,
|
||||
DEFAULT_TEMPLATE_URI,
|
||||
getTemplateMetadata,
|
||||
getTemplates,
|
||||
NoteFactory,
|
||||
TEMPLATES_DIR,
|
||||
} from '../services/templates';
|
||||
import { Resolver } from '../services/variable-resolver';
|
||||
|
||||
async function offerToCreateTemplate(): Promise<void> {
|
||||
const response = await window.showQuickPick(['Yes', 'No'], {
|
||||
@@ -116,217 +21,6 @@ async function offerToCreateTemplate(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function findFoamVariables(templateText: string): string[] {
|
||||
const regex = /\$(FOAM_[_a-zA-Z0-9]*)|\${(FOAM_[[_a-zA-Z0-9]*)}/g;
|
||||
var matches = [];
|
||||
const output: string[] = [];
|
||||
while ((matches = regex.exec(templateText))) {
|
||||
output.push(matches[1] || matches[2]);
|
||||
}
|
||||
const uniqVariables = [...new Set(output)];
|
||||
const knownVariables = uniqVariables.filter(x => knownFoamVariables.has(x));
|
||||
return knownVariables;
|
||||
}
|
||||
|
||||
async function resolveFoamTitle() {
|
||||
const title = await window.showInputBox({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: value =>
|
||||
value.trim().length === 0 ? 'Please enter a title' : undefined,
|
||||
});
|
||||
if (title === undefined) {
|
||||
throw new UserCancelledOperation();
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
function resolveFoamSelectedText() {
|
||||
return findSelectionContent()?.content ?? '';
|
||||
}
|
||||
|
||||
class Resolver {
|
||||
promises = new Map<string, Thenable<string>>();
|
||||
|
||||
constructor(
|
||||
private givenValues: Map<string, string>,
|
||||
private foamDate: Date
|
||||
) {}
|
||||
|
||||
resolve(name: string): Thenable<string> {
|
||||
if (this.givenValues.has(name)) {
|
||||
this.promises.set(name, Promise.resolve(this.givenValues.get(name)));
|
||||
} else if (!this.promises.has(name)) {
|
||||
switch (name) {
|
||||
case 'FOAM_TITLE':
|
||||
this.promises.set(name, resolveFoamTitle());
|
||||
break;
|
||||
case 'FOAM_SELECTED_TEXT':
|
||||
this.promises.set(name, Promise.resolve(resolveFoamSelectedText()));
|
||||
break;
|
||||
case 'FOAM_DATE_YEAR':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { year: 'numeric' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_YEAR_SHORT':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { year: '2-digit' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_MONTH':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { month: '2-digit' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_MONTH_NAME':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { month: 'long' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_MONTH_NAME_SHORT':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { month: 'short' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_DATE':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { day: '2-digit' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_DAY_NAME':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { weekday: 'long' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_DAY_NAME_SHORT':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate.toLocaleString('default', { weekday: 'short' })
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_HOUR':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate
|
||||
.toLocaleString('default', {
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.padStart(2, '0')
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_MINUTE':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate
|
||||
.toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.padStart(2, '0')
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_SECOND':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
this.foamDate
|
||||
.toLocaleString('default', {
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.padStart(2, '0')
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'FOAM_DATE_SECONDS_UNIX':
|
||||
this.promises.set(
|
||||
name,
|
||||
Promise.resolve(
|
||||
(this.foamDate.getTime() / 1000).toString().padStart(2, '0')
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this.promises.set(name, Promise.resolve(name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const result = this.promises.get(name);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveFoamVariables(
|
||||
variables: string[],
|
||||
givenValues: Map<string, string>,
|
||||
foamDate: Date = new Date()
|
||||
) {
|
||||
const resolver = new Resolver(givenValues, foamDate);
|
||||
const promises = variables.map(async variable =>
|
||||
Promise.resolve([variable, await resolver.resolve(variable)])
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const valueByName = new Map<string, string>();
|
||||
results.forEach(([variable, value]) => {
|
||||
valueByName.set(variable, value);
|
||||
});
|
||||
|
||||
return valueByName;
|
||||
}
|
||||
|
||||
export function substituteFoamVariables(
|
||||
templateText: string,
|
||||
givenValues: Map<string, string>
|
||||
) {
|
||||
givenValues.forEach((value, variable) => {
|
||||
const regex = new RegExp(
|
||||
// Matches a limited subset of the the TextMate variable syntax:
|
||||
// ${VARIABLE} OR $VARIABLE
|
||||
`\\\${${variable}}|\\$${variable}(\\W|$)`,
|
||||
// The latter is more complicated, since it needs to avoid replacing
|
||||
// longer variable names with the values of variables that are
|
||||
// substrings of the longer ones (e.g. `$FOO` and `$FOOBAR`. If you
|
||||
// replace $FOO first, and aren't careful, you replace the first
|
||||
// characters of `$FOOBAR`)
|
||||
'g' // 'g' => Global replacement (i.e. not just the first instance)
|
||||
);
|
||||
templateText = templateText.replace(regex, `${value}$1`);
|
||||
});
|
||||
|
||||
return templateText;
|
||||
}
|
||||
|
||||
function sortTemplatesMetadata(
|
||||
t1: Map<string, string>,
|
||||
t2: Map<string, string>
|
||||
@@ -363,8 +57,8 @@ async function askUserForTemplate() {
|
||||
const templatesMetadata = (
|
||||
await Promise.all(
|
||||
templates.map(async templateUri => {
|
||||
const metadata = await templateMetadata(templateUri);
|
||||
metadata.set('templatePath', path.basename(templateUri.path));
|
||||
const metadata = await getTemplateMetadata(templateUri);
|
||||
metadata.set('templatePath', templateUri.getBasename());
|
||||
return metadata;
|
||||
})
|
||||
)
|
||||
@@ -396,393 +90,54 @@ async function askUserForTemplate() {
|
||||
});
|
||||
}
|
||||
|
||||
async function askUserForFilepathConfirmation(
|
||||
defaultFilepath: URI,
|
||||
defaultFilename: string
|
||||
) {
|
||||
const fsPath = URI.toFsPath(defaultFilepath);
|
||||
return await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function appendSnippetVariableUsage(templateText: string, variable: string) {
|
||||
if (templateText.endsWith('\n')) {
|
||||
return `${templateText}\${${variable}}\n`;
|
||||
} else {
|
||||
return `${templateText}\n\${${variable}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveFoamTemplateVariables(
|
||||
templateText: string,
|
||||
extraVariablesToResolve: Set<string> = new Set(),
|
||||
givenValues: Map<string, string> = new Map(),
|
||||
foamDate: Date = new Date()
|
||||
): Promise<[Map<string, string>, string]> {
|
||||
const variablesInTemplate = findFoamVariables(templateText.toString());
|
||||
const variables = variablesInTemplate.concat(...extraVariablesToResolve);
|
||||
const uniqVariables = [...new Set(variables)];
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(
|
||||
uniqVariables,
|
||||
givenValues,
|
||||
foamDate
|
||||
);
|
||||
|
||||
if (
|
||||
resolvedValues.get('FOAM_SELECTED_TEXT') &&
|
||||
!variablesInTemplate.includes('FOAM_SELECTED_TEXT')
|
||||
) {
|
||||
templateText = appendSnippetVariableUsage(
|
||||
templateText,
|
||||
'FOAM_SELECTED_TEXT'
|
||||
);
|
||||
variablesInTemplate.push('FOAM_SELECTED_TEXT');
|
||||
variables.push('FOAM_SELECTED_TEXT');
|
||||
uniqVariables.push('FOAM_SELECTED_TEXT');
|
||||
}
|
||||
|
||||
const subbedText = substituteFoamVariables(
|
||||
templateText.toString(),
|
||||
resolvedValues
|
||||
);
|
||||
|
||||
return [resolvedValues, subbedText];
|
||||
}
|
||||
|
||||
async function writeTemplate(
|
||||
templateSnippet: SnippetString,
|
||||
filepath: URI,
|
||||
viewColumn: ViewColumn = ViewColumn.Active
|
||||
) {
|
||||
await workspace.fs.writeFile(
|
||||
toVsCodeUri(filepath),
|
||||
new TextEncoder().encode('')
|
||||
);
|
||||
await focusNote(filepath, true, viewColumn);
|
||||
await window.activeTextEditor.insertSnippet(templateSnippet);
|
||||
}
|
||||
|
||||
function currentDirectoryFilepath(filename: string) {
|
||||
const activeFile = window.activeTextEditor?.document?.uri.path;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? URI.parse(path.dirname(activeFile))
|
||||
: workspace.workspaceFolders[0].uri;
|
||||
|
||||
return URI.joinPath(currentDir, filename);
|
||||
}
|
||||
|
||||
function findSelectionContent(): FoamSelectionContent | undefined {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const document = editor.document;
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!document || selection.isEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
selection,
|
||||
content: document.getText(selection),
|
||||
};
|
||||
}
|
||||
|
||||
async function replaceSelectionWithWikiLink(
|
||||
document: TextDocument,
|
||||
newNoteFile: URI,
|
||||
selection: Selection
|
||||
) {
|
||||
const newNoteTitle = URI.getFileNameWithoutExtension(newNoteFile);
|
||||
|
||||
const originatingFileEdit = new WorkspaceEdit();
|
||||
originatingFileEdit.replace(document.uri, selection, `[[${newNoteTitle}]]`);
|
||||
|
||||
await workspace.applyEdit(originatingFileEdit);
|
||||
}
|
||||
|
||||
function resolveFilepathAttribute(filepath) {
|
||||
return isAbsolute(filepath)
|
||||
? URI.file(filepath)
|
||||
: URI.joinPath(workspace.workspaceFolders[0].uri, filepath);
|
||||
}
|
||||
|
||||
export function determineDefaultFilepath(
|
||||
resolvedValues: Map<string, string>,
|
||||
templateMetadata: Map<string, string>,
|
||||
fallbackURI: URI = undefined
|
||||
) {
|
||||
let defaultFilepath: URI;
|
||||
if (templateMetadata.get('filepath')) {
|
||||
defaultFilepath = resolveFilepathAttribute(
|
||||
templateMetadata.get('filepath')
|
||||
);
|
||||
} else if (fallbackURI) {
|
||||
return fallbackURI;
|
||||
} else {
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
defaultFilepath = currentDirectoryFilepath(`${defaultSlug}.md`);
|
||||
}
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a daily note from the daily note template.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use if daily-note.md template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
export async function createNoteFromDailyNoteTemplate(
|
||||
filepathFallbackURI: URI,
|
||||
templateFallbackText: string,
|
||||
targetDate: Date
|
||||
): Promise<void> {
|
||||
return await createNoteFromDefaultTemplate(
|
||||
new Map(),
|
||||
new Set(['FOAM_SELECTED_TEXT']),
|
||||
dailyNoteTemplateUri,
|
||||
filepathFallbackURI,
|
||||
templateFallbackText,
|
||||
targetDate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note when following a placeholder wikilink using the default template.
|
||||
* @param wikilinkPlaceholder the placeholder value from the wikilink. (eg. `[[Hello Joe]]` -> `Hello Joe`)
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
export async function createNoteForPlaceholderWikilink(
|
||||
wikilinkPlaceholder: string,
|
||||
filepathFallbackURI: URI
|
||||
): Promise<void> {
|
||||
return await createNoteFromDefaultTemplate(
|
||||
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']),
|
||||
defaultTemplateUri,
|
||||
filepathFallbackURI,
|
||||
wikilinkDefaultTemplateText
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note using the default note template.
|
||||
* @param givenValues already resolved values of Foam template variables. These are used instead of resolving the Foam template variables.
|
||||
* @param extraVariablesToResolve Foam template variables to resolve, in addition to those mentioned in the template.
|
||||
* @param templateUri the URI of the template to use.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use the default note template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
async function createNoteFromDefaultTemplate(
|
||||
givenValues: Map<string, string> = new Map(),
|
||||
extraVariablesToResolve: Set<string> = new Set([
|
||||
'FOAM_TITLE',
|
||||
'FOAM_SELECTED_TEXT',
|
||||
]),
|
||||
templateUri: URI = defaultTemplateUri,
|
||||
filepathFallbackURI: URI = undefined,
|
||||
templateFallbackText: string = defaultTemplateDefaultText,
|
||||
foamDate: Date = new Date()
|
||||
): Promise<void> {
|
||||
const templateText = existsSync(URI.toFsPath(templateUri))
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
: templateFallbackText;
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
let resolvedValues: Map<string, string>,
|
||||
templateWithResolvedVariables: string;
|
||||
try {
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(
|
||||
templateText,
|
||||
extraVariablesToResolve,
|
||||
givenValues.set('FOAM_SELECTED_TEXT', selectedContent?.content ?? ''),
|
||||
foamDate
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata,
|
||||
filepathFallbackURI
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
let filepath = defaultFilepath;
|
||||
if (existsSync(URI.toFsPath(filepath))) {
|
||||
const newFilepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
defaultFilename
|
||||
);
|
||||
|
||||
if (newFilepath === undefined) {
|
||||
return;
|
||||
}
|
||||
filepath = URI.file(newFilepath);
|
||||
}
|
||||
|
||||
await writeTemplate(
|
||||
templateSnippet,
|
||||
filepath,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
await replaceSelectionWithWikiLink(
|
||||
selectedContent.document,
|
||||
filepath,
|
||||
selectedContent.selection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNoteFromTemplate(
|
||||
templateFilename?: string
|
||||
): Promise<void> {
|
||||
const selectedTemplate = await askUserForTemplate();
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
templateFilename =
|
||||
(selectedTemplate as QuickPickItem).description ||
|
||||
(selectedTemplate as QuickPickItem).label;
|
||||
const templateUri = URI.joinPath(templatesDir, templateFilename);
|
||||
const templateText = await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString());
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
let resolvedValues, templateWithResolvedVariables;
|
||||
try {
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(
|
||||
templateText,
|
||||
new Set(['FOAM_SELECTED_TEXT']),
|
||||
new Map().set('FOAM_SELECTED_TEXT', selectedContent?.content ?? '')
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
const filepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
defaultFilename
|
||||
);
|
||||
|
||||
if (filepath === undefined) {
|
||||
return;
|
||||
}
|
||||
const filepathURI = URI.file(filepath);
|
||||
|
||||
await writeTemplate(
|
||||
templateSnippet,
|
||||
filepathURI,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
await replaceSelectionWithWikiLink(
|
||||
selectedContent.document,
|
||||
filepathURI,
|
||||
selectedContent.selection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewTemplate(): Promise<void> {
|
||||
const defaultFilename = 'new-template.md';
|
||||
const defaultTemplate = URI.joinPath(templatesDir, defaultFilename);
|
||||
const fsPath = URI.toFsPath(defaultTemplate);
|
||||
const filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
if (filename === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filenameURI = URI.file(filename);
|
||||
await workspace.fs.writeFile(
|
||||
toVsCodeUri(filenameURI),
|
||||
new TextEncoder().encode(templateContent)
|
||||
);
|
||||
await focusNote(filenameURI, false);
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
createNoteFromTemplate
|
||||
async () => {
|
||||
const selectedTemplate = await askUserForTemplate();
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
const templateFilename =
|
||||
(selectedTemplate as QuickPickItem).description ||
|
||||
(selectedTemplate as QuickPickItem).label;
|
||||
const templateUri = TEMPLATES_DIR.joinPath(templateFilename);
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
await NoteFactory.createFromTemplate(templateUri, resolver);
|
||||
}
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-default-template',
|
||||
createNoteFromDefaultTemplate
|
||||
() => {
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
NoteFactory.createFromTemplate(
|
||||
DEFAULT_TEMPLATE_URI,
|
||||
resolver,
|
||||
undefined,
|
||||
`---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}
|
||||
`
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-new-template',
|
||||
createNewTemplate
|
||||
createTemplate
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
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';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -46,7 +45,7 @@ const feature: FoamFeature = {
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e?.document?.uri?.scheme === 'file') {
|
||||
const note = foam.workspace.get(e.document.uri);
|
||||
const note = foam.workspace.get(fromVsCodeUri(e.document.uri));
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
type: 'didSelectNote',
|
||||
@@ -76,7 +75,7 @@ function generateGraphData(foam: Foam) {
|
||||
|
||||
foam.workspace.list().forEach(n => {
|
||||
const type = n.type === 'note' ? n.properties.type ?? 'note' : n.type;
|
||||
const title = n.type === 'note' ? n.title : path.basename(n.uri.path);
|
||||
const title = n.type === 'note' ? n.title : n.uri.getBasename();
|
||||
graph.nodeInfo[n.uri.path] = {
|
||||
id: n.uri.path,
|
||||
type: type,
|
||||
@@ -91,7 +90,7 @@ function generateGraphData(foam: Foam) {
|
||||
source: c.source.path,
|
||||
target: c.target.path,
|
||||
});
|
||||
if (URI.isPlaceholder(c.target)) {
|
||||
if (c.target.isPlaceholder()) {
|
||||
graph.nodeInfo[c.target.path] = {
|
||||
id: c.target.path,
|
||||
type: 'placeholder',
|
||||
@@ -143,7 +142,7 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
|
||||
case 'webviewDidSelectNode':
|
||||
const noteUri = vscode.Uri.parse(message.payload);
|
||||
const selectedNote = foam.workspace.get(noteUri);
|
||||
const selectedNote = foam.workspace.get(fromVsCodeUri(noteUri));
|
||||
|
||||
if (isSome(selectedNote)) {
|
||||
const doc = await vscode.workspace.openTextDocument(
|
||||
@@ -169,25 +168,27 @@ async function getWebviewContent(
|
||||
context: vscode.ExtensionContext,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const datavizPath = [context.extensionPath, 'static', 'dataviz'];
|
||||
const datavizPath = vscode.Uri.joinPath(
|
||||
vscode.Uri.file(context.extensionPath),
|
||||
'static',
|
||||
'dataviz'
|
||||
);
|
||||
|
||||
const getWebviewUri = (fileName: string) =>
|
||||
panel.webview.asWebviewUri(
|
||||
vscode.Uri.file(path.join(...datavizPath, fileName))
|
||||
);
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizPath, fileName));
|
||||
|
||||
const indexHtml = await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.file(path.join(...datavizPath, 'index.html'))
|
||||
vscode.Uri.joinPath(datavizPath, 'index.html')
|
||||
);
|
||||
|
||||
// Replace the script paths with the appropriate webview URI.
|
||||
const filled = new TextDecoder('utf-8')
|
||||
.decode(indexHtml)
|
||||
.replace(/<script data-replace src="([^"]+")/g, match => {
|
||||
const fileName = match
|
||||
.slice('<script data-replace src="'.length, -1)
|
||||
.trim();
|
||||
return '<script src="' + getWebviewUri(fileName).toString() + '"';
|
||||
.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;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
@@ -9,20 +8,15 @@ import {
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Range } from '../core/model/range';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const CONFIG_KEY = 'decorations.links.enable';
|
||||
|
||||
const linkDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
color: { id: 'textLink.foreground' },
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
color: { id: 'editorWarning.foreground' },
|
||||
color: { id: 'foam.placeholder' },
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
@@ -31,21 +25,31 @@ const updateDecorations = (
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (!editor || !areDecorationsEnabled()) {
|
||||
if (
|
||||
!editor ||
|
||||
!areDecorationsEnabled() ||
|
||||
editor.document.languageId !== 'markdown'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const note = parser.parse(editor.document.uri, editor.document.getText());
|
||||
let linkRanges = [];
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(editor.document.uri),
|
||||
editor.document.getText()
|
||||
);
|
||||
let placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = workspace.resolveLink(note, link);
|
||||
if (URI.isPlaceholder(linkUri)) {
|
||||
placeholderRanges.push(link.range);
|
||||
} else {
|
||||
linkRanges.push(link.range);
|
||||
if (linkUri.isPlaceholder()) {
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + 2,
|
||||
link.range.end.line,
|
||||
link.range.end.character - 2
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
editor.setDecorations(linkDecoration, linkRanges);
|
||||
editor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
};
|
||||
|
||||
@@ -75,7 +79,6 @@ const feature: FoamFeature = {
|
||||
|
||||
context.subscriptions.push(
|
||||
areDecorationsEnabled,
|
||||
linkDecoration,
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { createTestWorkspace } from '../test/test-utils';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { LinkProvider } from './document-link-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
|
||||
describe('Document links provider', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('should not return any link for empty documents', async () => {
|
||||
const { uri, content } = await createFile('');
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not return any link for documents without links', async () => {
|
||||
const { uri, content } = await createFile(
|
||||
'This is some content without links'
|
||||
);
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should support wikilinks', async () => {
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(`this is a link to [[${fileB.name}]].`);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 27));
|
||||
});
|
||||
|
||||
it('should support regular links', async () => {
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(
|
||||
`this is a link to [a file](./${fileB.base}).`
|
||||
);
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 38));
|
||||
});
|
||||
|
||||
it('should support placeholders', async () => {
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
const ws = new FoamWorkspace().set(parser.parse(fileA.uri, fileA.content));
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(toVsCodeUri(URI.placeholder('a placeholder')))
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 35));
|
||||
});
|
||||
|
||||
it('should support wikilinks that have an alias', async () => {
|
||||
const fileB = await createFile("# File B that's aliased");
|
||||
const fileA = await createFile(
|
||||
`this is a link to [[${fileB.name}|alias]].`
|
||||
);
|
||||
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 33));
|
||||
});
|
||||
|
||||
it('should support wikilink aliases in tables using escape character', async () => {
|
||||
const fileB = await createFile('# File that has to be aliased');
|
||||
const fileA = await createFile(`
|
||||
| Col A | ColB |
|
||||
| --- | --- |
|
||||
| [[${fileB.name}\\|alias]] | test |
|
||||
`);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
if (!getFoamVsCodeConfig('links.navigation.enable')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
new LinkProvider(foam.workspace, foam.services.parser)
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private parser: ResourceParser
|
||||
) {}
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
|
||||
return resource.links.map(link => {
|
||||
const target = this.workspace.resolveLink(resource, link);
|
||||
const command = OPEN_COMMAND.asURI(toVsCodeUri(target));
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
toVsCodeRange(link.range),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = URI.isPlaceholder(target)
|
||||
? `Create note for '${target.path}'`
|
||||
: `Go to ${URI.toFsPath(target)}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
@@ -6,23 +6,20 @@ import {
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Matcher } from '../core/services/datastore';
|
||||
import { getConfigFromVscode } from '../services/config';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { HoverProvider } from './hover-provider';
|
||||
|
||||
// We can't use createTestWorkspace from /packages/foam-vscode/src/test/test-utils.ts
|
||||
// because we need a MarkdownResourceProvider with a real instance of FileDataStore.
|
||||
const createWorkspace = () => {
|
||||
const config = getConfigFromVscode();
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
vscode.workspace.workspaceFolders.map(f => fromVsCodeUri(f.uri))
|
||||
);
|
||||
const resourceProvider = new MarkdownResourceProvider(matcher);
|
||||
const workspace = new FoamWorkspace();
|
||||
@@ -60,7 +57,7 @@ describe('Hover provider', () => {
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
const pos = new vscode.Position(0, 0);
|
||||
const result = await provider.provideHover(doc, pos, noCancelToken);
|
||||
|
||||
@@ -78,7 +75,7 @@ describe('Hover provider', () => {
|
||||
|
||||
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
const pos = new vscode.Position(0, 0);
|
||||
const result = await provider.provideHover(doc, pos, noCancelToken);
|
||||
|
||||
@@ -263,10 +260,36 @@ The content of file B`);
|
||||
it('should include other backlinks (but not self) to target wikilink', async () => {
|
||||
const fileA = await createFile(`This is some content`);
|
||||
const fileB = await createFile(
|
||||
`this is a link to [a file](./${fileA.base}).`
|
||||
`This is a direct link to [a file](./${fileA.base}).`
|
||||
);
|
||||
const fileC = await createFile(`Here is a wikilink to [[${fileA.name}]]`);
|
||||
|
||||
const ws = createWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content))
|
||||
.set(parser.parse(fileC.uri, fileC.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const pos = new vscode.Position(0, 29); // Set cursor position on the link.
|
||||
|
||||
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
|
||||
const result = await provider.provideHover(doc, pos, noCancelToken);
|
||||
|
||||
expect(result.contents).toHaveLength(2);
|
||||
expect(getValue(result.contents[0])).toEqual(`This is some content`);
|
||||
expect(getValue(result.contents[1])).toMatch(
|
||||
/^Also referenced in 1 note:/
|
||||
);
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('should only add a note only once no matter how many links it has to the target', async () => {
|
||||
const fileA = await createFile(`This is some content`);
|
||||
const fileB = await createFile(`This is a link to [[${fileA.name}]].`);
|
||||
const fileC = await createFile(
|
||||
`this is another note linked to [[${fileA.name}]]`
|
||||
`This note is linked to [[${fileA.name}]] twice, here is the second: [[${fileA.name}]]`
|
||||
);
|
||||
|
||||
const ws = createWorkspace()
|
||||
@@ -282,14 +305,12 @@ The content of file B`);
|
||||
const result = await provider.provideHover(doc, pos, noCancelToken);
|
||||
|
||||
expect(result.contents).toHaveLength(2);
|
||||
expect(getValue(result.contents[0])).toEqual(`This is some content`);
|
||||
expect(getValue(result.contents[1])).toMatch(
|
||||
/^Also referenced in 1 note:/
|
||||
);
|
||||
ws.dispose();
|
||||
graph.dispose();
|
||||
});
|
||||
|
||||
it('should work for placeholders', async () => {
|
||||
const fileA = await createFile(`Some content and a [[placeholder]]`);
|
||||
const fileB = await createFile(`More content to a [[placeholder]]`);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
|
||||
import { toVsCodeRange } from '../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
monitorFoamVsCodeConfig,
|
||||
@@ -59,7 +59,10 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const startResource = this.parser.parse(document.uri, document.getText());
|
||||
const startResource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
|
||||
const targetLink: ResourceLink | undefined = startResource.links.find(
|
||||
link =>
|
||||
@@ -72,29 +75,32 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const documentUri = fromVsCodeUri(document.uri);
|
||||
const targetUri = this.workspace.resolveLink(startResource, targetLink);
|
||||
const refs = this.graph
|
||||
.getBacklinks(targetUri)
|
||||
.filter(link => !URI.isEqual(link.source, document.uri));
|
||||
const sources = uniqWith(
|
||||
this.graph
|
||||
.getBacklinks(targetUri)
|
||||
.filter(link => !link.source.isEqual(documentUri))
|
||||
.map(link => link.source),
|
||||
(u1, u2) => u1.isEqual(u2)
|
||||
);
|
||||
|
||||
const links = refs.slice(0, 10).map(link => {
|
||||
const command = OPEN_COMMAND.asURI(link.source);
|
||||
return `- [${
|
||||
this.workspace.get(link.source).title
|
||||
}](${command.toString()})`;
|
||||
const links = sources.slice(0, 10).map(ref => {
|
||||
const command = OPEN_COMMAND.asURI(ref);
|
||||
return `- [${this.workspace.get(ref).title}](${command.toString()})`;
|
||||
});
|
||||
|
||||
const notes = `note${refs.length > 1 ? 's' : ''}`;
|
||||
const notes = `note${sources.length > 1 ? 's' : ''}`;
|
||||
const references = getNoteTooltip(
|
||||
[
|
||||
`Also referenced in ${refs.length} ${notes}:`,
|
||||
`Also referenced in ${sources.length} ${notes}:`,
|
||||
...links,
|
||||
links.length === refs.length ? '' : '- ...',
|
||||
links.length === sources.length ? '' : '- ...',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
let mdContent = null;
|
||||
if (!URI.isPlaceholder(targetUri)) {
|
||||
if (!targetUri.isPlaceholder()) {
|
||||
const content: string = await this.workspace.readAsMarkdown(targetUri);
|
||||
|
||||
mdContent = isSome(content)
|
||||
@@ -103,7 +109,7 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
}
|
||||
|
||||
const hover: vscode.Hover = {
|
||||
contents: [mdContent, refs.length > 0 ? references : null],
|
||||
contents: [mdContent, sources.length > 0 ? references : null],
|
||||
range: toVsCodeRange(targetLink.range),
|
||||
};
|
||||
return hover;
|
||||
|
||||
@@ -11,15 +11,18 @@ import orphans from './orphans';
|
||||
import placeholders from './placeholders';
|
||||
import backlinks from './backlinks';
|
||||
import utilityCommands from './utility-commands';
|
||||
import documentLinkProvider from './document-link-provider';
|
||||
import hoverProvider from './hover-provider';
|
||||
import previewNavigation from './preview-navigation';
|
||||
import completionProvider from './link-completion';
|
||||
import tagCompletionProvider from './tag-completion';
|
||||
import linkDecorations from './document-decorator';
|
||||
import navigationProviders from './navigation-provider';
|
||||
import wikilinkDiagnostics from './wikilink-diagnostics';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
export const features: FoamFeature[] = [
|
||||
navigationProviders,
|
||||
wikilinkDiagnostics,
|
||||
tagsExplorer,
|
||||
createReferences,
|
||||
openDailyNote,
|
||||
@@ -32,7 +35,6 @@ export const features: FoamFeature[] = [
|
||||
orphans,
|
||||
placeholders,
|
||||
backlinks,
|
||||
documentLinkProvider,
|
||||
hoverProvider,
|
||||
utilityCommands,
|
||||
linkDecorations,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
@@ -69,7 +68,7 @@ async function janitor(foam: Foam) {
|
||||
async function runJanitor(foam: Foam) {
|
||||
const notes: Resource[] = foam.workspace
|
||||
.list()
|
||||
.filter(r => URI.isMarkdownFile(r.uri));
|
||||
.filter(r => r.uri.isMarkdown());
|
||||
|
||||
let updatedHeadingCount = 0;
|
||||
let updatedDefinitionListCount = 0;
|
||||
@@ -86,11 +85,11 @@ async function runJanitor(foam: Foam) {
|
||||
);
|
||||
|
||||
const dirtyNotes = notes.filter(note =>
|
||||
dirtyEditorsFileName.includes(URI.toFsPath(note.uri))
|
||||
dirtyEditorsFileName.includes(note.uri.toFsPath())
|
||||
);
|
||||
|
||||
const nonDirtyNotes = notes.filter(
|
||||
note => !dirtyEditorsFileName.includes(URI.toFsPath(note.uri))
|
||||
note => !dirtyEditorsFileName.includes(note.uri.toFsPath())
|
||||
);
|
||||
|
||||
const wikilinkSetting = getWikilinkDefinitionSetting();
|
||||
@@ -126,7 +125,7 @@ async function runJanitor(foam: Foam) {
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
|
||||
return fs.promises.writeFile(URI.toFsPath(note.uri), text);
|
||||
return fs.promises.writeFile(note.uri.toFsPath(), text);
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
@@ -136,7 +135,7 @@ async function runJanitor(foam: Foam) {
|
||||
for (const doc of dirtyTextDocuments) {
|
||||
const editor = await window.showTextDocument(doc);
|
||||
const note = dirtyNotes.find(
|
||||
n => URI.toFsPath(n.uri) === editor.document.uri.fsPath
|
||||
n => n.uri.toFsPath() === editor.document.uri.fsPath
|
||||
)!;
|
||||
|
||||
// Get edits
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
@@ -8,15 +9,21 @@ import {
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { CompletionProvider } from './link-completion';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import {
|
||||
CompletionProvider,
|
||||
SectionCompletionProvider,
|
||||
} from './link-completion';
|
||||
|
||||
describe('Link Completion', () => {
|
||||
const root = vscode.workspace.workspaceFolders[0].uri;
|
||||
const parser = createMarkdownParser([]);
|
||||
const root = fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri);
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(
|
||||
createTestNote({
|
||||
root,
|
||||
uri: 'file-name.md',
|
||||
sections: ['Section One', 'Section Two'],
|
||||
})
|
||||
)
|
||||
.set(
|
||||
@@ -31,6 +38,12 @@ describe('Link Completion', () => {
|
||||
uri: 'path/to/file.md',
|
||||
links: [{ slug: 'placeholder text' }],
|
||||
})
|
||||
)
|
||||
.set(
|
||||
createTestNote({
|
||||
root,
|
||||
uri: 'another/file.md',
|
||||
})
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
@@ -61,19 +74,6 @@ describe('Link Completion', () => {
|
||||
expect(links).toBeNull();
|
||||
});
|
||||
|
||||
it('should return notes and placeholders', async () => {
|
||||
const { uri } = await createFile('[[file]] [[');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 11)
|
||||
);
|
||||
|
||||
expect(links.items.length).toEqual(4);
|
||||
});
|
||||
|
||||
it('should not return link outside the wikilink brackets', async () => {
|
||||
const { uri } = await createFile('[[file]] then');
|
||||
const { doc } = await showInEditor(uri);
|
||||
@@ -86,4 +86,68 @@ describe('Link Completion', () => {
|
||||
|
||||
expect(links).toBeNull();
|
||||
});
|
||||
|
||||
it('should return notes with unique identifiers, and placeholders', async () => {
|
||||
const { uri } = await createFile('[[file]] [[');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 11)
|
||||
);
|
||||
|
||||
expect(links.items.length).toEqual(5);
|
||||
expect(new Set(links.items.map(i => i.insertText))).toEqual(
|
||||
new Set([
|
||||
'to/file',
|
||||
'another/file',
|
||||
'File name with spaces',
|
||||
'file-name',
|
||||
'placeholder text',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sections for other notes', async () => {
|
||||
const { uri } = await createFile('[[file-name#');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 12)
|
||||
);
|
||||
|
||||
expect(new Set(links.items.map(i => i.label))).toEqual(
|
||||
new Set(['Section One', 'Section Two'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sections within the note', async () => {
|
||||
const { uri, content } = await createFile(`
|
||||
# Section 1
|
||||
|
||||
Content of section 1
|
||||
|
||||
# Section 2
|
||||
|
||||
Content of section 2
|
||||
|
||||
[[#
|
||||
`);
|
||||
ws.set(parser.parse(uri, content));
|
||||
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(9, 3)
|
||||
);
|
||||
|
||||
expect(new Set(links.items.map(i => i.label))).toEqual(
|
||||
new Set(['Section 1', 'Section 2'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@ import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const WIKILINK_REGEX = /\[\[[^[\]#]*$/;
|
||||
export const SECTION_REGEX = /\[\[([^[\]]*#)[^[\]]*$/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -18,11 +21,75 @@ const feature: FoamFeature = {
|
||||
mdDocSelector,
|
||||
new CompletionProvider(foam.workspace, foam.graph),
|
||||
'['
|
||||
),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new SectionCompletionProvider(foam.workspace),
|
||||
'#'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace) {}
|
||||
|
||||
provideCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.CompletionList<vscode.CompletionItem>> {
|
||||
const cursorPrefix = document
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
|
||||
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = cursorPrefix.match(SECTION_REGEX);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resourceId =
|
||||
match[1] === '#' ? fromVsCodeUri(document.uri) : match[1].slice(0, -1);
|
||||
|
||||
const resource = this.ws.find(resourceId);
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
cursorPrefix.lastIndexOf('#') + 1,
|
||||
position.line,
|
||||
position.character
|
||||
);
|
||||
if (resource) {
|
||||
const items = resource.sections.map(b => {
|
||||
const item = new ResourceCompletionItem(
|
||||
b.label,
|
||||
vscode.CompletionItemKind.Text,
|
||||
resource.uri.withFragment(b.label)
|
||||
);
|
||||
item.sortText = String(b.range.start.line).padStart(5, '0');
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
});
|
||||
return new vscode.CompletionList(items);
|
||||
}
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
item: ResourceCompletionItem | vscode.CompletionItem
|
||||
): vscode.ProviderResult<vscode.CompletionItem> {
|
||||
if (item instanceof ResourceCompletionItem) {
|
||||
return this.ws.readAsMarkdown(item.resourceUri).then(text => {
|
||||
item.documentation = getNoteTooltip(text);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
export class CompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
|
||||
@@ -38,34 +105,71 @@ export class CompletionProvider
|
||||
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
|
||||
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const requiresAutocomplete = cursorPrefix.match(/\[\[[^\[\]]*(?!.*\]\])/);
|
||||
const requiresAutocomplete = cursorPrefix.match(WIKILINK_REGEX);
|
||||
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = requiresAutocomplete[0];
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
position.character - (text.length - 2),
|
||||
position.line,
|
||||
position.character
|
||||
);
|
||||
const resources = this.ws.list().map(resource => {
|
||||
const item = new vscode.CompletionItem(
|
||||
vscode.workspace.asRelativePath(toVsCodeUri(resource.uri)),
|
||||
vscode.CompletionItemKind.File
|
||||
const label = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
|
||||
const item = new ResourceCompletionItem(
|
||||
label,
|
||||
vscode.CompletionItemKind.File,
|
||||
resource.uri
|
||||
);
|
||||
item.insertText = URI.getBasename(resource.uri);
|
||||
item.documentation = getNoteTooltip(resource.source.text);
|
||||
|
||||
item.filterText = resource.uri.getName();
|
||||
item.insertText = this.ws.getIdentifier(resource.uri);
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = ['#'];
|
||||
return item;
|
||||
});
|
||||
|
||||
const placeholders = Array.from(this.graph.placeholders.values()).map(
|
||||
uri => {
|
||||
return new vscode.CompletionItem(
|
||||
const item = new vscode.CompletionItem(
|
||||
uri.path,
|
||||
vscode.CompletionItemKind.Interface
|
||||
);
|
||||
item.insertText = uri.path;
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
}
|
||||
);
|
||||
|
||||
return new vscode.CompletionList([...resources, ...placeholders]);
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
item: ResourceCompletionItem | vscode.CompletionItem
|
||||
): vscode.ProviderResult<vscode.CompletionItem> {
|
||||
if (item instanceof ResourceCompletionItem) {
|
||||
return this.ws.readAsMarkdown(item.resourceUri).then(text => {
|
||||
item.documentation = getNoteTooltip(text);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A CompletionItem related to a Resource
|
||||
*/
|
||||
class ResourceCompletionItem extends vscode.CompletionItem {
|
||||
constructor(
|
||||
label: string,
|
||||
type: vscode.CompletionItemKind,
|
||||
public resourceUri: URI
|
||||
) {
|
||||
super(label, type);
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
237
packages/foam-vscode/src/features/navigation-provider.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { createTestWorkspace } from '../test/test-utils';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { NavigationProvider } from './navigation-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
|
||||
describe('Document navigation', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
});
|
||||
describe('Document links provider', () => {
|
||||
it('should not return any link for empty documents', async () => {
|
||||
const { uri, content } = await createFile('');
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not return any link for documents without links', async () => {
|
||||
const { uri, content } = await createFile(
|
||||
'This is some content without links'
|
||||
);
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should create links for wikilinks', async () => {
|
||||
const fileA = await createFile('# File A', ['file-a.md']);
|
||||
const fileB = await createFile(`this is a link to [[${fileA.name}]].`);
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileA.uri, fileB.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileA.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 26));
|
||||
});
|
||||
|
||||
it('should create links for placeholders', async () => {
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
const ws = new FoamWorkspace().set(
|
||||
parser.parse(fileA.uri, fileA.content)
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(URI.placeholder('a placeholder'))
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));
|
||||
});
|
||||
});
|
||||
|
||||
describe('definition provider', () => {
|
||||
it('should not create a definition for a placeholder', async () => {
|
||||
const fileA = await createFile(`this is a link to [[placeholder]].`);
|
||||
const ws = createTestWorkspace().set(
|
||||
parser.parse(fileA.uri, fileA.content)
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const definitions = await provider.provideDefinition(
|
||||
doc,
|
||||
new vscode.Position(0, 22)
|
||||
);
|
||||
|
||||
expect(definitions).toBeUndefined();
|
||||
});
|
||||
it('should create a definition for a wikilink', async () => {
|
||||
const fileA = await createFile('# File A');
|
||||
const fileB = await createFile(`this is a link to [[${fileA.name}]].`);
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const definitions = await provider.provideDefinition(
|
||||
doc,
|
||||
new vscode.Position(0, 22)
|
||||
);
|
||||
|
||||
expect(definitions.length).toEqual(1);
|
||||
expect(definitions[0].targetUri).toEqual(toVsCodeUri(fileA.uri));
|
||||
// target the whole file
|
||||
expect(definitions[0].targetRange).toEqual(new vscode.Range(0, 0, 0, 8));
|
||||
// select nothing
|
||||
expect(definitions[0].targetSelectionRange).toEqual(
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a definition for a regular link', async () => {
|
||||
const fileA = await createFile('# File A');
|
||||
const fileB = await createFile(
|
||||
`this is a link to [a file](./${fileA.base}).`
|
||||
);
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
|
||||
const definitions = await provider.provideDefinition(
|
||||
doc,
|
||||
new vscode.Position(0, 22)
|
||||
);
|
||||
|
||||
expect(definitions.length).toEqual(1);
|
||||
expect(definitions[0].targetUri).toEqual(toVsCodeUri(fileA.uri));
|
||||
});
|
||||
|
||||
it('should support wikilinks that have an alias', async () => {
|
||||
const fileA = await createFile("# File A that's aliased");
|
||||
const fileB = await createFile(
|
||||
`this is a link to [[${fileA.name}|alias]].`
|
||||
);
|
||||
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const definitions = await provider.provideDefinition(
|
||||
doc,
|
||||
new vscode.Position(0, 22)
|
||||
);
|
||||
|
||||
expect(definitions.length).toEqual(1);
|
||||
expect(definitions[0].targetUri).toEqual(toVsCodeUri(fileA.uri));
|
||||
});
|
||||
|
||||
it('should support wikilink aliases in tables using escape character', async () => {
|
||||
const fileA = await createFile('# File that has to be aliased');
|
||||
const fileB = await createFile(`
|
||||
| Col A | ColB |
|
||||
| --- | --- |
|
||||
| [[${fileA.name}\\|alias]] | test |
|
||||
`);
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const definitions = await provider.provideDefinition(
|
||||
doc,
|
||||
new vscode.Position(3, 10)
|
||||
);
|
||||
|
||||
expect(definitions.length).toEqual(1);
|
||||
expect(definitions[0].targetUri).toEqual(toVsCodeUri(fileA.uri));
|
||||
});
|
||||
});
|
||||
|
||||
describe('reference provider', () => {
|
||||
it('should provide references for wikilinks', async () => {
|
||||
const fileA = await createFile('The content of File A');
|
||||
const fileB = await createFile(
|
||||
`File B is connected to [[${fileA.name}]] and has a [[placeholder]].`
|
||||
);
|
||||
const fileC = await createFile(
|
||||
`File C is also connected to [[${fileA.name}]].`
|
||||
);
|
||||
const fileD = await createFile(`File C has a [[placeholder]].`);
|
||||
|
||||
const ws = createTestWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content))
|
||||
.set(parser.parse(fileC.uri, fileC.content))
|
||||
.set(parser.parse(fileD.uri, fileD.content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(fileB.uri);
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
|
||||
const refs = await provider.provideReferences(
|
||||
doc,
|
||||
new vscode.Position(0, 26)
|
||||
);
|
||||
expect(refs.length).toEqual(2);
|
||||
expect(refs[0]).toEqual({
|
||||
uri: toVsCodeUri(fileB.uri),
|
||||
range: new vscode.Range(0, 23, 0, 23 + 9),
|
||||
});
|
||||
});
|
||||
it('should provide references for placeholders', async () => {});
|
||||
});
|
||||
});
|
||||
175
packages/foam-vscode/src/features/navigation-provider.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Resource, ResourceLink, ResourceParser } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const navigationProvider = new NavigationProvider(
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
foam.services.parser
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDefinitionProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerReferenceProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides navigation and references for Foam links.
|
||||
* - We create definintions for existing wikilinks but not placeholders
|
||||
* - We create links for both
|
||||
* - We create references for both
|
||||
*
|
||||
* Placeholders are created as links so that when clicking on them a new note will be created.
|
||||
* Definitions are automatically invoked by VS Code on hover, whereas links require
|
||||
* the user to explicitly clicking - and we want the note creation to be explicit.
|
||||
*
|
||||
* Also see https://github.com/foambubble/foam/pull/724
|
||||
*/
|
||||
export class NavigationProvider
|
||||
implements
|
||||
vscode.DefinitionProvider,
|
||||
vscode.DocumentLinkProvider,
|
||||
vscode.ReferenceProvider {
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
private parser: ResourceParser
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Provide references for links and placholders
|
||||
*/
|
||||
public provideReferences(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.Location[]> {
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
const targetLink: ResourceLink | undefined = resource.links.find(link =>
|
||||
Range.containsPosition(link.range, position)
|
||||
);
|
||||
if (!targetLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = this.workspace.resolveLink(resource, targetLink);
|
||||
|
||||
return this.graph.getBacklinks(uri).map(connection => {
|
||||
return new vscode.Location(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(connection.link.range)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create definitions for resolved links
|
||||
*/
|
||||
public provideDefinition(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.LocationLink[] {
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
const targetLink: ResourceLink | undefined = resource.links.find(link =>
|
||||
Range.containsPosition(link.range, position)
|
||||
);
|
||||
if (!targetLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = this.workspace.resolveLink(resource, targetLink);
|
||||
if (uri.isPlaceholder()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetResource = this.workspace.get(uri);
|
||||
|
||||
let targetRange = Range.createFromPosition(
|
||||
targetResource.source.contentStart,
|
||||
targetResource.source.end
|
||||
);
|
||||
const section = Resource.findSection(targetResource, uri.fragment);
|
||||
if (section) {
|
||||
targetRange = section.range;
|
||||
}
|
||||
const result: vscode.LocationLink = {
|
||||
originSelectionRange: toVsCodeRange(targetLink.range),
|
||||
targetUri: toVsCodeUri(uri),
|
||||
targetRange: toVsCodeRange(targetRange),
|
||||
targetSelectionRange: toVsCodeRange(
|
||||
Range.createFromPosition(targetRange.start, targetRange.start)
|
||||
),
|
||||
};
|
||||
return [result];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create links for wikilinks and placeholders
|
||||
*/
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
|
||||
const targets: { link: ResourceLink; target: URI }[] = resource.links.map(
|
||||
link => ({
|
||||
link,
|
||||
target: this.workspace.resolveLink(resource, link),
|
||||
})
|
||||
);
|
||||
|
||||
return targets.map(o => {
|
||||
const command = OPEN_COMMAND.asURI(o.target);
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
o.link.range.start.line,
|
||||
o.link.range.start.character + 2,
|
||||
o.link.range.end.line,
|
||||
o.link.range.end.character - 2
|
||||
),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = o.target.isPlaceholder()
|
||||
? `Create note for '${o.target.path}'`
|
||||
: `Go to ${o.target.toFsPath()}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { focusNote } from '../utils';
|
||||
@@ -10,9 +9,7 @@ const feature: FoamFeature = {
|
||||
commands.registerCommand('foam-vscode.open-random-note', async () => {
|
||||
const foam = await foamPromise;
|
||||
const currentFile = window.activeTextEditor?.document.uri.path;
|
||||
const notes = foam.workspace
|
||||
.list()
|
||||
.filter(r => URI.isMarkdownFile(r.uri));
|
||||
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
|
||||
if (notes.length <= 1) {
|
||||
window.showInformationMessage(
|
||||
'Could not find another note to open. If you believe this is a bug, please file an issue.'
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../utils/grouped-resources-tree-data-provider';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -17,8 +18,8 @@ const feature: FoamFeature = {
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(
|
||||
dir => dir.uri
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
|
||||
fromVsCodeUri(dir.uri)
|
||||
);
|
||||
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
@@ -28,7 +29,7 @@ const feature: FoamFeature = {
|
||||
workspacesURIs,
|
||||
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
|
||||
uri => {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../utils/grouped-resources-tree-data-provider';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -16,8 +17,8 @@ const feature: FoamFeature = {
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(
|
||||
dir => dir.uri
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
|
||||
fromVsCodeUri(dir.uri)
|
||||
);
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'placeholders',
|
||||
@@ -29,7 +30,7 @@ const feature: FoamFeature = {
|
||||
.getAllNodes()
|
||||
.filter(uri => isPlaceholderResource(uri, foam.workspace)),
|
||||
uri => {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
@@ -53,7 +54,7 @@ const feature: FoamFeature = {
|
||||
export default feature;
|
||||
|
||||
export function isPlaceholderResource(uri: URI, workspace: FoamWorkspace) {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
} from '../test/test-utils-vscode';
|
||||
import {
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithFoamTags,
|
||||
@@ -10,7 +15,9 @@ import {
|
||||
|
||||
describe('Link generation in preview', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
uri: './path/to/note-a.md',
|
||||
// TODO: this should really just be the workspace folder, use that once #806 is fixed
|
||||
root: getUriInWorkspace('just-a-ref.md'),
|
||||
title: 'My note title',
|
||||
links: [{ slug: 'placeholder' }],
|
||||
});
|
||||
@@ -19,9 +26,7 @@ describe('Link generation in preview', () => {
|
||||
|
||||
it('generates a link to a note', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='${URI.toFsPath(
|
||||
noteA.uri
|
||||
)}' data-href='${URI.toFsPath(noteA.uri)}'>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'>note-a</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,12 +44,7 @@ describe('Link generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Stylable tag generation in preview', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
title: 'Note B',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteB);
|
||||
const md = markdownItWithFoamTags(MarkdownIt(), ws);
|
||||
const md = markdownItWithFoamTags(MarkdownIt(), new FoamWorkspace());
|
||||
|
||||
it('transforms a string containing multiple tags to a stylable html element', () => {
|
||||
expect(md.render(`Lorem #ipsum dolor #sit`)).toMatch(
|
||||
@@ -60,25 +60,14 @@ describe('Stylable tag generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const noteC = createTestNote({
|
||||
uri: 'note-c.md',
|
||||
text: 'This is the text of note C which includes ![[note-d]]',
|
||||
});
|
||||
const noteD = createTestNote({
|
||||
uri: 'note-d.md',
|
||||
text: 'This is the text of note D which includes ![[note-c]]',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteC)
|
||||
.set(noteD);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
it('should render an included note', () => {
|
||||
const note = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(note);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
it('renders an included note', () => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
@@ -90,20 +79,62 @@ describe('Displaying included notes in preview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the syntax when a note is not found', () => {
|
||||
it('should render an included section', async () => {
|
||||
// here we use createFile as the test note doesn't fill in
|
||||
// all the metadata we need
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note D
|
||||
|
||||
# Section 2
|
||||
This is the second section of note D
|
||||
|
||||
# Section 3
|
||||
This is the third section of note D
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
![[note-b]]`)
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 2]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.
|
||||
![[note-b]]</p>
|
||||
`
|
||||
`<p>This is the root node.</p>
|
||||
<p><h1>Section 2</h1>
|
||||
<p>This is the second section of note D</p>
|
||||
</p>`
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), new FoamWorkspace());
|
||||
|
||||
expect(md.render(`This is the root node. ![[non-existing-note]]`)).toMatch(
|
||||
`<p>This is the root node. ![[non-existing-note]]</p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a warning in case of cyclical inclusions', () => {
|
||||
expect(md.render(noteD.source.text)).toMatch(
|
||||
`<p>This is the text of note D which includes <p>This is the text of note C which includes <p>This is the text of note D which includes <div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: note-c</div></p>
|
||||
it('should display a warning in case of cyclical inclusions', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A which includes ![[note-b]]',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
text: 'This is the text of note B which includes ![[note-a]]',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(md.render(noteB.source.text)).toMatch(
|
||||
`<p>This is the text of note B which includes <p>This is the text of note A which includes <p>This is the text of note B which includes <div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: note-a</div></p>
|
||||
</p>
|
||||
</p>
|
||||
`
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNone } from '../utils';
|
||||
import { isNone, isSome } from '../utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
const refsStack: string[] = [];
|
||||
@@ -45,21 +46,32 @@ export const markdownItWithNoteInclusion = (
|
||||
return `![[${wikilink}]]`;
|
||||
}
|
||||
|
||||
const cyclicLinkDetected = refsStack.includes(wikilink);
|
||||
const cyclicLinkDetected = refsStack.includes(
|
||||
includedNote.uri.path.toLocaleLowerCase()
|
||||
);
|
||||
|
||||
if (!cyclicLinkDetected) {
|
||||
refsStack.push(wikilink.toLowerCase());
|
||||
refsStack.push(includedNote.uri.path.toLocaleLowerCase());
|
||||
}
|
||||
|
||||
const html = cyclicLinkDetected
|
||||
? `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`
|
||||
: md.render(includedNote.source.text);
|
||||
|
||||
if (!cyclicLinkDetected) {
|
||||
if (cyclicLinkDetected) {
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
} else {
|
||||
let content = includedNote.source.text;
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
}
|
||||
|
||||
return html;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
@@ -94,11 +106,8 @@ export const markdownItWithFoamLinks = (
|
||||
? wikilink.substr(wikilink.indexOf('|') + 1)
|
||||
: wikilink;
|
||||
|
||||
return `<a class='foam-note-link' title='${
|
||||
resource.title
|
||||
}' href='${URI.toFsPath(resource.uri)}' data-href='${URI.toFsPath(
|
||||
resource.uri
|
||||
)}'>${linkLabel}</a>`;
|
||||
const link = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='/${link}' data-href='/${link}'>${linkLabel}</a>`;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { TagCompletionProvider } from './tag-completion';
|
||||
|
||||
describe('Tag Completion', () => {
|
||||
const root = vscode.workspace.workspaceFolders[0].uri;
|
||||
const root = fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri);
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(
|
||||
createTestNote({
|
||||
@@ -78,4 +79,18 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a wikilink', async () => {
|
||||
const { uri } = await createFile('[[#prim');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 7)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { SECTION_REGEX } from './link-completion';
|
||||
|
||||
export const TAG_REGEX = /#(.*)/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -32,7 +35,8 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(/#(.*)/);
|
||||
const requiresAutocomplete =
|
||||
cursorPrefix.match(TAG_REGEX) && !cursorPrefix.match(SECTION_REGEX);
|
||||
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagReference, TagsProvider } from '.';
|
||||
import { bootstrap, Foam } from '../../core/model/foam';
|
||||
import { createConfigFromFolders, FoamConfig } from '../../core/config';
|
||||
import { MarkdownResourceProvider } from '../../core/markdown-provider';
|
||||
import { FileDataStore, Matcher } from '../../core/services/datastore';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../test/test-utils-vscode';
|
||||
import { TagItem, TagReference, TagsProvider } from './tags-tree-view';
|
||||
import { bootstrap, Foam } from '../core/model/foam';
|
||||
import { MarkdownResourceProvider } from '../core/markdown-provider';
|
||||
import { FileDataStore, Matcher } from '../core/services/datastore';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const config: FoamConfig = createConfigFromFolders([]);
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
const matcher = new Matcher([]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
@@ -29,7 +22,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
_foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
@@ -48,7 +41,7 @@ describe('Tags tree panel', () => {
|
||||
|
||||
const treeItems = (await provider.getChildren()) as TagItem[];
|
||||
|
||||
treeItems.map(item => expect(item.tag).toContain('test'));
|
||||
treeItems.forEach(item => expect(item.tag).toContain('test'));
|
||||
});
|
||||
|
||||
it('correctly handles a parent and child tag', async () => {
|
||||
@@ -1,11 +1,11 @@
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { URI } from '../core/model/uri';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getNoteTooltip, isSome } from '../../utils';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Resource, Tag } from '../../core/model/note';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, isSome } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Resource, Tag } from '../core/model/note';
|
||||
|
||||
const TAG_SEPARATOR = '/';
|
||||
const feature: FoamFeature = {
|
||||
@@ -1,54 +1,65 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createNoteForPlaceholderWikilink } from './create-from-template';
|
||||
import { fromVsCodeUri, toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { NoteFactory } from '../services/templates';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
export const OPEN_COMMAND = {
|
||||
command: 'foam-vscode.open-resource',
|
||||
title: 'Foam: Open Resource',
|
||||
|
||||
execute: async (params: { uri: URI }) => {
|
||||
const { uri } = params;
|
||||
switch (uri.scheme) {
|
||||
case 'file':
|
||||
return vscode.commands.executeCommand('vscode.open', toVsCodeUri(uri));
|
||||
|
||||
case 'placeholder':
|
||||
const title = uri.path.split('/').slice(-1)[0];
|
||||
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? vscode.workspace.workspaceFolders[0].uri
|
||||
: vscode.window.activeTextEditor?.document.uri
|
||||
? URI.getDir(vscode.window.activeTextEditor!.document.uri)
|
||||
: undefined;
|
||||
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = toVsCodeUri(
|
||||
URI.createResourceUriFromPlaceholder(basedir, uri)
|
||||
);
|
||||
|
||||
await createNoteForPlaceholderWikilink(title, target);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
asURI: (uri: URI) =>
|
||||
vscode.Uri.parse(`command:${OPEN_COMMAND.command}`).with({
|
||||
query: encodeURIComponent(JSON.stringify({ uri: URI.create(uri) })),
|
||||
query: encodeURIComponent(JSON.stringify({ uri })),
|
||||
}),
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext) => {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
OPEN_COMMAND.command,
|
||||
OPEN_COMMAND.execute
|
||||
async (params: { uri: URI }) => {
|
||||
const uri = new URI(params.uri);
|
||||
switch (uri.scheme) {
|
||||
case 'file':
|
||||
let selection = new vscode.Range(1, 0, 1, 0);
|
||||
if (uri.fragment) {
|
||||
const foam = await foamPromise;
|
||||
const resource = foam.workspace.get(uri);
|
||||
const section = Resource.findSection(resource, uri.fragment);
|
||||
if (section) {
|
||||
selection = toVsCodeRange(section.range);
|
||||
}
|
||||
}
|
||||
const targetUri =
|
||||
uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(uri);
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri, {
|
||||
selection: selection,
|
||||
});
|
||||
|
||||
case 'placeholder':
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? vscode.workspace.workspaceFolders[0].uri
|
||||
: vscode.window.activeTextEditor?.document.uri
|
||||
? vscode.window.activeTextEditor!.document.uri
|
||||
: undefined;
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
const title = uri.getName();
|
||||
const target = fromVsCodeUri(basedir)
|
||||
.resolve(uri, true)
|
||||
.changeExtension('', '.md');
|
||||
await NoteFactory.createForPlaceholderWikilink(title, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||