mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cff13e056 | ||
|
|
53c1a79ddd | ||
|
|
a840853666 | ||
|
|
47842cb618 | ||
|
|
455124513c | ||
|
|
1e00bbe8bd | ||
|
|
dda14ba9e7 | ||
|
|
129482a43e | ||
|
|
0c1c4da154 | ||
|
|
7f4b700b21 | ||
|
|
686e05ed25 | ||
|
|
b2c7ecbb3d | ||
|
|
2c643e0c63 | ||
|
|
3b33d3d696 | ||
|
|
87633e68b1 | ||
|
|
6c7b558f36 | ||
|
|
12037704d7 | ||
|
|
e549fb8c21 | ||
|
|
ac7d3243c4 | ||
|
|
748df5e352 | ||
|
|
dcd46f1378 | ||
|
|
f9f751a27a | ||
|
|
0764da0dd6 | ||
|
|
f747d7445a | ||
|
|
eb74e57a9e | ||
|
|
a01cf8ec8d | ||
|
|
5b63fa8108 | ||
|
|
ddf7ddf7b3 | ||
|
|
4b263667ea | ||
|
|
309194b3c3 | ||
|
|
c4f35b7649 | ||
|
|
b9e18de7e7 | ||
|
|
23cf5a021e | ||
|
|
8231ed14c5 | ||
|
|
3bea283c04 | ||
|
|
a3cffe8418 | ||
|
|
675e7fa216 | ||
|
|
87d12bf3af | ||
|
|
e118ab74b5 | ||
|
|
04a61eed0e | ||
|
|
350b3005f1 | ||
|
|
f7293b1eb4 | ||
|
|
672eb6ed20 | ||
|
|
37a9bc49bc | ||
|
|
38741ca52e | ||
|
|
ed762618ed | ||
|
|
21a32382a2 | ||
|
|
7e6c041b87 | ||
|
|
c9a0a1d53c | ||
|
|
0516088656 | ||
|
|
f98ff336bf | ||
|
|
1b1396d949 | ||
|
|
ebaab2ee59 | ||
|
|
c6a754f1a8 | ||
|
|
3fb35494d4 | ||
|
|
a7af7689a4 | ||
|
|
5b7a2ab022 | ||
|
|
88227d4028 | ||
|
|
a531c9f9cd |
@@ -851,6 +851,69 @@
|
||||
"contributions": [
|
||||
"tool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chrisUsick",
|
||||
"name": "Chris Usick",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6589365?v=4",
|
||||
"profile": "http://cu-dev.ca",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "josephdecock",
|
||||
"name": "Joe DeCock",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1145533?v=4",
|
||||
"profile": "https://github.com/josephdecock",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "drewtyler",
|
||||
"name": "Drew Tyler",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5640816?v=4",
|
||||
"profile": "http://www.drewtyler.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Lauviah0622",
|
||||
"name": "Lauviah0622",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43416399?v=4",
|
||||
"profile": "https://github.com/Lauviah0622",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joshdover",
|
||||
"name": "Josh Dover",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1813008?v=4",
|
||||
"profile": "https://www.elastic.co/elastic-agent",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "phelma",
|
||||
"name": "Phil Helm",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4057948?v=4",
|
||||
"profile": "http://phelm.co.uk",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lingyv-li",
|
||||
"name": "Larry Li",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8937944?v=4",
|
||||
"profile": "https://github.com/lingyv-li",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -28,6 +28,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
// Restrict usage of fs module outside tests to keep foam compatible with the browser
|
||||
"files": ["**/src/**"],
|
||||
"excludedFiles": ["**/src/test/**", "**/src/**/*{test,spec}.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"name": "fs",
|
||||
"message": "Extension code must not rely Node.js filesystem, use vscode.workspace.fs instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"import/core-modules": ["vscode"],
|
||||
"import/parsers": {
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
@@ -39,6 +40,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
@@ -60,4 +62,4 @@ jobs:
|
||||
- name: Run Tests
|
||||
uses: GabrielBB/xvfb-action@v1.4
|
||||
with:
|
||||
run: yarn test
|
||||
run: yarn test --stream
|
||||
|
||||
BIN
assets/screenshots/feature-link-sync.gif
Normal file
BIN
assets/screenshots/feature-link-sync.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 934 KiB |
@@ -1,7 +1,7 @@
|
||||
# Creating New Notes
|
||||
|
||||
- Write out a new `[[wikilink]]` and `Cmd` + `Click` to create a new file and enter it.
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap the 'editor.action.revealDefinition' key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create New Note` and enter a **Title Case Name** to create `Title Case Name.md`
|
||||
- Add a keyboard binding to make creating new notes easier.
|
||||
- The [[note-templates]] used by this command can be customized.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
- Ensure that you have all the [[recommended-extensions]] installed in Visual Studio Code
|
||||
- Reload Visual Studio Code by running `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "reload" and run the **Developer: Reload Window** command to for the updated extensions take effect
|
||||
- Check the formatting rules for links on [[foam-file-format]], [[wikilinks]] and [[link-formatting-and-autocompletion]]
|
||||
- Check the formatting rules for links on [[foam-file-format]] and [[wikilinks]]
|
||||
|
||||
## I don't want Foam enabled for all my workspaces
|
||||
Any extension you install in Visual Studio Code is enabled by default. Give the philosophy of Foam it works out of the box without doing any configuration upfront. In case you want to disable Foam for a specific workspace, or disable Foam by default and enable it for specific workspaces, it is advised to follow the best practices as [documented by Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-marketplace#_manage-extensions)
|
||||
|
||||
@@ -31,8 +31,6 @@ You can use **Foam** for organising your research, keeping re-discoverable notes
|
||||
|
||||
**Foam** is a tool that supports creating relationships between thoughts and information to help you think better.
|
||||
|
||||

|
||||
|
||||
Whether you want to build a [Second Brain](https://www.buildingasecondbrain.com/) or a [Zettelkasten](https://zettelkasten.de/posts/overview/), write a book, or just get better at long-term learning, **Foam** can help you organise your thoughts if you follow these simple rules:
|
||||
|
||||
1. Create a single **Foam** workspace for all your knowledge and research following the [Getting started](#getting-started) guide.
|
||||
@@ -227,6 +225,15 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://Cliffordfajardo.com"><img src="https://avatars.githubusercontent.com/u/6743796?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Clifford Fajardo </b></sub></a><br /><a href="#tool-cliffordfajardo" title="Tools">🔧</a></td>
|
||||
<td align="center"><a href="http://cu-dev.ca"><img src="https://avatars.githubusercontent.com/u/6589365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Chris Usick</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chrisUsick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/josephdecock"><img src="https://avatars.githubusercontent.com/u/1145533?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe DeCock</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=josephdecock" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.drewtyler.com"><img src="https://avatars.githubusercontent.com/u/5640816?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Drew Tyler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=drewtyler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Lauviah0622"><img src="https://avatars.githubusercontent.com/u/43416399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Lauviah0622</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lauviah0622" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.elastic.co/elastic-agent"><img src="https://avatars.githubusercontent.com/u/1813008?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Josh Dover</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joshdover" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://phelm.co.uk"><img src="https://avatars.githubusercontent.com/u/4057948?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Phil Helm</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=phelma" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/lingyv-li"><img src="https://avatars.githubusercontent.com/u/8937944?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Larry Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lingyv-li" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
|
||||
- Quick commits with VS Code's built in [[git-integration]]
|
||||
- Store your workspace in an auto-synced GitHub repo with [[write-your-notes-in-github-gist]]
|
||||
- Sync your GitHub repo automatically [[todo]].
|
||||
- Sync your GitHub repo automatically using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc).
|
||||
|
||||
## Publish
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Foam enables you to Link pages together using `[[file-name]]` annotations (i.e. `[[MediaWiki]]` links).
|
||||
|
||||
- Type `[[` and start typing a file name for autocompletion.
|
||||
- See [[link-formatting-and-autocompletion]] for more information, and how to setup your link autocompletions to make this easier.
|
||||
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on file name to navigate to file (`F12` also works while your cursor is on the file name)
|
||||
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on non-existent file to create that file in the workspace.
|
||||
- The note creation makes use of the special [`new-note.md` note template](features/note-templates)
|
||||
@@ -22,7 +21,6 @@ The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.f
|
||||
|
||||
- [[foam-file-format]]
|
||||
- [[note-templates]]
|
||||
- [[link-formatting-and-autocompletion]]
|
||||
- See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.17.8"
|
||||
"version": "0.19.1"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "lerna run test",
|
||||
"test": "yarn workspace foam-vscode test",
|
||||
"lint": "lerna run lint",
|
||||
"watch": "lerna run watch --concurrency 20 --stream"
|
||||
},
|
||||
@@ -38,4 +38,4 @@
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,5 @@ vsc-extension-quickstart.md
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
assets/screenshots
|
||||
node_modules
|
||||
|
||||
@@ -4,6 +4,61 @@ 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.19.1] - 2022-07-11
|
||||
|
||||
Internal:
|
||||
- Introduced cache for markdown parser (#1030)
|
||||
- Various code refactorings
|
||||
|
||||
## [0.19.0] - 2022-07-07
|
||||
|
||||
New Features:
|
||||
- Support for attachments (PDF) and images (#1027)
|
||||
- Support for opening day notes for other days as well (#1026, thanks @alper)
|
||||
|
||||
## [0.18.5] - 2022-06-29
|
||||
|
||||
Fixes and Improvements:
|
||||
- Support for `alias` YAML property to define note alias (#1014 - thanks @lingyv-li)
|
||||
|
||||
Internal:
|
||||
- Improved extension bundling (#1015 - thanks @lingyv-li)
|
||||
- Use `vscode.workspace.fs` instead of `fs` (#1005 - thanks @joshdover)
|
||||
|
||||
## [0.18.4] - 2022-06-03
|
||||
|
||||
Fixes and Improvements:
|
||||
- move past `]]` when writing wikilinks (#998 - thanks @Lauviah0622)
|
||||
- highlight improvements (#890 - thanks @memeplex)
|
||||
|
||||
## [0.18.3] - 2022-04-17
|
||||
|
||||
Fixes and Improvements:
|
||||
- Better reporting when links fail to resolve
|
||||
- Failing link resolution during graph computation no longer fatal
|
||||
|
||||
## [0.18.2] - 2022-04-14
|
||||
|
||||
Fixes and Improvements:
|
||||
- Fixed parsing error on empty direct links (#980 - thanks @chrisUsick)
|
||||
- Improved rendering in preview of wikilinks that have link definitions (#979 - thanks @josephdecock)
|
||||
- Restored handling of section-only wikilinks (#981)
|
||||
|
||||
## [0.18.1] - 2022-04-13
|
||||
|
||||
Fixes and Improvements:
|
||||
- Fixed parsing error for direct links with square brackets in them (#977)
|
||||
- Improved markdown direct link resolution (#972)
|
||||
- Improved templates support for custom paths (#970)
|
||||
|
||||
## [0.18.0] - 2022-04-11
|
||||
|
||||
Features:
|
||||
- Link synchronization on file rename
|
||||
|
||||
Internal:
|
||||
- Changed graph computation on workspace change to simplify code
|
||||
|
||||
## [0.17.8] - 2022-04-01
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -27,6 +27,12 @@ Foam helps you create the connections between your notes, and your placeholders
|
||||
|
||||

|
||||
|
||||
### Sync links on file rename
|
||||
|
||||
Foam updates the links to renamed files, so your notes stay consistent.
|
||||
|
||||

|
||||
|
||||
### Unique identifiers across directories
|
||||
|
||||
Foam supports files with the same name in multiple directories.
|
||||
|
||||
BIN
packages/foam-vscode/assets/screenshots/feature-link-sync.gif
Normal file
BIN
packages/foam-vscode/assets/screenshots/feature-link-sync.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 934 KiB |
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.17.8",
|
||||
"version": "0.19.1",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -23,6 +23,7 @@
|
||||
"onView:foam-vscode.tags-explorer",
|
||||
"onCommand:foam-vscode.update-wikilinks",
|
||||
"onCommand:foam-vscode.open-daily-note",
|
||||
"onCommand:foam-vscode.update-graph",
|
||||
"onCommand:foam-vscode.open-random-note",
|
||||
"onCommand:foam-vscode.janitor",
|
||||
"onCommand:foam-vscode.copy-without-brackets",
|
||||
@@ -62,25 +63,25 @@
|
||||
{
|
||||
"id": "foam-vscode.backlinks",
|
||||
"name": "Backlinks",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(references)",
|
||||
"contextualTitle": "Backlinks"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.tags-explorer",
|
||||
"name": "Tag Explorer",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(tag)",
|
||||
"contextualTitle": "Tags Explorer"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.orphans",
|
||||
"name": "Orphans",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(debug-gripper)",
|
||||
"contextualTitle": "Orphans"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.placeholders",
|
||||
"name": "Placeholders",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(debug-disconnect)",
|
||||
"contextualTitle": "Placeholders"
|
||||
}
|
||||
]
|
||||
@@ -127,6 +128,10 @@
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "foam-vscode.update-graph",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-orphans-by-folder",
|
||||
"when": "false"
|
||||
@@ -146,10 +151,22 @@
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.completion-move-cursor",
|
||||
"when": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "foam-vscode.clear-cache",
|
||||
"title": "Foam: Clear Cache"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.update-graph",
|
||||
"title": "Foam: Update graph"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.set-log-level",
|
||||
"title": "Foam: Set log level"
|
||||
@@ -164,6 +181,10 @@
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"title": "Foam: Open Today's Note"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note-for-date",
|
||||
"title": "Foam: Open Daily Note"
|
||||
},
|
||||
{
|
||||
@@ -213,6 +234,10 @@
|
||||
{
|
||||
"command": "foam-vscode.create-new-template",
|
||||
"title": "Foam: Create New Template"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.completion-move-cursor",
|
||||
"title": "Foam: Move cursor after completion"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
@@ -255,13 +280,13 @@
|
||||
"Disable wikilink definitions generation"
|
||||
]
|
||||
},
|
||||
"foam.links.hover.enable": {
|
||||
"description": "Enable displaying note content on hover links",
|
||||
"foam.links.sync.enable": {
|
||||
"description": "Enable synching links when moving/renaming notes",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"foam.decorations.links.enable": {
|
||||
"description": "Enable decorations for links",
|
||||
"foam.links.hover.enable": {
|
||||
"description": "Enable displaying note content on hover links",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
@@ -353,6 +378,11 @@
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.preview.embedNoteInContainer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Wrap embedded notes in a container when displayed in preview panel"
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
"default": 24,
|
||||
@@ -369,6 +399,10 @@
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"key": "alt+d"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note-for-date",
|
||||
"key": "alt+h"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -384,14 +418,13 @@
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
"vscode:prepublish": "yarn npm-install && yarn run build",
|
||||
"npm-install": "rimraf node_modules && npm i",
|
||||
"npm-cleanup": "rimraf package-lock.json node_modules && yarn",
|
||||
"package-extension": "npx vsce package && yarn npm-cleanup",
|
||||
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
|
||||
"vscode:prepublish": "yarn run esbuild-base -- --minify",
|
||||
"package-extension": "npx vsce package --yarn",
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
|
||||
"publish-extension-vscode": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx && yarn npm-cleanup"
|
||||
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^3.0.1",
|
||||
@@ -405,6 +438,7 @@
|
||||
"@types/vscode": "^1.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"esbuild": "^0.14.45",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
@@ -418,7 +452,8 @@
|
||||
"tsdx": "^0.13.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.5",
|
||||
"vscode-test": "^1.3.0"
|
||||
"vscode-test": "^1.3.0",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
@@ -428,6 +463,7 @@
|
||||
"glob": "^7.1.6",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.12.0",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '.';
|
||||
import { Range } from '../model/range';
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { generateHeading } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -20,12 +21,13 @@ describe('generateHeadings', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
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;
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
|
||||
});
|
||||
|
||||
it.skip('should add heading to a file that does not have them', () => {
|
||||
it.skip('should add heading to a file that does not have them', async () => {
|
||||
const note = findBySlug('file-without-title');
|
||||
const expected = {
|
||||
newText: `# File without Title
|
||||
@@ -34,19 +36,25 @@ describe('generateHeadings', () => {
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = detectNewline(noteText);
|
||||
const actual = await generateHeading(note, noteText, noteEol);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
expect(actual!.newText).toEqual(expected.newText);
|
||||
});
|
||||
|
||||
it('should not cause any changes to a file that has a heading', () => {
|
||||
it('should not cause any changes to a file that has a heading', async () => {
|
||||
const note = findBySlug('index');
|
||||
expect(generateHeading(note)).toBeNull();
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = detectNewline(noteText);
|
||||
const actual = await generateHeading(note, noteText, noteEol);
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it.skip('should generate heading when the file only contains frontmatter', () => {
|
||||
it.skip('should generate heading when the file only contains frontmatter', async () => {
|
||||
const note = findBySlug('file-with-only-frontmatter');
|
||||
|
||||
const expected = {
|
||||
@@ -54,7 +62,9 @@ describe('generateHeadings', () => {
|
||||
range: Range.create(3, 0, 3, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = detectNewline(noteText);
|
||||
const actual = await generateHeading(note, noteText, noteEol);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
47
packages/foam-vscode/src/core/janitor/generate-headings.ts
Normal file
47
packages/foam-vscode/src/core/janitor/generate-headings.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import matter from 'gray-matter';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
|
||||
export const generateHeading = async (
|
||||
note: Resource,
|
||||
noteText: string,
|
||||
eol: string
|
||||
): Promise<TextEdit | null> => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO now the note.title defaults to file name at parsing time, so this check
|
||||
// doesn't work anymore. Decide:
|
||||
// - whether do we actually want to continue generate the headings
|
||||
// - whether it should be under a config option
|
||||
// A possible approach would be around having a `sections` field in the note, and inspect
|
||||
// it to see if there is an h1 title. Alternatively parse directly the markdown in this function.
|
||||
if (note.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fm = matter(noteText);
|
||||
const contentStartLine = fm.matter.split(eol).length;
|
||||
const frontmatterExists = contentStartLine > 0;
|
||||
|
||||
let newLineExistsAfterFrontmatter = false;
|
||||
if (frontmatterExists) {
|
||||
const lines = noteText.split(eol);
|
||||
const index = contentStartLine - 1;
|
||||
const line = lines[index];
|
||||
newLineExistsAfterFrontmatter = line === '';
|
||||
}
|
||||
|
||||
const paddingStart = frontmatterExists ? eol : '';
|
||||
const paddingEnd = newLineExistsAfterFrontmatter ? eol : `${eol}${eol}`;
|
||||
|
||||
return {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
note.uri.getName()
|
||||
)}${paddingEnd}`,
|
||||
range: Range.create(contentStartLine, 0, contentStartLine, 0),
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
import { generateLinkReferences } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { EOL } from 'os';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -21,20 +24,23 @@ describe('generateLinkReferences', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
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;
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
const readFile = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_workspace.list().length).toEqual(10);
|
||||
});
|
||||
|
||||
it('should add link references to a file that does not have them', () => {
|
||||
it('should add link references to a file that does not have them', async () => {
|
||||
const note = findBySlug('index');
|
||||
const expected = {
|
||||
newText: textForNote(
|
||||
note,
|
||||
`
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
@@ -44,15 +50,22 @@ describe('generateLinkReferences', () => {
|
||||
),
|
||||
range: Range.create(9, 0, 9, 0),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
expect(actual!.newText).toEqual(expected.newText);
|
||||
});
|
||||
|
||||
it('should remove link definitions from a file that has them, if no links are present', () => {
|
||||
it('should remove link definitions from a file that has them, if no links are present', async () => {
|
||||
const note = findBySlug('second-document');
|
||||
|
||||
const expected = {
|
||||
@@ -60,19 +73,26 @@ describe('generateLinkReferences', () => {
|
||||
range: Range.create(6, 0, 8, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
expect(actual!.newText).toEqual(expected.newText);
|
||||
});
|
||||
|
||||
it('should update link definitions if they are present but changed', () => {
|
||||
it('should update link definitions if they are present but changed', async () => {
|
||||
const note = findBySlug('first-document');
|
||||
|
||||
const expected = {
|
||||
newText: textForNote(
|
||||
note,
|
||||
`[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
@@ -80,28 +100,43 @@ describe('generateLinkReferences', () => {
|
||||
range: Range.create(8, 0, 10, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
expect(actual!.newText).toEqual(expected.newText);
|
||||
});
|
||||
|
||||
it('should not cause any changes if link reference definitions were up to date', () => {
|
||||
it('should not cause any changes if link reference definitions were up to date', async () => {
|
||||
const note = findBySlug('third-document');
|
||||
|
||||
const expected = null;
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should put links with spaces in angel brackets', () => {
|
||||
it('should put links with spaces in angel brackets', async () => {
|
||||
const note = findBySlug('angel-reference');
|
||||
const expected = {
|
||||
newText: textForNote(
|
||||
note,
|
||||
`
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[Note being refered as angel]: <Note being refered as angel> "Note being refered as angel"
|
||||
@@ -110,27 +145,42 @@ describe('generateLinkReferences', () => {
|
||||
range: Range.create(3, 0, 3, 0),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
expect(actual!.newText).toEqual(expected.newText);
|
||||
});
|
||||
|
||||
it('should not remove explicitly entered link references', () => {
|
||||
it('should not remove explicitly entered link references', async () => {
|
||||
const note = findBySlug('file-with-explicit-link-references');
|
||||
const expected = null;
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not remove explicitly entered link references and have an implicit link', () => {
|
||||
it('should not remove explicitly entered link references and have an implicit link', async () => {
|
||||
const note = findBySlug('file-with-explicit-and-implicit-link-references');
|
||||
const expected = {
|
||||
newText: textForNote(
|
||||
note,
|
||||
`[^footerlink]: https://foambubble.github.io/
|
||||
[linkrefenrece]: https://foambubble.github.io/
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
@@ -140,7 +190,15 @@ describe('generateLinkReferences', () => {
|
||||
range: Range.create(5, 0, 10, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = EOL;
|
||||
const actual = await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
_workspace,
|
||||
false
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -154,6 +212,7 @@ describe('generateLinkReferences', () => {
|
||||
* @param note the note we are adjusting for
|
||||
* @param text starting text, using a \n line separator
|
||||
*/
|
||||
function textForNote(note: Resource, text: string): string {
|
||||
return text.split('\n').join(note.source.eol);
|
||||
function textForNote(text: string): string {
|
||||
const eol = EOL;
|
||||
return text.split('\n').join(eol);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../services/markdown-provider';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Position } from '../model/position';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
export const generateLinkReferences = async (
|
||||
note: Resource,
|
||||
text: string,
|
||||
eol: string,
|
||||
workspace: FoamWorkspace,
|
||||
includeExtensions: boolean
|
||||
): Promise<TextEdit | null> => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markdownReferences = createMarkdownReferences(
|
||||
workspace,
|
||||
note.uri,
|
||||
includeExtensions
|
||||
);
|
||||
|
||||
const newReferences =
|
||||
markdownReferences.length === 0
|
||||
? ''
|
||||
: [
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
...markdownReferences.map(stringifyMarkdownLinkReferenceDefinition),
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
].join(eol);
|
||||
|
||||
if (note.definitions.length === 0) {
|
||||
if (newReferences.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = text.split(eol);
|
||||
const end = Position.create(
|
||||
lines.length - 1,
|
||||
lines[lines.length - 1].length
|
||||
);
|
||||
const padding = end.character === 0 ? eol : `${eol}${eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: Range.createFromPosition(end, end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
const last = note.definitions[note.definitions.length - 1];
|
||||
|
||||
let nonGeneratedReferenceDefinitions = note.definitions;
|
||||
|
||||
// if we have more definitions then referenced pages AND the page refers to a page
|
||||
// we expect non-generated link definitions to be present
|
||||
// Collect all non-generated definitions, by removing the generated ones
|
||||
if (
|
||||
note.definitions.length > markdownReferences.length &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
// remove all autogenerated definitions
|
||||
const beginIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//begin'
|
||||
);
|
||||
const endIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//end'
|
||||
);
|
||||
|
||||
const generatedDefinitions = [...note.definitions].splice(
|
||||
beginIndex,
|
||||
endIndex - beginIndex + 1
|
||||
);
|
||||
|
||||
nonGeneratedReferenceDefinitions = note.definitions.filter(
|
||||
x => !generatedDefinitions.includes(x)
|
||||
);
|
||||
}
|
||||
|
||||
// When we only have explicitly defined link definitions &&
|
||||
// no indication of previously defined generated links &&
|
||||
// there is no reference to another page, return null
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
note.definitions.findIndex(({ label }) => label === '//begin') < 0 &&
|
||||
markdownReferences.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format link definitions for non-generated links
|
||||
const nonGeneratedReferences = nonGeneratedReferenceDefinitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(eol);
|
||||
|
||||
const oldReferences = note.definitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(eol);
|
||||
|
||||
// When the newly formatted references match the old ones, OR
|
||||
// when non-generated references are present, but no new ones are generated
|
||||
// return null
|
||||
if (
|
||||
oldReferences === newReferences ||
|
||||
(nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
newReferences === '' &&
|
||||
markdownReferences.length > 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fullReferences = `${newReferences}`;
|
||||
// If there are any non-generated definitions, add those to the output as well
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
fullReferences = `${nonGeneratedReferences}${eol}${newReferences}`;
|
||||
}
|
||||
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${fullReferences}`,
|
||||
range: Range.createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,174 +1,2 @@
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../services/markdown-provider';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export const generateLinkReferences = (
|
||||
note: Resource,
|
||||
workspace: FoamWorkspace,
|
||||
includeExtensions: boolean
|
||||
): TextEdit | null => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markdownReferences = createMarkdownReferences(
|
||||
workspace,
|
||||
note.uri,
|
||||
includeExtensions
|
||||
);
|
||||
|
||||
const newReferences =
|
||||
markdownReferences.length === 0
|
||||
? ''
|
||||
: [
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
...markdownReferences.map(stringifyMarkdownLinkReferenceDefinition),
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
].join(note.source.eol);
|
||||
|
||||
if (note.definitions.length === 0) {
|
||||
if (newReferences.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const padding =
|
||||
note.source.end.character === 0
|
||||
? note.source.eol
|
||||
: `${note.source.eol}${note.source.eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: Range.createFromPosition(note.source.end, note.source.end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
const last = note.definitions[note.definitions.length - 1];
|
||||
|
||||
let nonGeneratedReferenceDefinitions = note.definitions;
|
||||
|
||||
// if we have more definitions then referenced pages AND the page refers to a page
|
||||
// we expect non-generated link definitions to be present
|
||||
// Collect all non-generated definitions, by removing the generated ones
|
||||
if (
|
||||
note.definitions.length > markdownReferences.length &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
// remove all autogenerated definitions
|
||||
const beginIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//begin'
|
||||
);
|
||||
const endIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//end'
|
||||
);
|
||||
|
||||
const generatedDefinitions = [...note.definitions].splice(
|
||||
beginIndex,
|
||||
endIndex - beginIndex + 1
|
||||
);
|
||||
|
||||
nonGeneratedReferenceDefinitions = note.definitions.filter(
|
||||
x => !generatedDefinitions.includes(x)
|
||||
);
|
||||
}
|
||||
|
||||
// When we only have explicitly defined link definitions &&
|
||||
// no indication of previously defined generated links &&
|
||||
// there is no reference to another page, return null
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
note.definitions.findIndex(({ label }) => label === '//begin') < 0 &&
|
||||
markdownReferences.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format link definitions for non-generated links
|
||||
const nonGeneratedReferences = nonGeneratedReferenceDefinitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(note.source.eol);
|
||||
|
||||
const oldReferences = note.definitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(note.source.eol);
|
||||
|
||||
// When the newly formatted references match the old ones, OR
|
||||
// when non-generated references are present, but no new ones are generated
|
||||
// return null
|
||||
if (
|
||||
oldReferences === newReferences ||
|
||||
(nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
newReferences === '' &&
|
||||
markdownReferences.length > 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fullReferences = `${newReferences}`;
|
||||
// If there are any non-generated definitions, add those to the output as well
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
fullReferences = `${nonGeneratedReferences}${note.source.eol}${newReferences}`;
|
||||
}
|
||||
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${fullReferences}`,
|
||||
range: Range.createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO now the note.title defaults to file name at parsing time, so this check
|
||||
// doesn't work anymore. Decide:
|
||||
// - whether do we actually want to continue generate the headings
|
||||
// - whether it should be under a config option
|
||||
// A possible approach would be around having a `sections` field in the note, and inspect
|
||||
// it to see if there is an h1 title. Alternatively parse directly the markdown in this function.
|
||||
if (note.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontmatterExists = note.source.contentStart.line !== 1;
|
||||
|
||||
let newLineExistsAfterFrontmatter = false;
|
||||
if (frontmatterExists) {
|
||||
const lines = note.source.text.split(note.source.eol);
|
||||
const index = note.source.contentStart.line - 1;
|
||||
const line = lines[index];
|
||||
newLineExistsAfterFrontmatter = line === '';
|
||||
}
|
||||
|
||||
const paddingStart = frontmatterExists ? note.source.eol : '';
|
||||
const paddingEnd = newLineExistsAfterFrontmatter
|
||||
? note.source.eol
|
||||
: `${note.source.eol}${note.source.eol}`;
|
||||
|
||||
return {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
note.uri.getName()
|
||||
)}${paddingEnd}`,
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
note.source.contentStart
|
||||
),
|
||||
};
|
||||
};
|
||||
export { generateLinkReferences } from './generate-link-references';
|
||||
export { generateHeading } from './generate-headings';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { ResourceParser } from './note';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { FoamTags } from './tags';
|
||||
import { Logger } from '../utils/log';
|
||||
|
||||
@@ -24,23 +23,23 @@ export interface Foam extends IDisposable {
|
||||
export const bootstrap = async (
|
||||
matcher: IMatcher,
|
||||
dataStore: IDataStore,
|
||||
parser: ResourceParser,
|
||||
initialProviders: ResourceProvider[]
|
||||
) => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const workspace = new FoamWorkspace();
|
||||
const pStart = Date.now();
|
||||
const tsStart = Date.now();
|
||||
|
||||
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
|
||||
const pWsEnd = Date.now();
|
||||
Logger.info(`Workspace loaded in ${pWsEnd - pStart}ms`);
|
||||
const tsWsDone = Date.now();
|
||||
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
|
||||
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true);
|
||||
const pGraphEnd = Date.now();
|
||||
Logger.info(`Graph loaded in ${pGraphEnd - pWsEnd}ms`);
|
||||
const tsGraphDone = Date.now();
|
||||
Logger.info(`Graph loaded in ${tsGraphDone - tsWsDone}ms`);
|
||||
|
||||
const tags = FoamTags.fromWorkspace(workspace, true);
|
||||
const pTagsEnd = Date.now();
|
||||
Logger.info(`Tags loaded in ${pTagsEnd - pGraphEnd}ms`);
|
||||
const tsTagsEnd = Date.now();
|
||||
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
|
||||
|
||||
const foam: Foam = {
|
||||
workspace,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { debounce } from 'lodash';
|
||||
import { ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { Range } from './range';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { Logger } from '../utils/log';
|
||||
import { Emitter } from '../common/event';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
@@ -29,6 +29,9 @@ export class FoamGraph implements IDisposable {
|
||||
*/
|
||||
public readonly backlinks: Map<string, Connection[]> = new Map();
|
||||
|
||||
private onDidUpdateEmitter = new Emitter<void>();
|
||||
onDidUpdate = this.onDidUpdateEmitter.event;
|
||||
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
@@ -72,91 +75,56 @@ export class FoamGraph implements IDisposable {
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @param debounceFor how long to wait between change detection and graph update
|
||||
* @returns the FoamGraph
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring = false
|
||||
keepMonitoring = false,
|
||||
debounceFor = 0
|
||||
): FoamGraph {
|
||||
const graph = new FoamGraph(workspace);
|
||||
|
||||
workspace.list().forEach(resource => graph.resolveResource(resource));
|
||||
graph.update();
|
||||
if (keepMonitoring) {
|
||||
const updateGraph =
|
||||
debounceFor > 0
|
||||
? debounce(graph.update.bind(graph), 500)
|
||||
: graph.update.bind(graph);
|
||||
graph.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
graph.updateLinksRelatedToAddedResource(resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
graph.updateLinksForResource(change.old, change.new);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
graph.updateLinksRelatedToDeletedResource(resource);
|
||||
})
|
||||
workspace.onDidAdd(updateGraph),
|
||||
workspace.onDidUpdate(updateGraph),
|
||||
workspace.onDidDelete(updateGraph)
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const 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);
|
||||
public update() {
|
||||
const start = Date.now();
|
||||
this.backlinks.clear();
|
||||
this.links.clear();
|
||||
this.placeholders.clear();
|
||||
|
||||
for (const resource of this.workspace.resources()) {
|
||||
for (const link of resource.links) {
|
||||
try {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while resolving link ${
|
||||
link.rawText
|
||||
} in ${resource.uri.toFsPath()}, skipping.`,
|
||||
link,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res))
|
||||
);
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
}
|
||||
|
||||
private updateLinksForResource(oldResource: Resource, newResource: Resource) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
patch.removed.forEach(link => {
|
||||
const target = this.workspace.resolveLink(oldResource, link);
|
||||
return this.disconnect(oldResource.uri, target, link);
|
||||
}, this);
|
||||
patch.added.forEach(link => {
|
||||
const target = this.workspace.resolveLink(newResource, link);
|
||||
return this.connect(newResource.uri, target, link);
|
||||
}, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToDeletedResource(resource: Resource) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = this.links.get(uri.path) ?? [];
|
||||
this.links.delete(uri.path);
|
||||
resourcesPointedByDeletedNote.forEach(connection =>
|
||||
this.disconnect(uri, connection.target, connection.link)
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = this.backlinks.get(uri.path) ?? [];
|
||||
this.backlinks.delete(uri.path);
|
||||
notesPointingToDeletedResource.forEach(link =>
|
||||
this.resolveResource(this.workspace.get(link.source))
|
||||
);
|
||||
return this;
|
||||
const end = Date.now();
|
||||
Logger.debug(`Graph updated in ${end - start}ms`);
|
||||
this.onDidUpdateEmitter.fire();
|
||||
}
|
||||
|
||||
private connect(source: URI, target: URI, link: ResourceLink) {
|
||||
@@ -167,10 +135,9 @@ export class FoamGraph implements IDisposable {
|
||||
}
|
||||
this.links.get(source.path)?.push(connection);
|
||||
|
||||
if (!this.backlinks.get(target.path)) {
|
||||
if (!this.backlinks.has(target.path)) {
|
||||
this.backlinks.set(target.path, []);
|
||||
}
|
||||
|
||||
this.backlinks.get(target.path)?.push(connection);
|
||||
|
||||
if (target.isPlaceholder()) {
|
||||
@@ -179,65 +146,9 @@ export class FoamGraph implements IDisposable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private disconnect(source: URI, target: URI, link: ResourceLink | true) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!source.isEqual(c.source) || !target.isEqual(c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links.set(
|
||||
source.path,
|
||||
this.links.get(source.path)?.filter(connectionsToKeep) ?? []
|
||||
);
|
||||
if (this.links.get(source.path)?.length === 0) {
|
||||
this.links.delete(source.path);
|
||||
}
|
||||
this.backlinks.set(
|
||||
target.path,
|
||||
this.backlinks.get(target.path)?.filter(connectionsToKeep) ?? []
|
||||
);
|
||||
if (this.backlinks.get(target.path)?.length === 0) {
|
||||
this.backlinks.delete(target.path);
|
||||
if (target.isPlaceholder()) {
|
||||
this.placeholders.delete(uriToPlaceholderId(target));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public resolveResource(resource: Resource) {
|
||||
this.links.delete(resource.uri.path);
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
a.source.isEqual(b.source) &&
|
||||
a.target.isEqual(b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
a.type === b.type && Range.isEqual(a.range, b.range);
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import { URI } from './uri';
|
||||
import { Position } from './position';
|
||||
import { Range } from './range';
|
||||
|
||||
export interface NoteSource {
|
||||
text: string;
|
||||
contentStart: Position;
|
||||
end: Position;
|
||||
eol: string;
|
||||
}
|
||||
|
||||
export interface ResourceLink {
|
||||
type: 'wikilink' | 'link';
|
||||
target: string;
|
||||
label: string;
|
||||
rawText: string;
|
||||
range: Range;
|
||||
}
|
||||
@@ -29,6 +19,11 @@ export interface Tag {
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Alias {
|
||||
title: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
label: string;
|
||||
range: Range;
|
||||
@@ -41,11 +36,11 @@ export interface Resource {
|
||||
properties: any;
|
||||
sections: Section[];
|
||||
tags: Tag[];
|
||||
aliases: Alias[];
|
||||
links: ResourceLink[];
|
||||
|
||||
// TODO to remove
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export interface ResourceParser {
|
||||
@@ -67,6 +62,7 @@ export abstract class Resource {
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
typeof (thing as Resource).tags === 'object' &&
|
||||
typeof (thing as Resource).aliases === 'object' &&
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FoamWorkspace } from './workspace';
|
||||
export interface ResourceProvider extends IDisposable {
|
||||
init: (workspace: FoamWorkspace) => Promise<void>;
|
||||
supports: (uri: URI) => boolean;
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
readAsMarkdown: (uri: URI) => Promise<string | null>;
|
||||
fetch: (uri: URI) => Promise<Resource | null>;
|
||||
resolveLink: (
|
||||
|
||||
@@ -59,7 +59,8 @@ describe('FoamTags', () => {
|
||||
tags: ['primary'],
|
||||
});
|
||||
|
||||
tags.updateResourceWithinTagIndex(taglessPage, newPage);
|
||||
ws.set(newPage);
|
||||
tags.update();
|
||||
|
||||
expect(tags.tags).toEqual(new Map([['primary', [page.uri, newPage.uri]]]));
|
||||
});
|
||||
@@ -86,7 +87,8 @@ describe('FoamTags', () => {
|
||||
tags: ['new'],
|
||||
});
|
||||
|
||||
tags.updateResourceWithinTagIndex(page, pageEdited);
|
||||
ws.set(pageEdited);
|
||||
tags.update();
|
||||
|
||||
expect(tags.tags).toEqual(new Map([['new', [page.uri]]]));
|
||||
});
|
||||
@@ -112,12 +114,14 @@ describe('FoamTags', () => {
|
||||
tags: ['primary'],
|
||||
});
|
||||
|
||||
tags.updateResourceWithinTagIndex(page, pageEdited);
|
||||
ws.delete(page.uri);
|
||||
ws.set(pageEdited);
|
||||
tags.update();
|
||||
|
||||
expect(tags.tags).toEqual(new Map([['primary', [pageEdited.uri]]]));
|
||||
});
|
||||
|
||||
it('Updates the metadata of a tag when a note is delete', () => {
|
||||
it('Updates the metadata of a tag when a note is deleted', () => {
|
||||
const ws = createTestWorkspace();
|
||||
|
||||
const page = createTestNote({
|
||||
@@ -131,7 +135,8 @@ describe('FoamTags', () => {
|
||||
const tags = FoamTags.fromWorkspace(ws);
|
||||
expect(tags.tags).toEqual(new Map([['primary', [page.uri]]]));
|
||||
|
||||
tags.removeResourceFromTagIndex(page);
|
||||
ws.delete(page.uri);
|
||||
tags.update();
|
||||
|
||||
expect(tags.tags).toEqual(new Map());
|
||||
});
|
||||
|
||||
@@ -1,81 +1,66 @@
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { URI } from './uri';
|
||||
import { Resource } from './note';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { debounce } from 'lodash';
|
||||
import { Emitter } from '../common/event';
|
||||
|
||||
export class FoamTags implements IDisposable {
|
||||
public readonly tags: Map<string, URI[]> = new Map();
|
||||
|
||||
private onDidUpdateEmitter = new Emitter<void>();
|
||||
onDidUpdate = this.onDidUpdateEmitter.event;
|
||||
|
||||
/**
|
||||
* List of disposables to destroy with the tags
|
||||
*/
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(private readonly workspace: FoamWorkspace) {}
|
||||
|
||||
/**
|
||||
* Computes all tags in the workspace and keep them up-to-date
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @param debounceFor how long to wait between change detection and tags update
|
||||
* @returns the FoamTags
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring = false
|
||||
keepMonitoring = false,
|
||||
debounceFor = 0
|
||||
): FoamTags {
|
||||
const tags = new FoamTags();
|
||||
|
||||
workspace
|
||||
.list()
|
||||
.forEach(resource => tags.addResourceFromTagIndex(resource));
|
||||
const tags = new FoamTags(workspace);
|
||||
tags.update();
|
||||
|
||||
if (keepMonitoring) {
|
||||
const updateTags =
|
||||
debounceFor > 0
|
||||
? debounce(tags.update.bind(tags), 500)
|
||||
: tags.update.bind(tags);
|
||||
tags.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
tags.addResourceFromTagIndex(resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
tags.updateResourceWithinTagIndex(change.old, change.new);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
tags.removeResourceFromTagIndex(resource);
|
||||
})
|
||||
workspace.onDidAdd(updateTags),
|
||||
workspace.onDidUpdate(updateTags),
|
||||
workspace.onDidDelete(updateTags)
|
||||
);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.tags.clear();
|
||||
for (const resource of this.workspace.resources()) {
|
||||
for (const tag of new Set(resource.tags.map(t => t.label))) {
|
||||
const tagMeta = this.tags.get(tag) ?? [];
|
||||
tagMeta.push(resource.uri);
|
||||
this.tags.set(tag, tagMeta);
|
||||
}
|
||||
}
|
||||
this.onDidUpdateEmitter.fire();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
|
||||
updateResourceWithinTagIndex(oldResource: Resource, newResource: Resource) {
|
||||
this.removeResourceFromTagIndex(oldResource);
|
||||
this.addResourceFromTagIndex(newResource);
|
||||
}
|
||||
|
||||
addResourceFromTagIndex(resource: Resource) {
|
||||
new Set(resource.tags.map(t => t.label)).forEach(tag => {
|
||||
const tagMeta = this.tags.get(tag) ?? [];
|
||||
tagMeta.push(resource.uri);
|
||||
this.tags.set(tag, tagMeta);
|
||||
});
|
||||
}
|
||||
|
||||
removeResourceFromTagIndex(resource: Resource) {
|
||||
resource.tags.forEach(t => {
|
||||
const tag = t.label;
|
||||
if (this.tags.has(tag)) {
|
||||
const remainingLocations = this.tags
|
||||
.get(tag)
|
||||
?.filter(uri => !uri.isEqual(resource.uri));
|
||||
|
||||
if (remainingLocations && remainingLocations.length > 0) {
|
||||
this.tags.set(tag, remainingLocations);
|
||||
} else {
|
||||
this.tags.delete(tag);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,5 +71,13 @@ describe('Foam URI', () => {
|
||||
expect(URI.file('/my/file.markdown').resolve('../hello')).toEqual(
|
||||
URI.file('/hello.markdown')
|
||||
);
|
||||
expect(
|
||||
URI.file('/path/to/a/note.md').resolve('../another-note.md')
|
||||
).toEqual(URI.file('/path/to/another-note.md'));
|
||||
expect(
|
||||
URI.file('/path/to/a/note.md').relativeTo(
|
||||
URI.file('/path/to/another/note.md').getDirectory()
|
||||
)
|
||||
).toEqual(URI.file('../a/note.md'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,4 +167,23 @@ describe('Identifier computation', () => {
|
||||
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
|
||||
expect(identifier).toEqual('project/car/todo');
|
||||
});
|
||||
|
||||
it('should ignore elements from the exclude list', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const noteA = createTestNote({ uri: '/path/to/note-a.md' });
|
||||
const noteB = createTestNote({ uri: '/path/to/note-b.md' });
|
||||
const noteC = createTestNote({ uri: '/path/to/note-c.md' });
|
||||
const noteD = createTestNote({ uri: '/path/to/note-d.md' });
|
||||
const noteABis = createTestNote({ uri: '/path/to/another/note-a.md' });
|
||||
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD);
|
||||
expect(workspace.getIdentifier(noteABis.uri)).toEqual('another/note-a');
|
||||
expect(
|
||||
workspace.getIdentifier(noteABis.uri, [noteB.uri, noteA.uri])
|
||||
).toEqual('note-a');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
/**
|
||||
* Resources by path
|
||||
*/
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
private _resources: Map<string, Resource> = new Map();
|
||||
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
@@ -28,7 +28,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
set(resource: Resource) {
|
||||
const old = this.find(resource.uri);
|
||||
this.resources.set(normalize(resource.uri.path), resource);
|
||||
this._resources.set(normalize(resource.uri.path), resource);
|
||||
isSome(old)
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
@@ -36,8 +36,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
delete(uri: URI) {
|
||||
const deleted = this.resources.get(normalize(uri.path));
|
||||
this.resources.delete(normalize(uri.path));
|
||||
const deleted = this._resources.get(normalize(uri.path));
|
||||
this._resources.delete(normalize(uri.path));
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
@@ -48,11 +48,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public list(): Resource[] {
|
||||
return Array.from(this.resources.values());
|
||||
return Array.from(this._resources.values());
|
||||
}
|
||||
|
||||
public iterator(): IterableIterator<Resource> {
|
||||
return this.resources.values();
|
||||
public resources(): IterableIterator<Resource> {
|
||||
return this._resources.values();
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
@@ -69,9 +69,9 @@ export class FoamWorkspace implements IDisposable {
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
const resources = [];
|
||||
for (const key of this.resources.keys()) {
|
||||
for (const key of this._resources.keys()) {
|
||||
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
|
||||
resources.push(this.resources.get(normalize(key)));
|
||||
resources.push(this._resources.get(normalize(key)));
|
||||
}
|
||||
}
|
||||
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
|
||||
@@ -82,17 +82,25 @@ export class FoamWorkspace implements IDisposable {
|
||||
*
|
||||
* @param forResource the resource to compute the identifier for
|
||||
*/
|
||||
public getIdentifier(forResource: URI): string {
|
||||
public getIdentifier(forResource: URI, exclude?: 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);
|
||||
}
|
||||
for (const res of this._resources.values()) {
|
||||
// skip elements that cannot possibly match
|
||||
if (!res.uri.path.endsWith(basename)) {
|
||||
continue;
|
||||
}
|
||||
// skip self
|
||||
if (res.uri.isEqual(forResource)) {
|
||||
continue;
|
||||
}
|
||||
// skip exclude list
|
||||
if (exclude && exclude.find(ex => ex.isEqual(res.uri))) {
|
||||
continue;
|
||||
}
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
amongst.map(uri => uri.path)
|
||||
@@ -106,7 +114,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this.resources.get(normalize((reference as URI).path)) ?? null;
|
||||
return this._resources.get(normalize((reference as URI).path)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
const [path, fragment] = (reference as string).split('#');
|
||||
@@ -116,11 +124,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
if (isAbsolute(path) || isSome(baseUri)) {
|
||||
if (getExtension(path) !== '.md') {
|
||||
const uri = baseUri.resolve(path + '.md');
|
||||
resource = uri ? this.resources.get(normalize(uri.path)) : null;
|
||||
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;
|
||||
resource = uri ? this._resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,13 +145,15 @@ export class FoamWorkspace implements IDisposable {
|
||||
return provider.resolveLink(this, resource, link);
|
||||
}
|
||||
}
|
||||
return URI.placeholder(link.target);
|
||||
throw new Error(
|
||||
`Couldn't find provider for resource "${resource.uri.toString()}"`
|
||||
);
|
||||
}
|
||||
|
||||
public read(uri: URI): Promise<string | null> {
|
||||
public fetch(uri: URI): Promise<Resource | null> {
|
||||
for (const provider of this.providers) {
|
||||
if (provider.supports(uri)) {
|
||||
return provider.read(uri);
|
||||
return provider.fetch(uri);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
@@ -208,6 +218,16 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
static async fromProviders(
|
||||
providers: ResourceProvider[]
|
||||
): Promise<FoamWorkspace> {
|
||||
const workspace = new FoamWorkspace();
|
||||
for (const provider of providers) {
|
||||
await workspace.registerProvider(provider);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
const normalize = (v: string) => v.toLocaleLowerCase();
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Resource, ResourceLink } from '../model/note';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
|
||||
const imageExtensions = ['.png', '.jpg', '.gif'];
|
||||
const attachmentExtensions = ['.pdf', ...imageExtensions];
|
||||
|
||||
const asResource = (uri: URI): Resource => {
|
||||
const type = imageExtensions.includes(uri.getExtension())
|
||||
? 'image'
|
||||
: 'attachment';
|
||||
return {
|
||||
uri: uri,
|
||||
title: uri.getBasename(),
|
||||
type: type,
|
||||
aliases: [],
|
||||
properties: { type: type },
|
||||
sections: [],
|
||||
links: [],
|
||||
tags: [],
|
||||
definitions: [],
|
||||
};
|
||||
};
|
||||
|
||||
export class AttachmentResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly watcher?: IWatcher
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob =>
|
||||
this.dataStore.list(glob, this.matcher.exclude)
|
||||
)
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
for (const uri of files) {
|
||||
Logger.debug('Found: ' + uri.toString());
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
|
||||
if (this.watcher != null) {
|
||||
this.disposables = [
|
||||
this.watcher.onDidChange(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidCreate(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidDelete(uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return attachmentExtensions.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
if (imageExtensions.includes(uri.getExtension())) {
|
||||
return `}|height=200)`;
|
||||
}
|
||||
return `### ${uri.getBasename()}`;
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
return asResource(uri);
|
||||
}
|
||||
|
||||
resolveLink(w: FoamWorkspace, resource: Resource, l: ResourceLink) {
|
||||
throw new Error('not supported');
|
||||
// Silly workaround to make VS Code and es-lint happy
|
||||
// eslint-disable-next-line
|
||||
return resource.uri;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
|
||||
@@ -87,7 +87,7 @@ describe('Matcher', () => {
|
||||
describe('Datastore', () => {
|
||||
it('uses the matcher to get the file list', async () => {
|
||||
const matcher = new Matcher([testFolder], ['**/*.md'], []);
|
||||
const ds = new FileDataStore();
|
||||
const ds = new FileDataStore(readFileFromFs);
|
||||
expect((await ds.list(matcher.include[0])).length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { glob } from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { isWindows } from '../common/platform';
|
||||
import { Event } from '../common/event';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
@@ -93,6 +93,12 @@ export class Matcher implements IMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWatcher {
|
||||
onDidChange: Event<URI>;
|
||||
onDidCreate: Event<URI>;
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
@@ -115,6 +121,8 @@ export interface IDataStore {
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
constructor(private readFile: (uri: URI) => Promise<string>) {}
|
||||
|
||||
async list(glob: string, ignoreGlob?: string | string[]): Promise<URI[]> {
|
||||
const res = await findAllFiles(glob, {
|
||||
ignore: ignoreGlob,
|
||||
@@ -125,7 +133,7 @@ export class FileDataStore implements IDataStore {
|
||||
|
||||
async read(uri: URI) {
|
||||
try {
|
||||
return (await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
return await this.readFile(uri);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
|
||||
238
packages/foam-vscode/src/core/services/markdown-link.test.ts
Normal file
238
packages/foam-vscode/src/core/services/markdown-link.test.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { getRandomURI } from '../../test/test-utils';
|
||||
import { ResourceLink } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { MarkdownLink } from './markdown-link';
|
||||
|
||||
describe('MarkdownLink', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
describe('parse wikilink', () => {
|
||||
it('should parse target', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink]]`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse target and section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink#section]]`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse target and alias', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias');
|
||||
});
|
||||
it('should parse links with square brackets #975', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink [with] brackets]]`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink [with] brackets');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse links with square brackets in alias #975', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink|alias [with] brackets]]`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias [with] brackets');
|
||||
});
|
||||
it('should parse target and alias with escaped separator', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink\\|alias]]`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias');
|
||||
});
|
||||
it('should parse target section and alias', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink with spaces#section with spaces|alias with spaces]]`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink with spaces');
|
||||
expect(parsed.section).toEqual('section with spaces');
|
||||
expect(parsed.alias).toEqual('alias with spaces');
|
||||
});
|
||||
it('should parse section', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[#section]]`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse direct link', () => {
|
||||
it('should parse target', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [link](to/path.md)`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('link');
|
||||
});
|
||||
it('should parse target and section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [link](to/path.md#section)`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toEqual('link');
|
||||
});
|
||||
it('should parse section only', () => {
|
||||
const link: ResourceLink = {
|
||||
type: 'link',
|
||||
rawText: '[link](#section)',
|
||||
range: Range.create(0, 0),
|
||||
};
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toEqual('link');
|
||||
});
|
||||
it('should parse links with square brackets in label #975', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [inbox [xyz]](to/path.md)`
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('inbox [xyz]');
|
||||
});
|
||||
it('should parse links with empty label #975', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [](to/path.md)`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rename wikilink', () => {
|
||||
it('should rename the target only', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink#section]]`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: 'new-link',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink#section]]`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink#section]]`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: 'new-link',
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [[wikilink#section]]`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should be able to rename the alias', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
|
||||
.links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
alias: 'new-alias',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink|new-alias]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rename direct link', () => {
|
||||
it('should rename the target only', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [link](to/path.md)`)
|
||||
.links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: 'to/another-path.md',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [link](to/path.md#section)`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [link](to/path.md#section)`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: 'to/another-path.md',
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
getRandomURI(),
|
||||
`this is a [link](to/path.md#section)`
|
||||
).links[0];
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
packages/foam-vscode/src/core/services/markdown-link.ts
Normal file
65
packages/foam-vscode/src/core/services/markdown-link.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ResourceLink } from '../model/note';
|
||||
|
||||
export abstract class MarkdownLink {
|
||||
private static wikilinkRegex = new RegExp(
|
||||
/\[\[([^#|]+)?#?([^|]+)?\|?(.*)?\]\]/
|
||||
);
|
||||
private static directLinkRegex = new RegExp(
|
||||
/\[(.*)\]\(([^#]*)?#?([^\]]+)?\)/
|
||||
);
|
||||
|
||||
public static analyzeLink(link: ResourceLink) {
|
||||
try {
|
||||
if (link.type === 'wikilink') {
|
||||
const [, target, section, alias] = this.wikilinkRegex.exec(
|
||||
link.rawText
|
||||
);
|
||||
return {
|
||||
target: target?.replace(/\\/g, '') ?? '',
|
||||
section: section ?? '',
|
||||
alias: alias ?? '',
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
const [, alias, target, section] = this.directLinkRegex.exec(
|
||||
link.rawText
|
||||
);
|
||||
return {
|
||||
target: target ?? '',
|
||||
section: section ?? '',
|
||||
alias: alias ?? '',
|
||||
};
|
||||
}
|
||||
throw new Error(`Link of type ${link.type} is not supported`);
|
||||
} catch (e) {
|
||||
throw new Error(`Couldn't parse link ${link.rawText} - ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static createUpdateLinkEdit(
|
||||
link: ResourceLink,
|
||||
delta: { target?: string; section?: string; alias?: string }
|
||||
) {
|
||||
const { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
const newTarget = delta.target ?? target;
|
||||
const newSection = delta.section ?? section ?? '';
|
||||
const newAlias = delta.alias ?? alias ?? '';
|
||||
const sectionDivider = newSection ? '#' : '';
|
||||
const aliasDivider = newAlias ? '|' : '';
|
||||
if (link.type === 'wikilink') {
|
||||
return {
|
||||
newText: `[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
selection: link.range,
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
return {
|
||||
newText: `[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
selection: link.range,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: link of type ${link.type} is not supported`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMarkdownParser, ParserPlugin } from './markdown-parser';
|
||||
import { ResourceLink } from '../model/note';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { Range } from '../model/range';
|
||||
@@ -39,9 +38,7 @@ describe('Markdown parsing', () => {
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.label).toEqual('link to page b');
|
||||
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
|
||||
expect(link.target).toEqual('../doc/page-b.md');
|
||||
});
|
||||
|
||||
it('should detect links that have formatting in label', () => {
|
||||
@@ -51,8 +48,6 @@ describe('Markdown parsing', () => {
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.label).toEqual('link with formatting');
|
||||
expect(link.target).toEqual('../doc/page-b.md');
|
||||
});
|
||||
|
||||
it('should detect wikilinks', () => {
|
||||
@@ -63,13 +58,9 @@ describe('Markdown parsing', () => {
|
||||
let link = note.links[0];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[a link]]');
|
||||
expect(link.label).toEqual('a link');
|
||||
expect(link.target).toEqual('a link');
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[a file]]');
|
||||
expect(link.label).toEqual('a file');
|
||||
expect(link.target).toEqual('a file');
|
||||
});
|
||||
|
||||
it('should detect wikilinks that have aliases', () => {
|
||||
@@ -80,13 +71,9 @@ describe('Markdown parsing', () => {
|
||||
let link = note.links[0];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[link|link alias]]');
|
||||
expect(link.label).toEqual('link alias');
|
||||
expect(link.target).toEqual('link');
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[other link | spaced]]');
|
||||
expect(link.label).toEqual('spaced');
|
||||
expect(link.target).toEqual('other link');
|
||||
});
|
||||
|
||||
it('should skip wikilinks in codeblocks', () => {
|
||||
@@ -99,9 +86,9 @@ 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',
|
||||
expect(noteA.links.map(l => l.rawText)).toEqual([
|
||||
'[[first-wikilink]]',
|
||||
'[[second-wikilink]]',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -113,9 +100,9 @@ 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',
|
||||
expect(noteA.links.map(l => l.rawText)).toEqual([
|
||||
'[[first-wikilink]]',
|
||||
'[[second-wikilink]]',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -393,4 +380,61 @@ and some content`
|
||||
expect(note2.properties.hasHeading).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('Alias', () => {
|
||||
it('can find tags in comma separated string', () => {
|
||||
const note = parser.parse(
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
---
|
||||
alias: alias 1, alias 2 , alias3
|
||||
---
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
`
|
||||
);
|
||||
expect(note.aliases).toEqual([
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias 1',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias 2',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias3',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
it('can find tags in yaml array', () => {
|
||||
const note = parser.parse(
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
---
|
||||
alias:
|
||||
- alias 1
|
||||
- alias 2
|
||||
- alias3
|
||||
---
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
`
|
||||
);
|
||||
expect(note.aliases).toEqual([
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias 1',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias 2',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias3',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,14 +6,13 @@ import wikiLinkPlugin from 'remark-wiki-link';
|
||||
import frontmatterPlugin from 'remark-frontmatter';
|
||||
import { parse as parseYAML } from 'yaml';
|
||||
import visit from 'unist-util-visit';
|
||||
import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import { NoteLinkDefinition, Resource, ResourceParser } from '../model/note';
|
||||
import { Position } from '../model/position';
|
||||
import { Range } from '../model/range';
|
||||
import { extractHashtags, extractTagsFromProp, isSome } from '../utils';
|
||||
import { extractHashtags, extractTagsFromProp, hash, isSome } from '../utils';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { ICache } from '../utils/cache';
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
@@ -25,21 +24,38 @@ export interface ParserPlugin {
|
||||
onDidFindProperties?: (properties: any, note: Resource, node: Node) => void;
|
||||
}
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
type Checksum = string;
|
||||
|
||||
export interface ParserCacheEntry {
|
||||
checksum: Checksum;
|
||||
resource: Resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* This caches the parsed markdown for a given URI.
|
||||
*
|
||||
* The URI identifies the resource that needs to be parsed,
|
||||
* the checksum identifies the text that needs to be parsed.
|
||||
*
|
||||
* If the URI and the Checksum have not changed, the cached resource is returned.
|
||||
*/
|
||||
export type ParserCache = ICache<URI, ParserCacheEntry>;
|
||||
|
||||
export function createMarkdownParser(
|
||||
extraPlugins: ParserPlugin[]
|
||||
extraPlugins: ParserPlugin[] = [],
|
||||
cache?: ParserCache
|
||||
): ResourceParser {
|
||||
const parser = unified()
|
||||
.use(markdownParse, { gfm: true })
|
||||
.use(frontmatterPlugin, ['yaml'])
|
||||
.use(wikiLinkPlugin, { aliasDivider: ALIAS_DIVIDER_CHAR });
|
||||
.use(wikiLinkPlugin, { aliasDivider: '|' });
|
||||
|
||||
const plugins = [
|
||||
titlePlugin,
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
aliasesPlugin,
|
||||
sectionsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
@@ -63,7 +79,6 @@ export function createMarkdownParser(
|
||||
}
|
||||
}
|
||||
const tree = parser.parse(markdown);
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
const note: Resource = {
|
||||
uri: uri,
|
||||
@@ -72,14 +87,9 @@ export function createMarkdownParser(
|
||||
title: '',
|
||||
sections: [],
|
||||
tags: [],
|
||||
aliases: [],
|
||||
links: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
text: markdown,
|
||||
contentStart: astPointToFoamPosition(tree.position!.start),
|
||||
end: astPointToFoamPosition(tree.position!.end),
|
||||
eol: eol,
|
||||
},
|
||||
};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
@@ -97,12 +107,6 @@ export function createMarkdownParser(
|
||||
...note.properties,
|
||||
...yamlProperties,
|
||||
};
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = Position.create(
|
||||
node.position!.end.line! + 2,
|
||||
0
|
||||
);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
plugin.onDidFindProperties?.(yamlProperties, note, node);
|
||||
@@ -134,7 +138,23 @@ export function createMarkdownParser(
|
||||
return note;
|
||||
},
|
||||
};
|
||||
return foamParser;
|
||||
|
||||
const cachedParser: ResourceParser = {
|
||||
parse: (uri: URI, markdown: string): Resource => {
|
||||
const actualChecksum = hash(markdown);
|
||||
if (cache.has(uri)) {
|
||||
const { checksum, resource } = cache.get(uri);
|
||||
if (actualChecksum === checksum) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
const resource = foamParser.parse(uri, markdown);
|
||||
cache.set(uri, { checksum: actualChecksum, resource });
|
||||
return resource;
|
||||
},
|
||||
};
|
||||
|
||||
return isSome(cache) ? cachedParser : foamParser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +237,10 @@ const sectionsPlugin: ParserPlugin = {
|
||||
}
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
const end = Position.create(note.source.end.line + 1, 0);
|
||||
const end = Position.create(
|
||||
astPointToFoamPosition(tree.position.end).line + 1,
|
||||
0
|
||||
);
|
||||
// Close all the remainig sections
|
||||
while (sectionStack.length > 0) {
|
||||
const section = sectionStack.pop();
|
||||
@@ -255,31 +278,35 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const aliasesPlugin: ParserPlugin = {
|
||||
name: 'aliases',
|
||||
onDidFindProperties: (props, note, node) => {
|
||||
if (isSome(props.alias)) {
|
||||
const aliases = Array.isArray(props.alias)
|
||||
? props.alias
|
||||
: props.alias.split(',').map(m => m.trim());
|
||||
for (const alias of aliases) {
|
||||
note.aliases.push({
|
||||
title: alias,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note, noteSource) => {
|
||||
if (node.type === 'wikiLink') {
|
||||
const text = (node as any).value;
|
||||
const alias = node.data?.alias as string;
|
||||
const literalContent = noteSource.substring(
|
||||
node.position!.start.offset!,
|
||||
node.position!.end.offset!
|
||||
);
|
||||
|
||||
const hasAlias =
|
||||
literalContent !== text && literalContent.includes(ALIAS_DIVIDER_CHAR);
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
rawText: literalContent,
|
||||
label: hasAlias
|
||||
? alias.trim()
|
||||
: literalContent.substring(2, literalContent.length - 2),
|
||||
target: hasAlias
|
||||
? literalContent
|
||||
.substring(2, literalContent.indexOf(ALIAS_DIVIDER_CHAR))
|
||||
.replace(/\\/g, '')
|
||||
.trim()
|
||||
: text.trim(),
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
@@ -289,12 +316,13 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
const label = getTextFromChildren(node);
|
||||
const literalContent = noteSource.substring(
|
||||
node.position!.start.offset!,
|
||||
node.position!.end.offset!
|
||||
);
|
||||
note.links.push({
|
||||
type: 'link',
|
||||
target: targetUri,
|
||||
label: label,
|
||||
rawText: `[${label}](${targetUri})`,
|
||||
rawText: literalContent,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
@@ -314,7 +342,8 @@ const definitionsPlugin: ParserPlugin = {
|
||||
}
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
note.definitions = getFoamDefinitions(note.definitions, note.source.end);
|
||||
const end = astPointToFoamPosition(tree.position.end);
|
||||
note.definitions = getFoamDefinitions(note.definitions, end);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -147,10 +147,58 @@ describe('Link resolution', () => {
|
||||
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteC.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[2])).toEqual(noteD.uri);
|
||||
});
|
||||
|
||||
it('should resolve wikilink with section identifier', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
// uppercased filename, lowercased slug
|
||||
{ slug: 'page-b#section' },
|
||||
],
|
||||
});
|
||||
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteB.uri.withFragment('section')
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve section-only wikilinks', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
// uppercased filename, lowercased slug
|
||||
{ slug: '#section' },
|
||||
],
|
||||
});
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteA.uri.withFragment('section')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown direct links', () => {
|
||||
it('should support absolute path', () => {
|
||||
it('should support absolute path 1', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: '/path/to/another/page-b.md' }],
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/another/page-b.md',
|
||||
links: [{ to: '../../to/page-a.md' }],
|
||||
});
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
|
||||
});
|
||||
|
||||
it('should support relative path 1', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: './another/page-b.md' }],
|
||||
@@ -165,13 +213,13 @@ describe('Link resolution', () => {
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
|
||||
});
|
||||
|
||||
it('should support relative path', () => {
|
||||
it('should support relative path 2', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: 'more/page-c.md' }],
|
||||
links: [{ to: 'more/page-b.md' }],
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/more/page-c.md',
|
||||
uri: '/path/to/more/page-b.md',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
@@ -181,10 +229,10 @@ describe('Link resolution', () => {
|
||||
it('should default to relative path', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ to: 'page c.md' }],
|
||||
links: [{ to: 'page .md' }],
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/path/to/page c.md',
|
||||
uri: '/path/to/page .md',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB);
|
||||
|
||||
@@ -8,23 +8,19 @@ import { isNone, isSome } from '../utils';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, FileDataStore, IMatcher } from '../services/datastore';
|
||||
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { createMarkdownParser } from './markdown-parser';
|
||||
import { MarkdownLink } from './markdown-link';
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly watcherInit?: (triggers: {
|
||||
onDidChange: (uri: URI) => void;
|
||||
onDidCreate: (uri: URI) => void;
|
||||
onDidDelete: (uri: URI) => void;
|
||||
}) => IDisposable[],
|
||||
private readonly parser: ResourceParser = createMarkdownParser([]),
|
||||
private readonly dataStore: IDataStore = new FileDataStore()
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly parser: ResourceParser,
|
||||
private readonly watcher?: IWatcher
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
@@ -39,7 +35,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + uri.toString());
|
||||
Logger.debug('Found: ' + uri.toString());
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
@@ -47,36 +43,33 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables =
|
||||
this.watcherInit?.({
|
||||
onDidChange: async uri => {
|
||||
if (this.watcher != null) {
|
||||
this.disposables = [
|
||||
this.watcher.onDidChange(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidCreate: async uri => {
|
||||
}),
|
||||
this.watcher.onDidCreate(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidDelete: uri => {
|
||||
}),
|
||||
this.watcher.onDidDelete(uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
},
|
||||
}) ?? [];
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return uri.isMarkdown();
|
||||
}
|
||||
|
||||
read(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) {
|
||||
@@ -93,7 +86,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
const content = await this.read(uri);
|
||||
const content = await this.dataStore.read(uri);
|
||||
return isSome(content) ? this.parser.parse(uri, content) : null;
|
||||
}
|
||||
|
||||
@@ -103,11 +96,12 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
link: ResourceLink
|
||||
) {
|
||||
let targetUri: URI | undefined;
|
||||
const { target, section } = MarkdownLink.analyzeLink(link);
|
||||
switch (link.type) {
|
||||
case 'wikilink': {
|
||||
let definitionUri = undefined;
|
||||
for (const def of resource.definitions) {
|
||||
if (def.label === link.target) {
|
||||
if (def.label === target) {
|
||||
definitionUri = def.url;
|
||||
break;
|
||||
}
|
||||
@@ -118,12 +112,11 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
workspace.find(definedUri, resource.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
const [target, section] = link.target.split('#');
|
||||
targetUri =
|
||||
target === ''
|
||||
? resource.uri
|
||||
: workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
URI.placeholder(target);
|
||||
|
||||
if (section) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
@@ -132,10 +125,16 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
break;
|
||||
}
|
||||
case 'link': {
|
||||
const [target, section] = link.target.split('#');
|
||||
// force ambiguous links to be treated as relative
|
||||
const path =
|
||||
target.startsWith('/') ||
|
||||
target.startsWith('./') ||
|
||||
target.startsWith('../')
|
||||
? target
|
||||
: './' + target;
|
||||
targetUri =
|
||||
workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(resource.uri.resolve(link.target).path);
|
||||
workspace.find(path, resource.uri)?.uri ??
|
||||
URI.placeholder(resource.uri.resolve(path).path);
|
||||
if (section && !targetUri.isPlaceholder()) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
}
|
||||
@@ -191,7 +190,7 @@ to generate markdown reference list`
|
||||
label:
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText || link.label,
|
||||
: link.rawText,
|
||||
url: relativeUri.path,
|
||||
title: target.title,
|
||||
};
|
||||
|
||||
7
packages/foam-vscode/src/core/utils/cache.ts
Normal file
7
packages/foam-vscode/src/core/utils/cache.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface ICache<K, V> {
|
||||
get(key: K): V | undefined;
|
||||
has(key: K): boolean;
|
||||
set(key: K, data: V): void;
|
||||
del(key: K): void;
|
||||
clear(): void;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { posix } from 'path';
|
||||
import { promises, constants } from 'fs';
|
||||
|
||||
/**
|
||||
* Converts filesystem path to POSIX path. Supported inputs are:
|
||||
@@ -147,21 +146,6 @@ 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;
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
deleteFile,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
|
||||
@@ -23,21 +25,9 @@ describe('getDailyNotePath', () => {
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
const oldValue = await workspace
|
||||
.getConfiguration('foam')
|
||||
.get('openDailyNote.directory');
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toEqual(
|
||||
expectedPath.toFsPath()
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
|
||||
);
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', oldValue);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification', async () => {
|
||||
@@ -48,22 +38,9 @@ describe('getDailyNotePath', () => {
|
||||
? `${config}\\${isoDate}.md`
|
||||
: `${config}/${isoDate}.md`;
|
||||
|
||||
const oldValue = await workspace
|
||||
.getConfiguration('foam')
|
||||
.get('openDailyNote.directory');
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toMatch(
|
||||
expectedPath
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
|
||||
);
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', oldValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,21 +48,21 @@ describe('Daily note template', () => {
|
||||
it('Uses the daily note variables in the template', async () => {
|
||||
const targetDate = new Date(2021, 8, 12);
|
||||
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
await createFile('hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'daily-note.md',
|
||||
]);
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
|
||||
['.foam', 'templates', 'daily-note.md']
|
||||
);
|
||||
|
||||
const config = workspace.getConfiguration('foam');
|
||||
const uri = getDailyNotePath(config, targetDate);
|
||||
const uri = getDailyNotePath(targetDate);
|
||||
|
||||
await createDailyNoteIfNotExists(config, uri, targetDate);
|
||||
await createDailyNoteIfNotExists(targetDate);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toEqual('hello September 12 hello');
|
||||
|
||||
await deleteFile(template.uri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import { workspace } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import { existsInFs } from './core/utils/path';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
|
||||
import { NoteFactory } from './services/templates';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
@@ -12,24 +12,19 @@ import { NoteFactory } from './services/templates';
|
||||
* In the case that the daily note file does not exist,
|
||||
* it gets created along with any folders in its path.
|
||||
*
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @param date The target date. If not provided, the function returns immediately.
|
||||
*/
|
||||
export async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
const currentDate = date instanceof Date ? date : new Date();
|
||||
if (date == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dailyNotePath = getDailyNotePath(foamConfiguration, currentDate);
|
||||
|
||||
const isNew = await createDailyNoteIfNotExists(
|
||||
foamConfiguration,
|
||||
dailyNotePath,
|
||||
currentDate
|
||||
);
|
||||
const { didCreateFile, uri } = await createDailyNoteIfNotExists(date);
|
||||
// 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);
|
||||
if (!didCreateFile) {
|
||||
await focusNote(uri, didCreateFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,18 +37,13 @@ export async function openDailyNoteFor(date?: Date) {
|
||||
* In the case that the directory path is not absolute,
|
||||
* the resulting path will start on the current workspace top-level.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @returns The path to the daily note file.
|
||||
*/
|
||||
export function getDailyNotePath(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
): URI {
|
||||
const dailyNoteDirectory = URI.file(
|
||||
configuration.get('openDailyNote.directory') ?? '.'
|
||||
);
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
export function getDailyNotePath(date: Date): URI {
|
||||
const folder = getFoamVsCodeConfig<string>('openDailyNote.directory') ?? '.';
|
||||
const dailyNoteDirectory = URI.file(folder);
|
||||
const dailyNoteFilename = getDailyNoteFileName(date);
|
||||
|
||||
if (dailyNoteDirectory.isAbsolute()) {
|
||||
return dailyNoteDirectory.joinPath(dailyNoteFilename);
|
||||
@@ -72,18 +62,14 @@ export function getDailyNotePath(
|
||||
* `foam.openDailyNote.filenameFormat` and
|
||||
* `foam.openDailyNote.fileExtension`, respectively.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @returns The daily note's filename.
|
||||
*/
|
||||
export function getDailyNoteFileName(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
): string {
|
||||
const filenameFormat: string = configuration.get(
|
||||
export function getDailyNoteFileName(date: Date): string {
|
||||
const filenameFormat: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.filenameFormat'
|
||||
);
|
||||
const fileExtension: string = configuration.get(
|
||||
const fileExtension: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.fileExtension'
|
||||
);
|
||||
|
||||
@@ -96,37 +82,27 @@ export function getDailyNoteFileName(
|
||||
* In the case that the folders referenced in the file path also do not exist,
|
||||
* this function will create all folders in the path.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param dailyNotePath The path to daily note file.
|
||||
* @param currentDate The current date, to be used as a title.
|
||||
* @returns Wether the file was created.
|
||||
*/
|
||||
export async function createDailyNoteIfNotExists(
|
||||
configuration: WorkspaceConfiguration,
|
||||
dailyNotePath: URI,
|
||||
targetDate: Date
|
||||
) {
|
||||
if (await existsInFs(dailyNotePath.toFsPath())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function createDailyNoteIfNotExists(targetDate: Date) {
|
||||
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
|
||||
const titleFormat: string =
|
||||
configuration.get('openDailyNote.titleFormat') ??
|
||||
configuration.get('openDailyNote.filenameFormat');
|
||||
getFoamVsCodeConfig('openDailyNote.titleFormat') ??
|
||||
getFoamVsCodeConfig('openDailyNote.filenameFormat');
|
||||
|
||||
const templateFallbackText = `---
|
||||
foam_template:
|
||||
name: New Daily Note
|
||||
description: Foam's default daily note template
|
||||
filepath: "${workspace.asRelativePath(
|
||||
toVsCodeUri(pathFromLegacyConfiguration)
|
||||
)}"
|
||||
---
|
||||
# ${dateFormat(targetDate, titleFormat, false)}
|
||||
`;
|
||||
|
||||
await NoteFactory.createFromDailyNoteTemplate(
|
||||
dailyNotePath,
|
||||
return await NoteFactory.createFromDailyNoteTemplate(
|
||||
pathFromLegacyConfiguration,
|
||||
templateFallbackText,
|
||||
targetDate
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import { workspace, ExtensionContext, window, commands } from 'vscode';
|
||||
import { MarkdownResourceProvider } from './core/services/markdown-provider';
|
||||
import { bootstrap } from './core/model/foam';
|
||||
import { URI } from './core/model/uri';
|
||||
import { FileDataStore, Matcher } from './core/services/datastore';
|
||||
import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
|
||||
import { AttachmentResourceProvider } from './core/services/attachment-provider';
|
||||
import { VsCodeWatcher } from './services/watcher';
|
||||
import { createMarkdownParser } from './core/services/markdown-parser';
|
||||
import VsCodeBasedParserCache from './services/cache';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
@@ -18,30 +23,51 @@ export async function activate(context: ExtensionContext) {
|
||||
Logger.info('Starting Foam');
|
||||
|
||||
// Prepare Foam
|
||||
const dataStore = new FileDataStore();
|
||||
const readFile = async (uri: URI) =>
|
||||
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
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 watcher = new VsCodeWatcher(
|
||||
workspace.createFileSystemWatcher('**/*')
|
||||
);
|
||||
const parserCache = new VsCodeBasedParserCache(context);
|
||||
const parser = createMarkdownParser([], parserCache);
|
||||
|
||||
const foamPromise = bootstrap(matcher, dataStore, [markdownProvider]);
|
||||
const markdownProvider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
parser,
|
||||
watcher
|
||||
);
|
||||
const attachmentProvider = new AttachmentResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
watcher
|
||||
);
|
||||
|
||||
const foamPromise = bootstrap(matcher, dataStore, parser, [
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
]);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
|
||||
context.subscriptions.push(foam, markdownProvider);
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
|
||||
context.subscriptions.push(
|
||||
foam,
|
||||
watcher,
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
commands.registerCommand('foam-vscode.clear-cache', () =>
|
||||
parserCache.clear()
|
||||
)
|
||||
);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '../test/test-utils-vscode';
|
||||
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
@@ -10,7 +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';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -30,9 +30,7 @@ const feature: FoamFeature = {
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.backlinks', provider),
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -71,7 +69,7 @@ export class BacklinksTreeDataProvider
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
const lines = (
|
||||
(await this.workspace.read(resource.uri)) ?? ''
|
||||
(await this.workspace.readAsMarkdown(resource.uri)) ?? ''
|
||||
).split('\n');
|
||||
if (link.range.start.line < lines.length) {
|
||||
const line = lines[link.range.start.line];
|
||||
@@ -131,11 +129,11 @@ export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
public readonly resource: Resource,
|
||||
public readonly link: ResourceLink
|
||||
) {
|
||||
super(link.label, vscode.TreeItemCollapsibleState.None);
|
||||
super(link.rawText, vscode.TreeItemCollapsibleState.None);
|
||||
this.label = `${link.range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [resource.uri, { selection: link.range }],
|
||||
arguments: [toVsCodeUri(resource.uri), { selection: link.range }],
|
||||
title: 'Go to link',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import {
|
||||
createFile,
|
||||
getUriInWorkspace,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { createFile, showInEditor } from '../../test/test-utils-vscode';
|
||||
|
||||
describe('copyWithoutBrackets', () => {
|
||||
describe('copy-without-brackets command', () => {
|
||||
it('should get the input from the active editor selection', async () => {
|
||||
const { uri } = await createFile('This is my [[test-content]].');
|
||||
const { editor } = await showInEditor(uri);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { window, env, ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { removeBrackets } from '../utils';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { removeBrackets } from '../../utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
@@ -0,0 +1,28 @@
|
||||
import { commands, window } from 'vscode';
|
||||
import * as editor from '../../services/editor';
|
||||
|
||||
describe('create-note-from-default-template command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { createTemplate } from '../../services/templates';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-new-template',
|
||||
createTemplate
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { commands, window } from 'vscode';
|
||||
import * as editor from '../../services/editor';
|
||||
|
||||
describe('create-note-from-default-template command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { DEFAULT_TEMPLATE_URI, NoteFactory } from '../../services/templates';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-default-template',
|
||||
() => {
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
return NoteFactory.createFromTemplate(
|
||||
DEFAULT_TEMPLATE_URI,
|
||||
resolver,
|
||||
undefined,
|
||||
`---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}
|
||||
`
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { createFile } from '../../test/test-utils-vscode';
|
||||
|
||||
describe('create-note-from-template command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('offers to create template when none are available', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(['Yes', 'No'], {
|
||||
placeHolder:
|
||||
'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));
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,36 @@
|
||||
import { commands, ExtensionContext, QuickPickItem, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { FoamFeature } from '../../types';
|
||||
import {
|
||||
createTemplate,
|
||||
DEFAULT_TEMPLATE_URI,
|
||||
getTemplateMetadata,
|
||||
getTemplates,
|
||||
NoteFactory,
|
||||
TEMPLATES_DIR,
|
||||
} from '../services/templates';
|
||||
import { Resolver } from '../services/variable-resolver';
|
||||
} from '../../services/templates';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
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);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
async function offerToCreateTemplate(): Promise<void> {
|
||||
const response = await window.showQuickPick(['Yes', 'No'], {
|
||||
@@ -90,57 +112,4 @@ async function askUserForTemplate() {
|
||||
});
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
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',
|
||||
() => {
|
||||
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',
|
||||
createTemplate
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
12
packages/foam-vscode/src/features/commands/index.ts
Normal file
12
packages/foam-vscode/src/features/commands/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { default as copyWithoutBracketsCommand } from './copy-without-brackets';
|
||||
export { default as createFromDefaultTemplateCommand } from './create-note-from-default-template';
|
||||
export { default as createFromTemplateCommand } from './create-note-from-template';
|
||||
export { default as createNewTemplate } from './create-new-template';
|
||||
export { default as janitorCommand } from './janitor';
|
||||
export { default as openDailyNoteCommand } from './open-daily-note';
|
||||
export { default as openDailyNoteForDateCommand } from './open-daily-note-for-date';
|
||||
export { default as openDatedNote } from './open-dated-note';
|
||||
export { default as openRandomNoteCommand } from './open-random-note';
|
||||
export { default as openResource } from './open-resource';
|
||||
export { default as updateGraphCommand } from './update-graph';
|
||||
export { default as updateWikilinksCommand } from './update-wikilinks';
|
||||
@@ -5,19 +5,23 @@ import {
|
||||
commands,
|
||||
ProgressLocation,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { FoamFeature } from '../types';
|
||||
import { FoamFeature } from '../../types';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../settings';
|
||||
import { toVsCodePosition, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../core/janitor';
|
||||
import { Range } from '../core/model/range';
|
||||
import { applyTextEdit } from '../core/janitor/apply-text-edit';
|
||||
} from '../../settings';
|
||||
import {
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { applyTextEdit } from '../../core/janitor/apply-text-edit';
|
||||
import detectNewline from 'detect-newline';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -96,8 +100,10 @@ async function runJanitor(foam: Foam) {
|
||||
|
||||
// Apply Text Edits to Non Dirty Notes using fs module just like CLI
|
||||
|
||||
const fileWritePromises = nonDirtyNotes.map(note => {
|
||||
const heading = generateHeading(note);
|
||||
const fileWritePromises = nonDirtyNotes.map(async note => {
|
||||
const noteText = await foam.workspace.readAsMarkdown(note.uri);
|
||||
const noteEol = detectNewline(noteText);
|
||||
const heading = await generateHeading(note, noteText, noteEol);
|
||||
if (heading) {
|
||||
updatedHeadingCount += 1;
|
||||
}
|
||||
@@ -105,8 +111,10 @@ async function runJanitor(foam: Foam) {
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
? null
|
||||
: generateLinkReferences(
|
||||
: await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
foam.workspace,
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
|
||||
);
|
||||
@@ -121,11 +129,11 @@ async function runJanitor(foam: Foam) {
|
||||
// Apply Edits
|
||||
// Note: The ordering matters. Definitions need to be inserted
|
||||
// before heading, since inserting a heading changes line numbers below
|
||||
let text = note.source.text;
|
||||
let text = noteText;
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
|
||||
return fs.promises.writeFile(note.uri.toFsPath(), text);
|
||||
return workspace.fs.writeFile(toVsCodeUri(note.uri), Buffer.from(text));
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
@@ -138,13 +146,17 @@ async function runJanitor(foam: Foam) {
|
||||
n => n.uri.toFsPath() === editor.document.uri.fsPath
|
||||
)!;
|
||||
|
||||
const noteText = doc.getText();
|
||||
const eol = doc.eol.toString();
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const heading = await generateHeading(note, noteText, eol);
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
? null
|
||||
: generateLinkReferences(
|
||||
: await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
eol,
|
||||
foam.workspace,
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
|
||||
describe('open-daily-note-for-date command', () => {
|
||||
it('offers to pick which template to use', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.open-daily-note-for-date');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining([
|
||||
expect.objectContaining({
|
||||
label: expect.stringContaining(
|
||||
dateFormat(new Date(), 'mmm dd, yyyy')
|
||||
),
|
||||
}),
|
||||
]),
|
||||
{
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ExtensionContext, commands, window, QuickPickItem } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { openDailyNoteFor } from '../../dated-notes';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { range } from 'lodash';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.open-daily-note-for-date',
|
||||
async () => {
|
||||
const ws = (await foamPromise).workspace;
|
||||
const date = await window
|
||||
.showQuickPick<DateItem>(generateDateItems(ws), {
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
})
|
||||
.then(item => {
|
||||
return item?.date;
|
||||
});
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
class DateItem implements QuickPickItem {
|
||||
public label: string;
|
||||
public detail: string;
|
||||
public description: string;
|
||||
public alwaysShow?: boolean;
|
||||
constructor(public date: Date, offset: number, public exists: boolean) {
|
||||
const icon = exists ? '$(calendar)' : '$(new-file)';
|
||||
this.label = `${icon} ${dateFormat(date, 'mmm dd, yyyy')}`;
|
||||
this.detail = dateFormat(date, 'dddd');
|
||||
if (offset === 0) {
|
||||
this.detail = 'Today';
|
||||
} else if (offset === -1) {
|
||||
this.detail = 'Yesterday';
|
||||
} else if (offset === 1) {
|
||||
this.detail = 'Tomorrow';
|
||||
} else if (offset > -8 && offset < -1) {
|
||||
this.detail = `Last ${dateFormat(date, 'dddd')}`;
|
||||
} else if (offset > 1 && offset < 8) {
|
||||
this.detail = `Next ${dateFormat(date, 'dddd')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateDateItems(ws: FoamWorkspace): DateItem[] {
|
||||
const items = [
|
||||
...range(0, 32), // next month
|
||||
...range(-31, 0), // last month
|
||||
].map(offset => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + offset);
|
||||
// TODO this is only compatible with default settings as it would
|
||||
// be otherwise hard to "guess" the daily note path
|
||||
// Ideally we would read the daily note path from the config or template to properly match
|
||||
const noteBasename = dateFormat(date, 'yyyy-mm-dd', false);
|
||||
const exists = ws.find(noteBasename) ? true : false;
|
||||
return new DateItem(date, offset, exists);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default feature;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import { openDailyNoteFor } from '../../dated-notes';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', () =>
|
||||
openDailyNoteFor(new Date())
|
||||
)
|
||||
);
|
||||
|
||||
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
|
||||
commands.executeCommand('foam-vscode.open-daily-note');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import {
|
||||
createDailyNoteIfNotExists,
|
||||
openDailyNoteFor,
|
||||
} from '../../dated-notes';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-dated-note', date => {
|
||||
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
|
||||
case 'navigateToNote':
|
||||
return openDailyNoteFor(date);
|
||||
case 'createNote':
|
||||
return createDailyNoteIfNotExists(date);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { focusNote } from '../utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { focusNote } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
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';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { NoteFactory } from '../../services/templates';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
export const OPEN_COMMAND = {
|
||||
command: 'foam-vscode.open-resource',
|
||||
@@ -38,6 +37,13 @@ const feature: FoamFeature = {
|
||||
});
|
||||
}
|
||||
case 'placeholder': {
|
||||
const title = uri.getName();
|
||||
if (uri.isAbsolute()) {
|
||||
return NoteFactory.createForPlaceholderWikilink(
|
||||
title,
|
||||
URI.file(uri.path)
|
||||
);
|
||||
}
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? vscode.workspace.workspaceFolders[0].uri
|
||||
@@ -47,7 +53,6 @@ const feature: FoamFeature = {
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
const title = uri.getName();
|
||||
const target = fromVsCodeUri(basedir)
|
||||
.resolve(uri, true)
|
||||
.changeExtension('', '.md');
|
||||
17
packages/foam-vscode/src/features/commands/update-graph.ts
Normal file
17
packages/foam-vscode/src/features/commands/update-graph.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
|
||||
export const UPDATE_GRAPH_COMMAND_NAME = 'foam-vscode.update-graph';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(UPDATE_GRAPH_COMMAND_NAME, async () => {
|
||||
const foam = await foamPromise;
|
||||
return foam.graph.update();
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -19,23 +19,23 @@ import {
|
||||
isMdEditor,
|
||||
mdDocSelector,
|
||||
getText,
|
||||
} from '../utils';
|
||||
import { FoamFeature } from '../types';
|
||||
} from '../../utils';
|
||||
import { FoamFeature } from '../../types';
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../settings';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
} from '../../settings';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../core/services/markdown-provider';
|
||||
} from '../../core/services/markdown-provider';
|
||||
import {
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
} from '../core/janitor';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
} from '../../core/janitor/generate-link-references';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -45,13 +45,12 @@ const feature: FoamFeature = {
|
||||
commands.registerCommand('foam-vscode.update-wikilinks', () =>
|
||||
updateReferenceList(foam.workspace)
|
||||
),
|
||||
|
||||
workspace.onWillSaveTextDocument(e => {
|
||||
if (foam.services.matcher.isMatch(fromVsCodeUri(e.document.uri))) {
|
||||
if (e.document.languageId === 'markdown') {
|
||||
updateDocumentInNoteGraph(foam, e.document);
|
||||
e.waitUntil(updateReferenceList(foam.workspace));
|
||||
}
|
||||
if (
|
||||
e.document.languageId === 'markdown' &&
|
||||
foam.services.matcher.isMatch(fromVsCodeUri(e.document.uri))
|
||||
) {
|
||||
e.waitUntil(updateReferenceList(foam.workspace));
|
||||
}
|
||||
}),
|
||||
languages.registerCodeLensProvider(
|
||||
@@ -59,27 +58,9 @@ const feature: FoamFeature = {
|
||||
new WikilinkReferenceCodeLensProvider(foam.workspace)
|
||||
)
|
||||
);
|
||||
|
||||
// when a file is created as a result of peekDefinition
|
||||
// action on a wikilink, add definition update references
|
||||
foam.workspace.onDidAdd(_ => {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDocumentInNoteGraph(foam, editor.document);
|
||||
updateReferenceList(foam.workspace);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
|
||||
foam.workspace.set(
|
||||
foam.services.parser.parse(fromVsCodeUri(document.uri), document.getText())
|
||||
);
|
||||
}
|
||||
|
||||
async function createReferenceList(foam: FoamWorkspace) {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { Uri, commands, window, workspace } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createFile } from '../test/test-utils-vscode';
|
||||
import * as editor from '../services/editor';
|
||||
|
||||
describe('Create from template commands', () => {
|
||||
describe('create-note-from-template', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('offers to create template when none are available', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(['Yes', 'No'], {
|
||||
placeHolder:
|
||||
'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', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create-new-template', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a new template', async () => {
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'hello-world.md'
|
||||
).fsPath;
|
||||
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(template);
|
||||
});
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-new-template');
|
||||
|
||||
const file = await workspace.fs.readFile(toVsCodeUri(URI.file(template)));
|
||||
expect(window.showInputBox).toHaveBeenCalled();
|
||||
expect(file).toBeDefined();
|
||||
});
|
||||
|
||||
it('can be cancelled', async () => {
|
||||
// This is the default template which would be created.
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'new-template.md'
|
||||
).fsPath;
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-new-template');
|
||||
|
||||
expect(window.showInputBox).toHaveBeenCalled();
|
||||
await expect(
|
||||
workspace.fs.readFile(toVsCodeUri(URI.file(template)))
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,13 +33,9 @@ const feature: FoamFeature = {
|
||||
updateGraph(panel, foam);
|
||||
};
|
||||
|
||||
const noteAddedListener = foam.workspace.onDidAdd(onFoamChanged);
|
||||
const noteUpdatedListener = foam.workspace.onDidUpdate(onFoamChanged);
|
||||
const noteDeletedListener = foam.workspace.onDidDelete(onFoamChanged);
|
||||
const noteUpdatedListener = foam.graph.onDidUpdate(onFoamChanged);
|
||||
panel.onDidDispose(() => {
|
||||
noteAddedListener.dispose();
|
||||
noteUpdatedListener.dispose();
|
||||
noteDeletedListener.dispose();
|
||||
panel = undefined;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
workspace,
|
||||
ExtensionContext,
|
||||
commands,
|
||||
languages,
|
||||
CompletionItemProvider,
|
||||
CompletionItem,
|
||||
@@ -9,12 +7,8 @@ import {
|
||||
CompletionList,
|
||||
CompletionTriggerKind,
|
||||
} from 'vscode';
|
||||
import {
|
||||
createDailyNoteIfNotExists,
|
||||
getDailyNoteFileName,
|
||||
openDailyNoteFor,
|
||||
getDailyNotePath,
|
||||
} from '../dated-notes';
|
||||
import { getDailyNoteFileName } from '../dated-notes';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
interface DateSnippet {
|
||||
@@ -32,11 +26,6 @@ const daysOfWeek = [
|
||||
{ day: 'friday', index: 5 },
|
||||
{ day: 'saturday', index: 6 },
|
||||
];
|
||||
type AfterCompletionOptions = 'noop' | 'createNote' | 'navigateToNote';
|
||||
const foamConfig = workspace.getConfiguration('foam');
|
||||
const foamNavigateOnSelect: AfterCompletionOptions = foamConfig.get(
|
||||
'dateSnippets.afterCompletion'
|
||||
);
|
||||
|
||||
const generateDayOfWeekSnippets = (): DateSnippet[] => {
|
||||
const getTarget = (day: number) => {
|
||||
@@ -64,7 +53,7 @@ const createCompletionItem = ({ snippet, date, detail }: DateSnippet) => {
|
||||
);
|
||||
completionItem.insertText = getDailyNoteLink(date);
|
||||
completionItem.detail = `${completionItem.insertText} - ${detail}`;
|
||||
if (foamNavigateOnSelect !== 'noop') {
|
||||
if (getFoamVsCodeConfig('dateSnippets.afterCompletion') !== 'noop') {
|
||||
completionItem.command = {
|
||||
command: 'foam-vscode.open-dated-note',
|
||||
title: 'Open a note for the given date',
|
||||
@@ -75,8 +64,8 @@ const createCompletionItem = ({ snippet, date, detail }: DateSnippet) => {
|
||||
};
|
||||
|
||||
const getDailyNoteLink = (date: Date) => {
|
||||
const foamExtension = foamConfig.get('openDailyNote.fileExtension');
|
||||
const name = getDailyNoteFileName(foamConfig, date);
|
||||
const foamExtension = getFoamVsCodeConfig('openDailyNote.fileExtension');
|
||||
const name = getDailyNoteFileName(date);
|
||||
return `[[${name.replace(`.${foamExtension}`, '')}]]`;
|
||||
};
|
||||
|
||||
@@ -210,33 +199,17 @@ export const datesCompletionProvider: CompletionItemProvider = {
|
||||
},
|
||||
};
|
||||
|
||||
const datedNoteCommand = (date: Date) => {
|
||||
if (foamNavigateOnSelect === 'navigateToNote') {
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
if (foamNavigateOnSelect === 'createNote') {
|
||||
return createDailyNoteIfNotExists(
|
||||
foamConfig,
|
||||
getDailyNotePath(foamConfig, date),
|
||||
date
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-dated-note', date =>
|
||||
datedNoteCommand(date)
|
||||
languages.registerCompletionItemProvider('markdown', completions, '/'),
|
||||
languages.registerCompletionItemProvider(
|
||||
'markdown',
|
||||
datesCompletionProvider,
|
||||
'/',
|
||||
'+'
|
||||
)
|
||||
);
|
||||
languages.registerCompletionItemProvider('markdown', completions, '/');
|
||||
languages.registerCompletionItemProvider(
|
||||
'markdown',
|
||||
datesCompletionProvider,
|
||||
'/',
|
||||
'+'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
monitorFoamVsCodeConfig,
|
||||
} from '../services/config';
|
||||
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 placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
@@ -21,15 +15,10 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
});
|
||||
|
||||
const updateDecorations = (
|
||||
areDecorationsEnabled: () => boolean,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (
|
||||
!editor ||
|
||||
!areDecorationsEnabled() ||
|
||||
editor.document.languageId !== 'markdown'
|
||||
) {
|
||||
if (!editor || editor.document.languageId !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
const note = parser.parse(
|
||||
@@ -43,9 +32,9 @@ const updateDecorations = (
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + 2,
|
||||
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
|
||||
link.range.end.line,
|
||||
link.range.end.character - 2
|
||||
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -58,14 +47,10 @@ const feature: FoamFeature = {
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const areDecorationsEnabled: ConfigurationMonitor<boolean> = monitorFoamVsCodeConfig(
|
||||
CONFIG_KEY
|
||||
);
|
||||
const foam = await foamPromise;
|
||||
let activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
const immediatelyUpdateDecorations = updateDecorations(
|
||||
areDecorationsEnabled,
|
||||
foam.services.parser,
|
||||
foam.workspace
|
||||
);
|
||||
@@ -78,7 +63,6 @@ const feature: FoamFeature = {
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
|
||||
context.subscriptions.push(
|
||||
areDecorationsEnabled,
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Matcher } from '../core/services/datastore';
|
||||
import { FileDataStore, Matcher } from '../core/services/datastore';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { HoverProvider } from './hover-provider';
|
||||
import { readFileFromFs } from '../test/test-utils';
|
||||
|
||||
// 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.
|
||||
@@ -19,7 +20,13 @@ const createWorkspace = () => {
|
||||
const matcher = new Matcher(
|
||||
vscode.workspace.workspaceFolders.map(f => fromVsCodeUri(f.uri))
|
||||
);
|
||||
const resourceProvider = new MarkdownResourceProvider(matcher);
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const parser = createMarkdownParser();
|
||||
const resourceProvider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
parser
|
||||
);
|
||||
const workspace = new FoamWorkspace();
|
||||
workspace.registerProvider(resourceProvider);
|
||||
return workspace;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
import createReferences from './wikilink-reference-generation';
|
||||
import openDailyNote from './open-daily-note';
|
||||
import janitor from './janitor';
|
||||
import * as commands from './commands';
|
||||
import dataviz from './dataviz';
|
||||
import copyWithoutBrackets from './copy-without-brackets';
|
||||
import openDatedNote from './open-dated-note';
|
||||
import dateSnippets from './date-snippets';
|
||||
import tagsExplorer from './tags-tree-view';
|
||||
import createFromTemplate from './create-from-template';
|
||||
import openRandomNote from './open-random-note';
|
||||
import orphans from './orphans';
|
||||
import placeholders from './placeholders';
|
||||
import backlinks from './backlinks';
|
||||
import utilityCommands from './utility-commands';
|
||||
import hoverProvider from './hover-provider';
|
||||
import previewNavigation from './preview-navigation';
|
||||
import completionProvider from './link-completion';
|
||||
import completionProvider, { completionCursorMove } from './link-completion';
|
||||
import tagCompletionProvider from './tag-completion';
|
||||
import linkDecorations from './document-decorator';
|
||||
import navigationProviders from './navigation-provider';
|
||||
import wikilinkDiagnostics from './wikilink-diagnostics';
|
||||
import refactor from './refactor';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
export const features: FoamFeature[] = [
|
||||
...Object.values(commands),
|
||||
refactor,
|
||||
navigationProviders,
|
||||
wikilinkDiagnostics,
|
||||
tagsExplorer,
|
||||
createReferences,
|
||||
openDailyNote,
|
||||
openRandomNote,
|
||||
janitor,
|
||||
dataviz,
|
||||
copyWithoutBrackets,
|
||||
openDatedNote,
|
||||
createFromTemplate,
|
||||
dateSnippets,
|
||||
orphans,
|
||||
placeholders,
|
||||
backlinks,
|
||||
hoverProvider,
|
||||
utilityCommands,
|
||||
linkDecorations,
|
||||
previewNavigation,
|
||||
completionProvider,
|
||||
tagCompletionProvider,
|
||||
completionCursorMove,
|
||||
];
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { createTestNote, createTestWorkspace } from '../test/test-utils';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
describe('Link Completion', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const root = fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri);
|
||||
const ws = new FoamWorkspace();
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(
|
||||
createTestNote({
|
||||
root,
|
||||
@@ -158,4 +157,30 @@ Content of section 2
|
||||
new Set(['Section 1', 'Section 2'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return page alias', async () => {
|
||||
const { uri, content } = await createFile(
|
||||
`
|
||||
---
|
||||
alias: alias-a
|
||||
---
|
||||
[[
|
||||
`,
|
||||
['new-note-with-alias.md']
|
||||
);
|
||||
ws.set(parser.parse(uri, content));
|
||||
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(4, 2)
|
||||
);
|
||||
|
||||
const aliasCompletionItem = links.items.find(i => i.label === 'alias-a');
|
||||
expect(aliasCompletionItem).not.toBeNull();
|
||||
expect(aliasCompletionItem.label).toBe('alias-a');
|
||||
expect(aliasCompletionItem.insertText).toBe('new-note-with-alias|alias-a');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,15 @@ import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const aliasCommitCharacters = ['#'];
|
||||
export const linkCommitCharacters = ['#', '|'];
|
||||
export const sectionCommitCharacters = ['|'];
|
||||
|
||||
const COMPLETION_CURSOR_MOVE = {
|
||||
command: 'foam-vscode.completion-move-cursor',
|
||||
title: 'Foam: Move cursor after completion',
|
||||
};
|
||||
|
||||
export const WIKILINK_REGEX = /\[\[[^[\]]*(?!.*\]\])/;
|
||||
export const SECTION_REGEX = /\[\[([^[\]]*#(?!.*\]\]))/;
|
||||
|
||||
@@ -31,6 +40,65 @@ const feature: FoamFeature = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* always jump to the closing bracket, but jump back the cursor when commit
|
||||
* by alias divider `|` and section divider `#`
|
||||
* See https://github.com/foambubble/foam/issues/962,
|
||||
*/
|
||||
|
||||
export const completionCursorMove: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
COMPLETION_CURSOR_MOVE.command,
|
||||
async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const document = activeEditor.document;
|
||||
const currentPosition = activeEditor.selection.active;
|
||||
const cursorChange = vscode.window.onDidChangeTextEditorSelection(
|
||||
async e => {
|
||||
const changedPosition = e.selections[0].active;
|
||||
const preChar = document
|
||||
.lineAt(changedPosition.line)
|
||||
.text.charAt(changedPosition.character - 1);
|
||||
|
||||
const {
|
||||
character: selectionChar,
|
||||
line: selectionLine,
|
||||
} = e.selections[0].active;
|
||||
|
||||
const {
|
||||
line: completionLine,
|
||||
character: completionChar,
|
||||
} = currentPosition;
|
||||
|
||||
const inCompleteBySectionDivider =
|
||||
linkCommitCharacters.includes(preChar) &&
|
||||
selectionLine === completionLine &&
|
||||
selectionChar === completionChar + 1;
|
||||
|
||||
cursorChange.dispose();
|
||||
if (inCompleteBySectionDivider) {
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'left',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'right',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace) {}
|
||||
@@ -70,6 +138,8 @@ export class SectionCompletionProvider
|
||||
);
|
||||
item.sortText = String(b.range.start.line).padStart(5, '0');
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = sectionCommitCharacters;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
return item;
|
||||
});
|
||||
return new vscode.CompletionList(items);
|
||||
@@ -104,12 +174,12 @@ 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.
|
||||
const requiresAutocomplete = cursorPrefix.match(WIKILINK_REGEX);
|
||||
|
||||
if (!requiresAutocomplete || requiresAutocomplete[0].indexOf('#') >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = requiresAutocomplete[0];
|
||||
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
position.character - (text.length - 2),
|
||||
@@ -123,12 +193,32 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.File,
|
||||
resource.uri
|
||||
);
|
||||
item.sortText =
|
||||
resource.type === 'attachment' ? `1-${item.label}` : `0-${item.label}`;
|
||||
item.filterText = resource.uri.getName();
|
||||
item.insertText = this.ws.getIdentifier(resource.uri);
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = ['#'];
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.commitCharacters = linkCommitCharacters;
|
||||
return item;
|
||||
});
|
||||
const aliases = this.ws.list().flatMap(resource =>
|
||||
resource.aliases.map(a => {
|
||||
const item = new ResourceCompletionItem(
|
||||
a.title,
|
||||
vscode.CompletionItemKind.Reference,
|
||||
resource.uri
|
||||
);
|
||||
item.insertText = this.ws.getIdentifier(resource.uri) + '|' + a.title;
|
||||
item.detail = `Alias of ${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
)}`;
|
||||
item.range = replacementRange;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.commitCharacters = aliasCommitCharacters;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
const placeholders = Array.from(this.graph.placeholders.values()).map(
|
||||
uri => {
|
||||
const item = new vscode.CompletionItem(
|
||||
@@ -136,12 +226,17 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.Interface
|
||||
);
|
||||
item.insertText = uri.path;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
}
|
||||
);
|
||||
|
||||
return new vscode.CompletionList([...resources, ...placeholders]);
|
||||
return new vscode.CompletionList([
|
||||
...resources,
|
||||
...aliases,
|
||||
...placeholders,
|
||||
]);
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
|
||||
@@ -8,10 +8,9 @@ import {
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { NavigationProvider } from './navigation-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
|
||||
describe('Document navigation', () => {
|
||||
@@ -31,7 +30,7 @@ describe('Document navigation', () => {
|
||||
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 ws = createTestWorkspace().set(parser.parse(uri, content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
@@ -45,7 +44,7 @@ describe('Document navigation', () => {
|
||||
const { uri, content } = await createFile(
|
||||
'This is some content without links'
|
||||
);
|
||||
const ws = new FoamWorkspace().set(parser.parse(uri, content));
|
||||
const ws = createTestWorkspace().set(parser.parse(uri, content));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(toVsCodeUri(uri));
|
||||
@@ -74,7 +73,7 @@ describe('Document navigation', () => {
|
||||
|
||||
it('should create links for placeholders', async () => {
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
const ws = new FoamWorkspace().set(
|
||||
const ws = createTestWorkspace().set(
|
||||
parser.parse(fileA.uri, fileA.content)
|
||||
);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
@@ -125,8 +124,8 @@ describe('Document navigation', () => {
|
||||
|
||||
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));
|
||||
// target the beginning of the file
|
||||
expect(definitions[0].targetRange).toEqual(new vscode.Range(0, 0, 0, 0));
|
||||
// select nothing
|
||||
expect(definitions[0].targetSelectionRange).toEqual(
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
@@ -232,6 +231,6 @@ describe('Document navigation', () => {
|
||||
range: new vscode.Range(0, 23, 0, 23 + 9),
|
||||
});
|
||||
});
|
||||
// it('should provide references for placeholders', async () => {});
|
||||
it.todo('should provide references for placeholders');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,13 +2,14 @@ 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 { OPEN_COMMAND } from './commands/open-resource';
|
||||
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';
|
||||
import { Position } from '../core/model/position';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -119,16 +120,20 @@ export class NavigationProvider
|
||||
|
||||
const targetRange = section
|
||||
? section.range
|
||||
: Range.createFromPosition(
|
||||
targetResource.source.contentStart,
|
||||
targetResource.source.end
|
||||
);
|
||||
: Range.createFromPosition(Position.create(0, 0), Position.create(0, 0));
|
||||
const targetSelectionRange = section
|
||||
? section.range
|
||||
: Range.createFromPosition(targetRange.start);
|
||||
|
||||
const result: vscode.LocationLink = {
|
||||
originSelectionRange: toVsCodeRange(targetLink.range),
|
||||
originSelectionRange: new vscode.Range(
|
||||
targetLink.range.start.line,
|
||||
targetLink.range.start.character +
|
||||
(targetLink.type === 'wikilink' ? 2 : 0),
|
||||
targetLink.range.end.line,
|
||||
targetLink.range.end.character -
|
||||
(targetLink.type === 'wikilink' ? 2 : 0)
|
||||
),
|
||||
targetUri: toVsCodeUri(uri.asPlain()),
|
||||
targetRange: toVsCodeRange(targetRange),
|
||||
targetSelectionRange: toVsCodeRange(targetSelectionRange),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { ExtensionContext, commands, workspace } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { openDailyNoteFor } from '../dated-notes';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', openDailyNoteFor)
|
||||
);
|
||||
if (
|
||||
workspace.getConfiguration('foam').get('openDailyNote.onStartup', false)
|
||||
) {
|
||||
commands.executeCommand('foam-vscode.open-daily-note');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -40,9 +40,7 @@ const feature: FoamFeature = {
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
|
||||
...provider.commands,
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { isPlaceholderResource } from './placeholders';
|
||||
|
||||
describe('isPlaceholderResource', () => {
|
||||
it('should return true when a placeholder', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: '',
|
||||
links: [{ slug: 'placeholder' }],
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
expect(
|
||||
isPlaceholderResource(URI.placeholder('placeholder'), ws)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when an empty note is provided', () => {
|
||||
const noteA = createTestNote({ uri: 'note-a.md', text: '' });
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing only whitespace is provided', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '',
|
||||
text: ' \n\t\n\t ',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing only a title is provided', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '',
|
||||
text: '# Title',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing a title followed by whitespace is provided', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '',
|
||||
text: '# Title \n\t\n \t \n ',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when there is more than one line containing more than just whitespace', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '',
|
||||
text: '# Title\nA line that is not the title\nAnother line',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when there is at least one line of non-text content', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '',
|
||||
text: 'A line that is not the title\n',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
|
||||
expect(isPlaceholderResource(noteA.uri, ws)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getPlaceholdersConfig } from '../settings';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
GroupedResourcesTreeDataProvider,
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../utils/grouped-resources-tree-data-provider';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
@@ -25,16 +22,9 @@ const feature: FoamFeature = {
|
||||
'placeholder',
|
||||
getPlaceholdersConfig(),
|
||||
workspacesURIs,
|
||||
() =>
|
||||
foam.graph
|
||||
.getAllNodes()
|
||||
.filter(uri => isPlaceholderResource(uri, foam.workspace)),
|
||||
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
|
||||
uri => {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
return new ResourceTreeItem(resource, foam.workspace);
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -44,28 +34,9 @@ const feature: FoamFeature = {
|
||||
provider
|
||||
),
|
||||
...provider.commands,
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
export function isPlaceholderResource(uri: URI, workspace: FoamWorkspace) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const resource = workspace.find(uri);
|
||||
const contentLines =
|
||||
resource?.source.text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.filter(line => !line.startsWith('#')) ?? '';
|
||||
|
||||
return contentLines.length === 0;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,18 @@ import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
withModifiedFoamConfiguration,
|
||||
} from '../test/test-utils-vscode';
|
||||
import {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithFoamTags,
|
||||
markdownItWithNoteInclusion,
|
||||
markdownItWithRemoveLinkReferences,
|
||||
} from './preview-navigation';
|
||||
|
||||
const parser = createMarkdownParser();
|
||||
|
||||
describe('Link generation in preview', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: './path/to/note-a.md',
|
||||
@@ -22,7 +27,11 @@ describe('Link generation in preview', () => {
|
||||
links: [{ slug: 'placeholder' }],
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
const md = markdownItWithFoamLinks(MarkdownIt(), ws);
|
||||
|
||||
const md = [
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, ws), MarkdownIt());
|
||||
|
||||
it('generates a link to a note', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
@@ -41,6 +50,14 @@ describe('Link generation in preview', () => {
|
||||
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">random-text</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a wikilink even when there is a link reference', () => {
|
||||
const note = `[[note-a]]
|
||||
[note-a]: <note-a.md> "Note A"`;
|
||||
expect(md.render(note)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a>\n[note-a]: <note-a.md> "Note A"</p>\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stylable tag generation in preview', () => {
|
||||
@@ -60,23 +77,52 @@ describe('Stylable tag generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
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('should render an included note in flat mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the text of note A</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included note in container mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-a]]`);
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('This is the text of note A');
|
||||
}
|
||||
);
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section', async () => {
|
||||
@@ -99,15 +145,21 @@ This is the third section of note D
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 2]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
).toMatch(
|
||||
`<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);
|
||||
@@ -121,23 +173,26 @@ This is the third section of note D
|
||||
);
|
||||
});
|
||||
|
||||
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>
|
||||
`
|
||||
it('should display a warning in case of cyclical inclusions', async () => {
|
||||
const noteA = await createFile(
|
||||
'This is the text of note A which includes ![[note-b]]',
|
||||
['preview', 'note-a.md']
|
||||
);
|
||||
|
||||
const noteBText = 'This is the text of note B which includes ![[note-a]]';
|
||||
const noteB = await createFile(noteBText, ['preview', 'note-b.md']);
|
||||
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(noteA.uri, noteA.content))
|
||||
.set(parser.parse(noteB.uri, noteB.content));
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink: note-a');
|
||||
|
||||
deleteFile(noteA);
|
||||
deleteFile(noteB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,12 @@ 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[] = [];
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { Range } from '../core/model/range';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -31,6 +34,8 @@ const feature: FoamFeature = {
|
||||
},
|
||||
};
|
||||
|
||||
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
|
||||
const refsStack: string[] = [];
|
||||
export const markdownItWithNoteInclusion = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
@@ -56,22 +61,46 @@ export const markdownItWithNoteInclusion = (
|
||||
|
||||
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;
|
||||
}
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
const noteText = readFileSync(
|
||||
includedNote.uri.toFsPath()
|
||||
).toString();
|
||||
content = getFoamVsCodeConfig(CONFIG_EMBED_NOTE_IN_CONTAINER)
|
||||
? `<div class="embed-container-note">${md.render(noteText)}</div>`
|
||||
: noteText;
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
content = `
|
||||
<div class="embed-container-attachment">
|
||||
${md.renderInline('[[' + wikilink + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`
|
||||
)})`
|
||||
)}</div>`;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
@@ -92,22 +121,20 @@ export const markdownItWithFoamLinks = (
|
||||
regex: /\[\[([^[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
try {
|
||||
const linkHasAlias = wikilink.includes(ALIAS_DIVIDER_CHAR);
|
||||
const resourceLink = linkHasAlias
|
||||
? wikilink.substring(0, wikilink.indexOf('|'))
|
||||
: wikilink;
|
||||
const { target, alias } = MarkdownLink.analyzeLink({
|
||||
rawText: '[[' + wikilink + ']]',
|
||||
type: 'wikilink',
|
||||
range: Range.create(0, 0),
|
||||
});
|
||||
const label = isEmpty(alias) ? target : alias;
|
||||
|
||||
const resource = workspace.find(resourceLink);
|
||||
const resource = workspace.find(target);
|
||||
if (isNone(resource)) {
|
||||
return getPlaceholderLink(resourceLink);
|
||||
return getPlaceholderLink(label);
|
||||
}
|
||||
|
||||
const linkLabel = linkHasAlias
|
||||
? wikilink.substr(wikilink.indexOf('|') + 1)
|
||||
: wikilink;
|
||||
|
||||
const link = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='/${link}' data-href='/${link}'>${linkLabel}</a>`;
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='/${link}' data-href='/${link}'>${label}</a>`;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
@@ -155,18 +182,17 @@ export const markdownItWithRemoveLinkReferences = (
|
||||
) => {
|
||||
md.inline.ruler.before('link', 'clear-references', state => {
|
||||
if (state.env.references) {
|
||||
Object.keys(state.env.references).forEach(refKey => {
|
||||
// Forget about reference links that contain an alias divider
|
||||
// Aliased reference links will lead the MarkdownParser to include wrong link references
|
||||
if (refKey.includes(ALIAS_DIVIDER_CHAR)) {
|
||||
delete state.env.references[refKey];
|
||||
}
|
||||
const src = state.src.toLowerCase();
|
||||
const foamLinkRegEx = /\[\[([^[\]]+?)\]\]/g;
|
||||
const foamLinks = [...src.matchAll(foamLinkRegEx)].map(m =>
|
||||
m[1].toLowerCase()
|
||||
);
|
||||
|
||||
// When the reference is present due to an inclusion of that note, we
|
||||
// need to remove that reference. This ensures the MarkdownIt parser
|
||||
// will not replace the wikilink syntax with an <a href> link and as a result
|
||||
// break our inclusion logic.
|
||||
if (state.src.toLowerCase().includes(`![[${refKey.toLowerCase()}]]`)) {
|
||||
Object.keys(state.env.references).forEach(refKey => {
|
||||
// Remove all references that have corresponding wikilinks.
|
||||
// If the markdown parser sees a reference, it will format it before
|
||||
// we get a chance to create the wikilink.
|
||||
if (foamLinks.includes(refKey.toLowerCase())) {
|
||||
delete state.env.references[refKey];
|
||||
}
|
||||
});
|
||||
|
||||
228
packages/foam-vscode/src/features/refactor.spec.ts
Normal file
228
packages/foam-vscode/src/features/refactor.spec.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { wait, waitForExpect } from '../test/test-utils';
|
||||
import {
|
||||
closeEditors,
|
||||
createFile,
|
||||
cleanWorkspace,
|
||||
readFile,
|
||||
renameFile,
|
||||
showInEditor,
|
||||
runCommand,
|
||||
deleteFile,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { UPDATE_GRAPH_COMMAND_NAME } from './commands/update-graph';
|
||||
|
||||
describe('Note rename sync', () => {
|
||||
beforeAll(async () => {
|
||||
await closeEditors();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
afterAll(closeEditors);
|
||||
|
||||
describe('wikilinks', () => {
|
||||
it('should sync wikilinks to renamed notes', async () => {
|
||||
const noteA = await createFile(`Content of note A`, [
|
||||
'refactor',
|
||||
'wikilinks',
|
||||
'rename-note-a.md',
|
||||
]);
|
||||
const noteB = await createFile(
|
||||
`Link to [[${noteA.name}]]. Also a [[placeholder]] and again [[${noteA.name}]]`,
|
||||
['refactor', 'wikilinks', 'rename-note-b.md']
|
||||
);
|
||||
const noteC = await createFile(`Link to [[${noteA.name}]] from note C.`, [
|
||||
'refactor',
|
||||
'wikilinks',
|
||||
'rename-note-c.md',
|
||||
]);
|
||||
const { doc } = await showInEditor(noteB.uri);
|
||||
|
||||
const newName = 'renamed-note-a';
|
||||
const newUri = noteA.uri.resolve(newName);
|
||||
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
// check it updates documents open in editors
|
||||
expect(doc.getText().trim()).toEqual(
|
||||
`Link to [[${newName}]]. Also a [[placeholder]] and again [[${newName}]]`
|
||||
);
|
||||
// and documents not open in editors
|
||||
expect((await readFile(noteC.uri)).trim()).toEqual(
|
||||
`Link to [[${newName}]] from note C.`
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteB.uri);
|
||||
await deleteFile(noteC.uri);
|
||||
});
|
||||
|
||||
it('should use the best identifier based on the new note location', async () => {
|
||||
const noteA = await createFile(`Content of note A`, [
|
||||
'refactor',
|
||||
'wikilink',
|
||||
'first',
|
||||
'note-a.md',
|
||||
]);
|
||||
await createFile(`Content of note B`, [
|
||||
'refactor',
|
||||
'wikilink',
|
||||
'second',
|
||||
'note-b.md',
|
||||
]);
|
||||
const noteC = await createFile(`Link to [[${noteA.name}]] from note C.`);
|
||||
|
||||
const { doc } = await showInEditor(noteC.uri);
|
||||
|
||||
// rename note A
|
||||
const newUri = noteA.uri.resolve('note-b.md');
|
||||
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
expect(doc.getText().trim()).toEqual(
|
||||
`Link to [[first/note-b]] from note C.`
|
||||
);
|
||||
});
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteC.uri);
|
||||
});
|
||||
|
||||
it('should use the best identifier when moving the note to another directory', async () => {
|
||||
const noteA = await createFile(`Content of note A`, [
|
||||
'refactor',
|
||||
'wikilink',
|
||||
'first',
|
||||
'note-a.md',
|
||||
]);
|
||||
await createFile(`Content of note B`, [
|
||||
'refactor',
|
||||
'wikilink',
|
||||
'second',
|
||||
'note-b.md',
|
||||
]);
|
||||
const noteC = await createFile(`Link to [[${noteA.name}]] from note C.`);
|
||||
|
||||
const { doc } = await showInEditor(noteC.uri);
|
||||
|
||||
const newUri = noteA.uri.resolve('../second/note-a.md');
|
||||
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
expect(doc.getText().trim()).toEqual(`Link to [[note-a]] from note C.`);
|
||||
});
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteC.uri);
|
||||
});
|
||||
|
||||
it('should keep the alias in wikilinks', async () => {
|
||||
const noteA = await createFile(`Content of note A`);
|
||||
const noteB = await createFile(`Link to [[${noteA.name}|Alias]]`);
|
||||
|
||||
const { doc } = await showInEditor(noteB.uri);
|
||||
|
||||
// rename note A
|
||||
const newUri = noteA.uri.resolve('new-note-a.md');
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
expect(doc.getText().trim()).toEqual(`Link to [[new-note-a|Alias]]`);
|
||||
});
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteB.uri);
|
||||
});
|
||||
|
||||
it('should keep the section part of the wikilink', async () => {
|
||||
const noteA = await createFile(`Content of note A`);
|
||||
const noteB = await createFile(`Link to [[${noteA.name}#Section]]`);
|
||||
|
||||
const { doc } = await showInEditor(noteB.uri);
|
||||
|
||||
// rename note A
|
||||
const newUri = noteA.uri.resolve('new-note-with-section.md');
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
expect(doc.getText().trim()).toEqual(
|
||||
`Link to [[new-note-with-section#Section]]`
|
||||
);
|
||||
});
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteB.uri);
|
||||
});
|
||||
|
||||
it('should sync when moving the note to a new folder', async () => {
|
||||
const noteA = await createFile(`Content of note A`, [
|
||||
'refactor',
|
||||
'first',
|
||||
'note-a.md',
|
||||
]);
|
||||
const noteC = await createFile(`Link to [[note-a]] from note C.`);
|
||||
|
||||
const newUri = noteA.uri.resolve('../note-a.md');
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
const content = await readFile(noteC.uri);
|
||||
expect(content.trim()).toEqual(`Link to [[note-a]] from note C.`);
|
||||
});
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteC.uri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('direct links', () => {
|
||||
beforeAll(async () => {
|
||||
await closeEditors();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
beforeEach(closeEditors);
|
||||
|
||||
it('should rename relative direct links', async () => {
|
||||
const noteA = await createFile(
|
||||
`Content of note A. Lorem etc etc etc etc`,
|
||||
['refactor', 'direct-links', 'f1', 'note-a.md']
|
||||
);
|
||||
const noteB = await createFile(
|
||||
`Link to [note](../f1/note-a.md) from note B.`,
|
||||
['refactor', 'direct-links', 'f2', 'note-b.md']
|
||||
);
|
||||
const { doc } = await showInEditor(noteB.uri);
|
||||
|
||||
const newUri = noteA.uri.resolve('../note-a.md');
|
||||
// wait for the rename events to be propagated
|
||||
await wait(1000);
|
||||
await runCommand(UPDATE_GRAPH_COMMAND_NAME);
|
||||
await renameFile(noteA.uri, newUri);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
expect(doc.getText().trim()).toEqual(
|
||||
`Link to [note](../note-a.md) from note B.`
|
||||
);
|
||||
});
|
||||
|
||||
await deleteFile(newUri);
|
||||
await deleteFile(noteB.uri);
|
||||
});
|
||||
});
|
||||
});
|
||||
108
packages/foam-vscode/src/features/refactor.ts
Normal file
108
packages/foam-vscode/src/features/refactor.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { isAbsolute } from '../core/utils/path';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { FoamFeature } from '../types';
|
||||
import { fromVsCodeUri, toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onWillRenameFiles(async e => {
|
||||
if (!getFoamVsCodeConfig<boolean>('links.sync.enable')) {
|
||||
return;
|
||||
}
|
||||
const renameEdits = new vscode.WorkspaceEdit();
|
||||
e.files.forEach(({ oldUri, newUri }) => {
|
||||
const connections = foam.graph.getBacklinks(fromVsCodeUri(oldUri));
|
||||
connections.forEach(async connection => {
|
||||
const { target } = MarkdownLink.analyzeLink(connection.link);
|
||||
switch (connection.link.type) {
|
||||
case 'wikilink': {
|
||||
const identifier = foam.workspace.getIdentifier(
|
||||
fromVsCodeUri(newUri),
|
||||
[fromVsCodeUri(oldUri)]
|
||||
);
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(
|
||||
connection.link,
|
||||
{ target: identifier }
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'link': {
|
||||
const path = isAbsolute(target)
|
||||
? '/' + vscode.workspace.asRelativePath(newUri)
|
||||
: fromVsCodeUri(newUri).relativeTo(
|
||||
connection.source.getDirectory()
|
||||
).path;
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(
|
||||
connection.link,
|
||||
{ target: path }
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
if (renameEdits.size > 0) {
|
||||
// We break the update by file because applying it at once was causing
|
||||
// dirty state and editors not always saving or closing
|
||||
for (const renameEditForUri of renameEdits.entries()) {
|
||||
const [uri, edits] = renameEditForUri;
|
||||
const fileEdits = new vscode.WorkspaceEdit();
|
||||
fileEdits.set(uri, edits);
|
||||
await vscode.workspace.applyEdit(fileEdits);
|
||||
const editor = await vscode.workspace.openTextDocument(uri);
|
||||
// Because the save happens within 50ms of opening the doc, it will be then closed
|
||||
editor.save();
|
||||
}
|
||||
|
||||
// Reporting
|
||||
const nUpdates = renameEdits.entries().reduce((acc, entry) => {
|
||||
return (acc += entry[1].length);
|
||||
}, 0);
|
||||
const links = nUpdates > 1 ? 'links' : 'link';
|
||||
const nFiles = renameEdits.size;
|
||||
const files = nFiles > 1 ? 'files' : 'file';
|
||||
Logger.info(
|
||||
`Updated links in the following files:`,
|
||||
...renameEdits
|
||||
.entries()
|
||||
.map(e => vscode.workspace.asRelativePath(e[0]))
|
||||
);
|
||||
vscode.window.showInformationMessage(
|
||||
`Updated ${nUpdates} ${links} across ${nFiles} ${files}.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error while updating references to file', e);
|
||||
vscode.window.showErrorMessage(
|
||||
`Foam couldn't update the links to ${vscode.workspace.asRelativePath(
|
||||
e.newUri
|
||||
)}. Check the logs for error details.`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
export default feature;
|
||||
@@ -1,16 +1,19 @@
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { createTestNote, readFileFromFs } 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/services/markdown-provider';
|
||||
import { FileDataStore, Matcher } from '../core/services/datastore';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const matcher = new Matcher([]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
@@ -22,7 +25,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_foam = await bootstrap(matcher, dataStore, parser, [mdProvider]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
@@ -21,9 +21,7 @@ const feature: FoamFeature = {
|
||||
provider
|
||||
)
|
||||
);
|
||||
foam.workspace.onDidUpdate(() => provider.refresh());
|
||||
foam.workspace.onDidAdd(() => provider.refresh());
|
||||
foam.workspace.onDidDelete(() => provider.refresh());
|
||||
foam.tags.onDidUpdate(() => provider.refresh());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -111,7 +109,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
|
||||
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
|
||||
if (item instanceof TagReference) {
|
||||
const content = await this.workspace.read(item.note.uri);
|
||||
const content = await this.workspace.readAsMarkdown(item.note.uri);
|
||||
if (isSome(content)) {
|
||||
item.tooltip = getNoteTooltip(content);
|
||||
}
|
||||
@@ -166,16 +164,14 @@ export class TagReference extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: Tag, public readonly note: Resource) {
|
||||
super(note.title, vscode.TreeItemCollapsibleState.None);
|
||||
const uri = toVsCodeUri(note.uri);
|
||||
this.title = note.title;
|
||||
this.description = note.uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(note.uri))?.uri.path,
|
||||
''
|
||||
);
|
||||
this.description = vscode.workspace.asRelativePath(uri);
|
||||
this.tooltip = undefined;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [
|
||||
note.uri,
|
||||
uri,
|
||||
{
|
||||
preview: true,
|
||||
selection: toVsCodeRange(tag.range),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Foam } from '../core/model/foam';
|
||||
import { Resource, ResourceParser } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNone } from '../utils';
|
||||
import {
|
||||
@@ -131,7 +132,7 @@ export function updateDiagnostics(
|
||||
|
||||
for (const link of resource.links) {
|
||||
if (link.type === 'wikilink') {
|
||||
const [target, section] = link.target.split('#');
|
||||
const { target, section } = MarkdownLink.analyzeLink(link);
|
||||
const targets = workspace.listByIdentifier(target);
|
||||
if (targets.length > 1) {
|
||||
result.push({
|
||||
|
||||
88
packages/foam-vscode/src/services/cache.ts
Normal file
88
packages/foam-vscode/src/services/cache.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { debounce } from 'lodash';
|
||||
import LRU from 'lru-cache';
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import {
|
||||
ParserCache,
|
||||
ParserCacheEntry,
|
||||
} from '../core/services/markdown-parser';
|
||||
import { Logger } from '../core/utils/log';
|
||||
|
||||
/**
|
||||
* This is a best effort implementation to cache resources.
|
||||
* It's not a perfect solution, but it's a good start.
|
||||
*
|
||||
* We use the URI and a checksum of the markdown file to cache the resource.
|
||||
*/
|
||||
export default class VsCodeBasedParserCache implements ParserCache {
|
||||
static CACHE_NAME = 'foam-cache';
|
||||
private _cache: LRU<string, ParserCacheEntry>;
|
||||
|
||||
constructor(private context: ExtensionContext, size = 10000) {
|
||||
this._cache = new LRU({
|
||||
max: size,
|
||||
updateAgeOnGet: true,
|
||||
updateAgeOnHas: false,
|
||||
});
|
||||
const source = context.workspaceState.get(
|
||||
VsCodeBasedParserCache.CACHE_NAME,
|
||||
[]
|
||||
);
|
||||
try {
|
||||
this._cache.load(source);
|
||||
} catch (e) {
|
||||
Logger.warn(`Failed to load cache: ${e}`);
|
||||
this.clear();
|
||||
}
|
||||
Logger.debug('Cache size: ' + this._cache.size);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._cache.clear();
|
||||
this.context.workspaceState.update(VsCodeBasedParserCache.CACHE_NAME, []);
|
||||
}
|
||||
|
||||
get(uri: URI): ParserCacheEntry {
|
||||
const result = this._cache.get(uri.toString());
|
||||
if (result) {
|
||||
// The cache returns a plain object, but we need an actual
|
||||
// instance of URI in the resource (we check instanceof in the code),
|
||||
// so to be sure we convert it here.
|
||||
const { checksum, resource } = result;
|
||||
const rehydrated = {
|
||||
...resource,
|
||||
uri: new URI(resource.uri),
|
||||
};
|
||||
return {
|
||||
checksum,
|
||||
resource: rehydrated,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has(uri: URI): boolean {
|
||||
return this._cache.has(uri.toString());
|
||||
}
|
||||
|
||||
set(uri: URI, entry: ParserCacheEntry): void {
|
||||
this._cache.set(uri.toString(), entry);
|
||||
delayedSync(this._cache, this.context);
|
||||
}
|
||||
|
||||
del(uri: URI): void {
|
||||
this._cache.delete(uri.toString());
|
||||
delayedSync(this._cache, this.context);
|
||||
}
|
||||
}
|
||||
|
||||
const delayedSync = debounce(
|
||||
(cache: LRU<string, ParserCacheEntry>, context) => {
|
||||
Logger.debug('Updating parser cache');
|
||||
context.workspaceState.update(
|
||||
VsCodeBasedParserCache.CACHE_NAME,
|
||||
cache.dump()
|
||||
);
|
||||
},
|
||||
1000
|
||||
);
|
||||
@@ -4,8 +4,8 @@ export interface ConfigurationMonitor<T> extends Disposable {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export const getFoamVsCodeConfig = <T>(key: string): T =>
|
||||
workspace.getConfiguration('foam').get(key);
|
||||
export const getFoamVsCodeConfig = <T>(key: string, defaultValue?: T): T =>
|
||||
workspace.getConfiguration('foam').get(key, defaultValue);
|
||||
|
||||
export const updateFoamVsCodeConfig = <T>(key: string, value: T) =>
|
||||
workspace.getConfiguration().update('foam.' + key, value);
|
||||
|
||||
@@ -48,8 +48,9 @@ export async function createDocAndFocus(
|
||||
toVsCodeUri(filepath),
|
||||
new TextEncoder().encode('')
|
||||
);
|
||||
await focusNote(filepath, true, viewColumn);
|
||||
await window.activeTextEditor.insertSnippet(text);
|
||||
const note = await focusNote(filepath, true, viewColumn);
|
||||
await note.editor.insertSnippet(text);
|
||||
await note.document.save();
|
||||
}
|
||||
|
||||
export async function replaceSelection(
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('Create note from template', () => {
|
||||
const templateA = await createFile(
|
||||
`---
|
||||
foam_template: # foam template metadata
|
||||
filepath: "${uri.toFsPath()}"
|
||||
filepath: ${uri.toFsPath()}
|
||||
---
|
||||
`,
|
||||
['.foam', 'templates', 'template-with-path.md']
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { existsSync } from 'fs';
|
||||
import { TextEncoder } from 'util';
|
||||
import { SnippetString, ViewColumn, window, workspace } from 'vscode';
|
||||
import { FileType, SnippetString, ViewColumn, window, workspace } from 'vscode';
|
||||
import { focusNote } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
replaceSelection,
|
||||
} from './editor';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
/**
|
||||
* The templates directory
|
||||
@@ -69,6 +69,32 @@ export async function getTemplates(): Promise<URI[]> {
|
||||
return templates;
|
||||
}
|
||||
|
||||
export async function getTemplateInfo(
|
||||
templateUri: URI,
|
||||
templateFallbackText = '',
|
||||
resolver: Resolver
|
||||
) {
|
||||
const templateText = (await fileExists(templateUri))
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
: templateFallbackText;
|
||||
|
||||
const templateWithResolvedVariables = await resolver.resolveText(
|
||||
templateText
|
||||
);
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
|
||||
return {
|
||||
metadata: templateMetadata,
|
||||
text: templateWithFoamFrontmatterRemoved,
|
||||
};
|
||||
}
|
||||
|
||||
export const NoteFactory = {
|
||||
/**
|
||||
* Creates a new note using a template.
|
||||
@@ -81,72 +107,67 @@ export const NoteFactory = {
|
||||
templateUri: URI,
|
||||
resolver: Resolver,
|
||||
filepathFallbackURI?: URI,
|
||||
templateFallbackText = ''
|
||||
): Promise<void> => {
|
||||
const templateText = existsSync(templateUri.toFsPath())
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
: templateFallbackText;
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
if (selectedContent?.content) {
|
||||
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content);
|
||||
}
|
||||
|
||||
let templateWithResolvedVariables: string;
|
||||
templateFallbackText = '',
|
||||
onFileExists?: (filePath: URI) => Promise<string | undefined>
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
try {
|
||||
templateWithResolvedVariables = await resolver.resolveText(templateText);
|
||||
onFileExists = onFileExists
|
||||
? onFileExists
|
||||
: (existingFile: URI) => {
|
||||
const filename = existingFile.getBasename();
|
||||
return askUserForFilepathConfirmation(existingFile, filename);
|
||||
};
|
||||
|
||||
const template = await getTemplateInfo(
|
||||
templateUri,
|
||||
templateFallbackText,
|
||||
resolver
|
||||
);
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
if (selectedContent?.content) {
|
||||
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content);
|
||||
}
|
||||
|
||||
const templateSnippet = new SnippetString(template.text);
|
||||
|
||||
let newFilePath = await determineNewNoteFilepath(
|
||||
template.metadata.get('filepath'),
|
||||
filepathFallbackURI,
|
||||
resolver
|
||||
);
|
||||
while (await fileExists(newFilePath)) {
|
||||
const proposedNewFilepath = await onFileExists(newFilePath);
|
||||
|
||||
if (proposedNewFilepath === undefined) {
|
||||
return { didCreateFile: false, uri: newFilePath };
|
||||
}
|
||||
newFilePath = URI.file(proposedNewFilepath);
|
||||
}
|
||||
|
||||
await createDocAndFocus(
|
||||
templateSnippet,
|
||||
newFilePath,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
const newNoteTitle = newFilePath.getName();
|
||||
|
||||
await replaceSelection(
|
||||
selectedContent.document,
|
||||
selectedContent.selection,
|
||||
`[[${newNoteTitle}]]`
|
||||
);
|
||||
}
|
||||
|
||||
return { didCreateFile: true, uri: newFilePath };
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(
|
||||
templateWithFoamFrontmatterRemoved
|
||||
);
|
||||
|
||||
let filepath = await determineNewNoteFilepath(
|
||||
templateMetadata.get('filepath'),
|
||||
filepathFallbackURI,
|
||||
resolver
|
||||
);
|
||||
|
||||
if (existsSync(filepath.toFsPath())) {
|
||||
const filename = filepath.getBasename();
|
||||
const newFilepath = await askUserForFilepathConfirmation(
|
||||
filepath,
|
||||
filename
|
||||
);
|
||||
|
||||
if (newFilepath === undefined) {
|
||||
return;
|
||||
}
|
||||
filepath = URI.file(newFilepath);
|
||||
}
|
||||
|
||||
await createDocAndFocus(
|
||||
templateSnippet,
|
||||
filepath,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
const newNoteTitle = filepath.getName();
|
||||
|
||||
await replaceSelection(
|
||||
selectedContent.document,
|
||||
selectedContent.selection,
|
||||
`[[${newNoteTitle}]]`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -158,13 +179,17 @@ export const NoteFactory = {
|
||||
filepathFallbackURI: URI,
|
||||
templateFallbackText: string,
|
||||
targetDate: Date
|
||||
): Promise<void> => {
|
||||
const resolver = new Resolver(new Map(), targetDate);
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
const resolver = new Resolver(
|
||||
new Map().set('FOAM_TITLE', dateFormat(targetDate, 'yyyy-mm-dd', false)),
|
||||
targetDate
|
||||
);
|
||||
return NoteFactory.createFromTemplate(
|
||||
DAILY_NOTE_TEMPLATE_URI,
|
||||
resolver,
|
||||
filepathFallbackURI,
|
||||
templateFallbackText
|
||||
templateFallbackText,
|
||||
_ => Promise.resolve(undefined)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -176,7 +201,7 @@ export const NoteFactory = {
|
||||
createForPlaceholderWikilink: (
|
||||
wikilinkPlaceholder: string,
|
||||
filepathFallbackURI: URI
|
||||
): Promise<void> => {
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
const resolver = new Resolver(
|
||||
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
|
||||
new Date()
|
||||
@@ -198,10 +223,10 @@ export const createTemplate = async (): Promise<void> => {
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
validateInput: async value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
: (await fileExists(URI.parse(value)))
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
@@ -226,10 +251,10 @@ async function askUserForFilepathConfirmation(
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
validateInput: async value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
: (await fileExists(URI.parse(value)))
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
@@ -260,3 +285,12 @@ export async function determineNewNoteFilepath(
|
||||
);
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
async function fileExists(uri: URI): Promise<boolean> {
|
||||
try {
|
||||
const stat = await workspace.fs.stat(toVsCodeUri(uri));
|
||||
return stat.type === FileType.File;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
30
packages/foam-vscode/src/services/watcher.ts
Normal file
30
packages/foam-vscode/src/services/watcher.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IDisposable } from '../core/common/lifecycle';
|
||||
import { Emitter } from '../core/common/event';
|
||||
import { IWatcher } from '../core/services/datastore';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FileSystemWatcher } from 'vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export class VsCodeWatcher implements IWatcher, IDisposable {
|
||||
public onDidCreateEmitter = new Emitter<URI>();
|
||||
public onDidChangeEmitter = new Emitter<URI>();
|
||||
public onDidDeleteEmitter = new Emitter<URI>();
|
||||
onDidCreate = this.onDidCreateEmitter.event;
|
||||
onDidChange = this.onDidChangeEmitter.event;
|
||||
onDidDelete = this.onDidDeleteEmitter.event;
|
||||
|
||||
constructor(private readonly vsCodeWatcher: FileSystemWatcher) {
|
||||
vsCodeWatcher.onDidCreate(uri =>
|
||||
this.onDidCreateEmitter.fire(fromVsCodeUri(uri))
|
||||
);
|
||||
vsCodeWatcher.onDidChange(uri =>
|
||||
this.onDidChangeEmitter.fire(fromVsCodeUri(uri))
|
||||
);
|
||||
vsCodeWatcher.onDidDelete(uri =>
|
||||
this.onDidDeleteEmitter.fire(fromVsCodeUri(uri))
|
||||
);
|
||||
}
|
||||
dispose(): void {
|
||||
this.vsCodeWatcher.dispose();
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,9 @@ async function main() {
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
throw new Error('Some Foam tests failed');
|
||||
// throw new Error('Some Foam tests failed');
|
||||
console.log('Some Foam tests failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import path from 'path';
|
||||
import { TextEncoder } from 'util';
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { URI } from '../core/model/uri';
|
||||
@@ -64,13 +64,23 @@ export const createFile = async (content: string, filepath: string[] = []) => {
|
||||
return { uri, content, ...filenameComponents };
|
||||
};
|
||||
|
||||
export const renameFile = (from: URI, to: URI) => {
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
edit.renameFile(toVsCodeUri(from), toVsCodeUri(to));
|
||||
return vscode.workspace.applyEdit(edit);
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
export const readFile = async (uri: URI) => {
|
||||
const content = await vscode.workspace.fs.readFile(toVsCodeUri(uri));
|
||||
return decoder.decode(content);
|
||||
};
|
||||
|
||||
export const createNote = (r: Resource) => {
|
||||
const content = `# ${r.title}
|
||||
|
||||
some content and ${r.links
|
||||
.map(l =>
|
||||
l.type === 'wikilink' ? `[[${l.label}]]` : `[${l.label}](${l.target})`
|
||||
)
|
||||
.map(l => l.rawText)
|
||||
.join(' some content between links.\n')}
|
||||
last line.
|
||||
`;
|
||||
@@ -79,3 +89,32 @@ export const createNote = (r: Resource) => {
|
||||
new TextEncoder().encode(content)
|
||||
);
|
||||
};
|
||||
|
||||
export const runCommand = async <T>(command: string, args: T = undefined) =>
|
||||
vscode.commands.executeCommand(command, args);
|
||||
|
||||
/**
|
||||
* Runs a function with a modified configuration and
|
||||
* restores the original configuration afterwards
|
||||
*
|
||||
* @param key the key of the configuration to modify
|
||||
* @param value the value to set the configuration to
|
||||
* @param fn the function to execute
|
||||
*/
|
||||
export const withModifiedConfiguration = async (key, value, fn: () => void) => {
|
||||
const old = vscode.workspace.getConfiguration().get(key);
|
||||
await vscode.workspace.getConfiguration().update(key, value);
|
||||
await fn();
|
||||
await vscode.workspace.getConfiguration().update(key, old);
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a function with a modified Foam configuration and
|
||||
* restores the original configuration afterwards
|
||||
*
|
||||
* @param key the key of the Foam configuration to modify
|
||||
* @param value the value to set the configuration to
|
||||
* @param fn the function to execute
|
||||
*/
|
||||
export const withModifiedFoamConfiguration = (key, value, fn: () => void) =>
|
||||
withModifiedConfiguration(`foam.${key}`, value, fn);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* This file should not depend on VS Code as it's used for unit tests
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { Range } from '../core/model/range';
|
||||
import { URI } from '../core/model/uri';
|
||||
@@ -8,6 +9,9 @@ import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Matcher } from '../core/services/datastore';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { NoteLinkDefinition, Resource } from '../core/model/note';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
|
||||
export { default as waitForExpect } from 'wait-for-expect';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -19,10 +23,6 @@ export const TEST_DATA_DIR = URI.file(__dirname).joinPath(
|
||||
|
||||
const position = Range.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
const eol = '\n';
|
||||
|
||||
/**
|
||||
* Turns a string into a URI
|
||||
* The goal of this function is to make sure we are consistent in the
|
||||
@@ -33,10 +33,15 @@ export const strToUri = URI.file;
|
||||
export const createTestWorkspace = () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*']);
|
||||
const provider = new MarkdownResourceProvider(matcher, undefined, undefined, {
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
});
|
||||
const parser = createMarkdownParser();
|
||||
const provider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
{
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
},
|
||||
parser
|
||||
);
|
||||
workspace.registerProvider(provider);
|
||||
return workspace;
|
||||
};
|
||||
@@ -47,6 +52,7 @@ export const createTestNote = (params: {
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
tags?: string[];
|
||||
aliases?: string[];
|
||||
text?: string;
|
||||
sections?: string[];
|
||||
root?: URI;
|
||||
@@ -67,6 +73,11 @@ export const createTestNote = (params: {
|
||||
label: t,
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
})) ?? [],
|
||||
aliases:
|
||||
params.aliases?.map(a => ({
|
||||
title: a,
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
})) ?? [],
|
||||
links: params.links
|
||||
? params.links.map((link, index) => {
|
||||
const range = Range.create(
|
||||
@@ -78,26 +89,16 @@ export const createTestNote = (params: {
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
target: link.slug,
|
||||
label: link.slug,
|
||||
range: range,
|
||||
rawText: 'link text',
|
||||
rawText: `[[${link.slug}]]`,
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
target: link.to,
|
||||
label: 'link text',
|
||||
range: range,
|
||||
rawText: 'link text',
|
||||
rawText: `[link text](${link.to})`,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
source: {
|
||||
eol: eol,
|
||||
end: documentEnd,
|
||||
contentStart: documentStart,
|
||||
text: params.text ?? '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,3 +114,7 @@ export const randomString = (len = 5) =>
|
||||
|
||||
export const getRandomURI = () =>
|
||||
URI.file('/random-uri-root/' + randomString() + '.md');
|
||||
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
export const readFileFromFs = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
|
||||
@@ -161,6 +161,8 @@ export async function focusNote(
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
|
||||
return { document, editor };
|
||||
}
|
||||
|
||||
export function getContainsTooltip(titles: string[]): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { OPEN_COMMAND } from '../features/utility-commands';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import {
|
||||
GroupedResoucesConfigGroupBy,
|
||||
GroupedResourcesConfig,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
GroupedResoucesConfigGroupBy,
|
||||
} from '../settings';
|
||||
import { getContainsTooltip, getNoteTooltip, isSome } from '../utils';
|
||||
import { OPEN_COMMAND } from '../features/utility-commands';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import { toVsCodeUri } from './vsc-utils';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
@@ -16,3 +16,22 @@
|
||||
background-color: var(--vscode-editorError-background);
|
||||
color: var(--vscode-editorError-foreground);
|
||||
}
|
||||
|
||||
.embed-container-note {
|
||||
padding: 0.5em;
|
||||
margin: 1.5em 0;
|
||||
border: 1px solid var(--vscode-editorLineNumber-foreground);
|
||||
}
|
||||
|
||||
.embed-container-attachment {
|
||||
padding: 0.25em;
|
||||
margin: 1.5em 0;
|
||||
text-align: center;
|
||||
border: 1px solid var(--vscode-editorLineNumber-foreground);
|
||||
}
|
||||
|
||||
.embed-container-image {
|
||||
margin: auto;
|
||||
padding: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user