mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83a90177b9 | ||
|
|
37aec28af6 | ||
|
|
447f7fc068 | ||
|
|
ad1243665a | ||
|
|
f07de73bc4 | ||
|
|
c431ccfb62 | ||
|
|
f31ef897cc | ||
|
|
7a5f45c0ce | ||
|
|
df32d9e708 | ||
|
|
b3d4691bfa | ||
|
|
f5260f7d3f | ||
|
|
9b4b7ec84d | ||
|
|
52b7f86a9f | ||
|
|
2db7060124 | ||
|
|
a4f04b3b6b | ||
|
|
b5a8a5d7c7 | ||
|
|
f5a29e431c | ||
|
|
5a7a1ba89f | ||
|
|
b054bafc78 | ||
|
|
8acb60253a | ||
|
|
3c69508dcb | ||
|
|
a368be9b47 | ||
|
|
b1f76bb653 | ||
|
|
d4bc16b9bd | ||
|
|
882b0b6012 | ||
|
|
048623d910 | ||
|
|
f2fbe927ae | ||
|
|
d0ee71be1b | ||
|
|
2a14dc0c57 | ||
|
|
745acbabd3 | ||
|
|
c226cddcd8 | ||
|
|
1b0d3239b5 | ||
|
|
cddf3bc769 | ||
|
|
bd158e9b8e |
@@ -1004,6 +1004,24 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Skakerman",
|
||||
"name": "Scott Akerman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15224439?v=4",
|
||||
"profile": "http://scottakerman.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jimgraham",
|
||||
"name": "Jim Graham",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/430293?v=4",
|
||||
"profile": "http://www.jim-graham.net/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -6,21 +6,22 @@
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": { "node": true, "es6": true },
|
||||
"plugins": ["@typescript-eslint", "import", "jest"],
|
||||
"plugins": ["jest"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-redeclare": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -11,16 +11,16 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12'
|
||||
node-version: '18'
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
name: Build and Test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-10.15, ubuntu-18.04, windows-2019]
|
||||
os: [macos-12, ubuntu-22.04, windows-2022]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
@@ -44,11 +44,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12'
|
||||
node-version: '18'
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
|
||||
43
.github/workflows/update-docs.yml
vendored
Normal file
43
.github/workflows/update-docs.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- docs/user/**/*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: foambubble/foam-template
|
||||
path: foam-template
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: foam
|
||||
- name: Copy and fixup user docs files
|
||||
id: copy
|
||||
run: |
|
||||
rm -r foam-template/docs
|
||||
cp -r foam/docs/user foam-template/docs
|
||||
|
||||
# Strip autogenerated wikileaks references because
|
||||
# they are not an appropriate default user experience.
|
||||
(cd foam-template/docs; sed -i '/\[\/\/begin\]/,/\[\/\/end\]/d' $(find . -type f -name \*.md))
|
||||
|
||||
# Set the commit message format
|
||||
echo "message=Docs sync @ $(cd foam; git log --pretty='format:%h %s')" >> $GITHUB_OUTPUT
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.FOAM_DOCS_SYNC_TOKEN }}
|
||||
path: foam-template
|
||||
commit-message: ${{ steps.copy.outputs.message }}
|
||||
branch: bot/foam-docs-sync
|
||||
delete-branch: true
|
||||
title: Sync docs from foam
|
||||
body: Copy docs from main foam repo
|
||||
@@ -247,6 +247,8 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://yongliangliu.com"><img src="https://avatars.githubusercontent.com/u/41845017?v=4?s=60" width="60px;" alt="Liu YongLiang"/><br /><sub><b>Liu YongLiang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tlylt" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://scottakerman.com"><img src="https://avatars.githubusercontent.com/u/15224439?v=4?s=60" width="60px;" alt="Scott Akerman"/><br /><sub><b>Scott Akerman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skakerman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -9,16 +9,19 @@ In particular, some commands can be very customizible and can help with custom w
|
||||
This command creates a note.
|
||||
Although it works fine on its own, it can be customized to achieve various use cases.
|
||||
Here are the settings available for the command:
|
||||
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
|
||||
|
||||
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
|
||||
- templatePath: The path of the template to use. If relative it will be resolved against the workspace root.
|
||||
- title: The title of the note (that is, the `FOAM_TITLE` variable)
|
||||
- text: The text to use for the note. If also a template is provided, the template has precedence
|
||||
- variables: Variables to use in the text or template (e.g. `FOAM_TITLE`)
|
||||
- date: The date used to resolve the FOAM_DATE_* variables. in `YYYY-MM-DD` format
|
||||
- variables: Variables to use in the text or template
|
||||
- date: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
|
||||
- onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel': What to do in case the target file already exists
|
||||
|
||||
To customize a command and associate a key binding to it, open the key binding settings and add the appropriate configuration, here are some examples:
|
||||
|
||||
- Create a note called `test note.md` with some text. If the note already exists, ask for a new name
|
||||
|
||||
```
|
||||
{
|
||||
"key": "alt+f",
|
||||
@@ -32,6 +35,7 @@ To customize a command and associate a key binding to it, open the key binding s
|
||||
```
|
||||
|
||||
- Create a note following the `weekly-note.md` template. If the note already exists, open it
|
||||
|
||||
```
|
||||
{
|
||||
"key": "alt+g",
|
||||
@@ -43,3 +47,27 @@ To customize a command and associate a key binding to it, open the key binding s
|
||||
}
|
||||
```
|
||||
|
||||
## foam-vscode.open-resource command
|
||||
|
||||
This command opens a resource.
|
||||
|
||||
Normally it receives a `URI`, which identifies the resource to open.
|
||||
|
||||
It is also possible to pass in a filter, which will be run against the workspace resources to find one or more matches.
|
||||
|
||||
- If there is one match, it will be opened
|
||||
- If there is more than one match, a quick pick will show up allowing the user to select the desired resource
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
{
|
||||
"key": "alt+f",
|
||||
"command": "foam-vscode.open-resource",
|
||||
"args": {
|
||||
"filter": {
|
||||
"title": "Weekly Note*"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
42
docs/user/features/resouce-filters.md
Normal file
42
docs/user/features/resouce-filters.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Resource Filters
|
||||
|
||||
Resource filters can be passed to some Foam commands to limit their scope.
|
||||
|
||||
A filter supports the following parameters:
|
||||
|
||||
- `tag`: include a resource if it has the given tag (e.g. `{"tag": "#research"}`)
|
||||
- `type`: include a resource if it is of the given type (e.g. `{"type": "daily-note"}`)
|
||||
- `path`: include a resource if its path matches the given regex (e.g. `{"path": "/projects/*"}`). **Note that this parameter supports regex and not globs.**
|
||||
- `expression`: include a resource if it makes the given expression `true`, where `resource` represents the resource being evaluated (e.g. `{"expression": "resource.type ==='weekly-note'"}`)
|
||||
- `title`: include a resource if the title matches the given regex (e.g. `{"title": "Team meeting:*"}`)
|
||||
|
||||
A filter also supports some logical operators:
|
||||
|
||||
- `and`: include a resource if it matches all the sub-parameters (e.g `{"and": [{"tag": "#research"}, {"title": "Paper *"}]}`)
|
||||
- `or`: include a resource if it matches any of the sub-parameters (e.g `{"or": [{"tag": "#research"}, {"title": "Paper *"}]}`)
|
||||
- `not`: invert the result of the nested filter (e.g. `{"not": {"type": "daily-note"}}`)
|
||||
|
||||
Here is an example of a complex filter, for example to show the Foam graph only of a subset of the workspace:
|
||||
|
||||
```
|
||||
{
|
||||
"key": "alt+f",
|
||||
"command": "foam-vscode.show-graph",
|
||||
"args": {
|
||||
"filter": {
|
||||
"and": [
|
||||
{
|
||||
"or": [
|
||||
{ "type": 'daily-note' },
|
||||
{ "type": 'weekly-note' },
|
||||
{ "path": '/projects/*' },
|
||||
],
|
||||
"not": {
|
||||
{ "tag": '#b' },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -5,6 +5,7 @@
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [Links/Graphs/BackLinks don't work. How do I enable them?](#linksgraphsbacklinks-dont-work-how-do-i-enable-them)
|
||||
- [I don't want Foam enabled for all my workspaces](#i-dont-want-foam-enabled-for-all-my-workspaces)
|
||||
- [I want to publish the graph view to GitHub pages or Vercel](#i-want-to-publish-the-graph-view-to-github-pages-or-vercel)
|
||||
|
||||
## Links/Graphs/BackLinks don't work. How do I enable them?
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Using Foam
|
||||
|
||||
Foam is a collection VS Code extensions and recipes that power up the editor into a full-blown note taking system.
|
||||
This folder contains user documentation describing how to get started using Foam, what its main features are, and strategies for getting the most out of Foam.
|
||||
The full docs are included in the `foam-template` repo that most users start from.
|
||||
Foam is a collection VS Code extensions and recipes that power up the editor
|
||||
into a full-blown note taking system. This folder contains user documentation
|
||||
describing how to get started using Foam, what its main features are, and
|
||||
strategies for getting the most out of Foam. The full docs are included in the
|
||||
`foam-template` repo that most users start from.
|
||||
|
||||
> See also [[frequently-asked-questions]].
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.20.6"
|
||||
"version": "0.21.4"
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"all-contributors-cli": "^6.16.1",
|
||||
"lerna": "^3.22.1"
|
||||
"lerna": "^6.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -33,6 +33,7 @@
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
|
||||
@@ -4,6 +4,56 @@ 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.21.4] - 2023-04-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed issue with generated daily note template due to path escape (#1188, #1190)
|
||||
|
||||
## [0.21.3] - 2023-04-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed relative path from workspace root in templates (#1188)
|
||||
|
||||
## [0.21.2] - 2023-04-11
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed embed with relative paths (#1168, #1170)
|
||||
- Improved multi-root folder support for daily notes (#1126, #1175)
|
||||
- Improved use of tag completion (#1183 - thanks @jimgraham)
|
||||
- Fixed relative path use in note creation when using templates (#1170)
|
||||
|
||||
Internal:
|
||||
|
||||
- Sync user docs with foam-template docs (#1180 - thanks @infogulch)
|
||||
|
||||
## [0.21.1] - 2023-02-24
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed note creation from placeholder (#1172)
|
||||
|
||||
## [0.21.0] - 2023-02-16
|
||||
|
||||
Features:
|
||||
|
||||
- Added support for filters for the `foam-vscode.open-resource` command (#1161)
|
||||
|
||||
## [0.20.8] - 2023-02-10
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated most dependencies (#1160)
|
||||
|
||||
## [0.20.7] - 2023-01-31
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Inform the user that directory renaming is not supported (#1143)
|
||||
- Fixed extra `web` directory in published extension (#1152 - thanks @piousdeer)
|
||||
|
||||
## [0.20.6] - 2023-01-21
|
||||
|
||||
Fixes and Improvements:
|
||||
@@ -51,7 +101,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.20.0] - 2022-09-30
|
||||
|
||||
New Features:
|
||||
Features:
|
||||
|
||||
- Added `foam-vscode.create-note` command, which can be very customized for several use cases (#1076)
|
||||
|
||||
@@ -107,7 +157,7 @@ Internal:
|
||||
|
||||
## [0.19.0] - 2022-07-07
|
||||
|
||||
New Features:
|
||||
Features:
|
||||
|
||||
- Support for attachments (PDF) and images (#1027)
|
||||
- Support for opening day notes for other days as well (#1026, thanks @alper)
|
||||
@@ -587,7 +637,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.7.1] - 2020-11-27
|
||||
|
||||
New Feature:
|
||||
Features:
|
||||
|
||||
- Foam logging can now be inspected in VsCode Output panel (#377)
|
||||
|
||||
@@ -599,7 +649,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.7.0] - 2020-11-25
|
||||
|
||||
New Features:
|
||||
Features:
|
||||
|
||||
- Foam stays in sync with changes in notes
|
||||
- Dataviz: Added multiple selection in graph (shift+click on node)
|
||||
@@ -611,7 +661,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.6.0] - 2020-11-19
|
||||
|
||||
New features:
|
||||
Features:
|
||||
|
||||
- Added command to create notes from templates (#115 - Thanks @ingalless)
|
||||
|
||||
@@ -626,7 +676,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.5.0] - 2020-11-09
|
||||
|
||||
New features:
|
||||
Features:
|
||||
|
||||
- Added tags panel (#311)
|
||||
|
||||
@@ -640,7 +690,7 @@ Fixes and Improvements:
|
||||
|
||||
## [0.4.0] - 2020-10-28
|
||||
|
||||
New features:
|
||||
Features:
|
||||
|
||||
- Added `Foam: Show Graph` command
|
||||
- Added date snippets (/+1d, ...) to create wikilinks to dates in daily note format
|
||||
@@ -668,7 +718,7 @@ Fixes and improvements:
|
||||
|
||||
## [0.3.0] - 2020-07-25
|
||||
|
||||
New features:
|
||||
Features:
|
||||
|
||||
- [Daily Notes](https://foambubble.github.io/foam/daily-notes)
|
||||
- [Janitor](https://foambubble.github.io/foam/workspace-janitor) for updating headings and link references across your workspace
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
|
||||
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.20.6",
|
||||
"version": "0.21.4",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -19,21 +19,15 @@
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:.vscode/foam.json",
|
||||
"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",
|
||||
"onCommand:foam-vscode.show-graph",
|
||||
"onCommand:foam-vscode.create-new-template",
|
||||
"onCommand:foam-vscode.create-note",
|
||||
"onCommand:foam-vscode.create-note-from-template",
|
||||
"onCommand:foam-vscode.create-note-from-default-template"
|
||||
"workspaceContains:.vscode/foam.json"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": "limited",
|
||||
"description": "No expressions are allowed in filters."
|
||||
}
|
||||
},
|
||||
"contributes": {
|
||||
"markdown.markdownItPlugins": true,
|
||||
"markdown.previewStyles": [
|
||||
@@ -469,7 +463,7 @@
|
||||
"test:unit": "node ./out/test/run-tests.js --unit",
|
||||
"pretest:e2e": "yarn build",
|
||||
"test:e2e": "node ./out/test/run-tests.js --e2e",
|
||||
"lint": "tsdx lint src",
|
||||
"lint": "dts lint src",
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
@@ -483,32 +477,31 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"@types/remove-markdown": "^0.1.1",
|
||||
"@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",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"@types/vscode": "^1.70.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"dts-cli": "^1.6.3",
|
||||
"esbuild": "^0.17.7",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.2.2",
|
||||
"jest-extended": "^0.11.5",
|
||||
"jest": "^27.5.1",
|
||||
"jest-extended": "^3.2.3",
|
||||
"markdown-it": "^12.0.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^26.4.4",
|
||||
"tsdx": "^0.13.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.5",
|
||||
"typescript": "^4.9.5",
|
||||
"vscode-test": "^1.3.0",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
@@ -518,7 +511,7 @@
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.12.0",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { Range } from '../model/range';
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text text on which the textEdit will be applied
|
||||
* @param textEdit
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const eol = detectNewline(text) || os.EOL;
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
const endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
};
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import matter from 'gray-matter';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { TextEdit } from '../services/text-edit';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
|
||||
export const generateHeading = async (
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../services/markdown-provider';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '../services/text-edit';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
@@ -22,12 +22,7 @@ describe('Graph', () => {
|
||||
const noteD = createTestNote({ uri: '/Page D.md' });
|
||||
const noteE = createTestNote({ uri: '/page e.md' });
|
||||
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD)
|
||||
.set(noteE);
|
||||
workspace.set(noteA).set(noteB).set(noteC).set(noteD).set(noteE);
|
||||
const graph = FoamGraph.fromWorkspace(workspace);
|
||||
|
||||
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
@@ -69,9 +64,7 @@ describe('Graph', () => {
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createTestWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
expect(graph.getBacklinks(noteA.uri)).toEqual([
|
||||
{
|
||||
@@ -95,9 +88,7 @@ describe('Graph', () => {
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createTestWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
|
||||
@@ -165,9 +156,7 @@ describe('Graph', () => {
|
||||
uri: '/path/to/more/attachment-b.pdf',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentA)
|
||||
.set(attachmentB);
|
||||
ws.set(noteA).set(attachmentA).set(attachmentB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
|
||||
@@ -189,9 +178,7 @@ describe('Graph', () => {
|
||||
uri: '/path/to/attachment-a.pdf',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentA)
|
||||
.set(attachmentABis);
|
||||
ws.set(noteA).set(attachmentA).set(attachmentABis);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
@@ -211,9 +198,7 @@ describe('Graph', () => {
|
||||
uri: '/path/to/attachment-a.pdf',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(attachmentABis)
|
||||
.set(attachmentA);
|
||||
ws.set(noteA).set(attachmentABis).set(attachmentA);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
@@ -323,9 +308,7 @@ describe('Regenerating graph after workspace changes', () => {
|
||||
uri: '/path/to/more/page-c.md',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC);
|
||||
ws.set(noteA).set(noteB).set(noteC);
|
||||
let graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
@@ -512,9 +495,7 @@ describe('Updating graph on workspace state', () => {
|
||||
uri: '/path/to/more/page-c.md',
|
||||
});
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC);
|
||||
ws.set(noteA).set(noteB).set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ResourceLink {
|
||||
type: 'wikilink' | 'link';
|
||||
rawText: string;
|
||||
range: Range;
|
||||
isEmbed: boolean;
|
||||
}
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
|
||||
@@ -26,7 +26,8 @@ import * as pathUtils from '../utils/path';
|
||||
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
const _regexp =
|
||||
/^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
export class URI {
|
||||
readonly scheme: string;
|
||||
|
||||
@@ -87,6 +87,13 @@ describe('Workspace resources', () => {
|
||||
const res = ws.find('test-file#my-section');
|
||||
expect(res.uri.fragment).toEqual('my-section');
|
||||
});
|
||||
|
||||
it('should find absolute files even when no basedir is provided', () => {
|
||||
const noteA = createTestNote({ uri: '/a/path/to/file.md' });
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
|
||||
expect(ws.find('/a/path/to/file.md').uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Identifier computation', () => {
|
||||
@@ -100,10 +107,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(first)
|
||||
.set(second)
|
||||
.set(third);
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
|
||||
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
|
||||
@@ -120,10 +124,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(first)
|
||||
.set(second)
|
||||
.set(third);
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
@@ -176,11 +177,7 @@ describe('Identifier computation', () => {
|
||||
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);
|
||||
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])
|
||||
|
||||
@@ -121,14 +121,16 @@ export class FoamWorkspace implements IDisposable {
|
||||
if (FoamWorkspace.isIdentifier(path)) {
|
||||
resource = this.listByIdentifier(path)[0];
|
||||
} else {
|
||||
if (isAbsolute(path) || isSome(baseUri)) {
|
||||
if (getExtension(path) !== '.md') {
|
||||
const uri = baseUri.resolve(path + '.md');
|
||||
resource = uri ? this._resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
if (!resource) {
|
||||
const uri = baseUri.resolve(path);
|
||||
resource = uri ? this._resources.get(normalize(uri.path)) : null;
|
||||
const candidates = [path, path + '.md'];
|
||||
for (const candidate of candidates) {
|
||||
const searchKey = isAbsolute(candidate)
|
||||
? candidate
|
||||
: isSome(baseUri)
|
||||
? baseUri.resolve(candidate).path
|
||||
: null;
|
||||
resource = this._resources.get(normalize(searchKey));
|
||||
if (resource) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ describe('MarkdownLink', () => {
|
||||
type: 'link',
|
||||
rawText: '[link](#section)',
|
||||
range: Range.create(0, 0),
|
||||
isEmbed: false,
|
||||
};
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('');
|
||||
@@ -161,7 +162,7 @@ describe('MarkdownLink', () => {
|
||||
target: 'new-link',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
@@ -172,7 +173,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -184,7 +185,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -195,7 +196,7 @@ describe('MarkdownLink', () => {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to rename the alias', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
|
||||
@@ -204,7 +205,7 @@ describe('MarkdownLink', () => {
|
||||
alias: 'new-alias',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink|new-alias]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +217,7 @@ describe('MarkdownLink', () => {
|
||||
target: 'to/another-path.md',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
@@ -227,7 +228,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -239,7 +240,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -250,7 +251,7 @@ describe('MarkdownLink', () => {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,16 +46,17 @@ export abstract class MarkdownLink {
|
||||
const newAlias = delta.alias ?? alias ?? '';
|
||||
const sectionDivider = newSection ? '#' : '';
|
||||
const aliasDivider = newAlias ? '|' : '';
|
||||
const embed = link.isEmbed ? '!' : '';
|
||||
if (link.type === 'wikilink') {
|
||||
return {
|
||||
newText: `[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
selection: link.range,
|
||||
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
return {
|
||||
newText: `[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
selection: link.range,
|
||||
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('Markdown parsing', () => {
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect links that have formatting in label', () => {
|
||||
@@ -48,6 +49,15 @@ describe('Markdown parsing', () => {
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect embed links', () => {
|
||||
const note = createNoteFromMarkdown('this is ');
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.isEmbed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should detect wikilinks', () => {
|
||||
@@ -61,6 +71,16 @@ describe('Markdown parsing', () => {
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[a file]]');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect wikilink embeds', () => {
|
||||
const note = createNoteFromMarkdown('Some content and ![[an embed]]');
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('![[an embed]]');
|
||||
expect(link.isEmbed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should detect wikilinks that have aliases', () => {
|
||||
@@ -74,6 +94,7 @@ describe('Markdown parsing', () => {
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[other link | spaced]]');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should skip wikilinks in codeblocks', () => {
|
||||
|
||||
@@ -299,18 +299,33 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note, noteSource) => {
|
||||
if (node.type === 'wikiLink') {
|
||||
const isEmbed =
|
||||
noteSource.charAt(node.position!.start.offset - 1) === '!';
|
||||
|
||||
const literalContent = noteSource.substring(
|
||||
node.position!.start.offset!,
|
||||
isEmbed
|
||||
? node.position!.start.offset! - 1
|
||||
: node.position!.start.offset!,
|
||||
node.position!.end.offset!
|
||||
);
|
||||
|
||||
const range = isEmbed
|
||||
? Range.create(
|
||||
node.position.start.line - 1,
|
||||
node.position.start.column - 2,
|
||||
node.position.end.line - 1,
|
||||
node.position.end.column - 1
|
||||
)
|
||||
: astPositionToFoamRange(node.position!);
|
||||
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
rawText: literalContent,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
range,
|
||||
isEmbed,
|
||||
});
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
if (node.type === 'link' || node.type === 'image') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = note.uri.resolve(targetUri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
@@ -324,6 +339,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'link',
|
||||
rawText: literalContent,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
isEmbed: literalContent.startsWith('!'),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,10 +51,7 @@ describe('Link resolution', () => {
|
||||
);
|
||||
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
|
||||
const noteB2 = createNoteFromMarkdown('Page b 2', '/path/two/page b.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteB2);
|
||||
workspace.set(noteA).set(noteB).set(noteB2);
|
||||
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
|
||||
});
|
||||
|
||||
@@ -63,10 +60,7 @@ describe('Link resolution', () => {
|
||||
const noteA = createNoteFromMarkdown('Link to [[page b]]', '/page-a.md');
|
||||
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
|
||||
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteB2);
|
||||
workspace.set(noteA).set(noteB).set(noteB2);
|
||||
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
|
||||
});
|
||||
|
||||
@@ -80,10 +74,7 @@ describe('Link resolution', () => {
|
||||
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB1)
|
||||
.set(noteB2)
|
||||
.set(noteB3);
|
||||
ws.set(noteA).set(noteB1).set(noteB2).set(noteB3);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteB3.uri);
|
||||
@@ -97,10 +88,7 @@ describe('Link resolution', () => {
|
||||
);
|
||||
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
|
||||
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteB2);
|
||||
workspace.set(noteA).set(noteB).set(noteB2);
|
||||
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
|
||||
expect(workspace.resolveLink(noteA, noteA.links[1])).toEqual(noteB.uri);
|
||||
});
|
||||
@@ -157,9 +145,7 @@ describe('Link resolution', () => {
|
||||
],
|
||||
});
|
||||
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createTestWorkspace().set(noteA).set(noteB);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteB.uri.withFragment('section')
|
||||
@@ -258,10 +244,7 @@ describe('Link resolution', () => {
|
||||
);
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD);
|
||||
ws.set(noteA).set(noteB).set(noteC).set(noteD);
|
||||
|
||||
expect(ws.resolveLink(noteB, noteB.links[0])).toEqual(noteA.uri);
|
||||
expect(ws.resolveLink(noteC, noteC.links[0])).toEqual(noteA.uri);
|
||||
|
||||
116
packages/foam-vscode/src/core/services/resource-filter.test.ts
Normal file
116
packages/foam-vscode/src/core/services/resource-filter.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Logger } from '../utils/log';
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
import { createFilter } from './resource-filter';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('Resource Filter', () => {
|
||||
describe('Filter parameters', () => {
|
||||
it('should support expressions when code execution is enabled', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: 'type-2',
|
||||
});
|
||||
|
||||
const filter = createFilter(
|
||||
{
|
||||
expression: 'resource.type === "type-1"',
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not allow expressions when code execution is not enabled', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: 'type-2',
|
||||
});
|
||||
|
||||
const filter = createFilter(
|
||||
{
|
||||
expression: 'resource.type === "type-1"',
|
||||
},
|
||||
false
|
||||
);
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support resource type', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: 'type-2',
|
||||
});
|
||||
|
||||
const filter = createFilter(
|
||||
{
|
||||
type: 'type-1',
|
||||
},
|
||||
false
|
||||
);
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support resource title', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
title: 'title-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
title: 'title-2',
|
||||
});
|
||||
const noteC = createTestNote({
|
||||
uri: 'note-c.md',
|
||||
title: 'another title',
|
||||
});
|
||||
|
||||
const filter = createFilter(
|
||||
{
|
||||
title: '^title',
|
||||
},
|
||||
false
|
||||
);
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeTruthy();
|
||||
expect(filter(noteC)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter operators', () => {
|
||||
it('should support the OR operator', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: 'type-2',
|
||||
});
|
||||
|
||||
const filter = createFilter(
|
||||
{
|
||||
or: [{ type: 'type-1' }, { type: 'type-2' }],
|
||||
},
|
||||
false
|
||||
);
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
77
packages/foam-vscode/src/core/services/resource-filter.ts
Normal file
77
packages/foam-vscode/src/core/services/resource-filter.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { negate } from 'lodash';
|
||||
import { Resource } from '../model/note';
|
||||
|
||||
export interface FilterDescriptor
|
||||
extends FilterDescriptorOp,
|
||||
FilterDescriptorParam {}
|
||||
|
||||
interface FilterDescriptorOp {
|
||||
and?: FilterDescriptor[];
|
||||
or?: FilterDescriptor[];
|
||||
not?: FilterDescriptor;
|
||||
}
|
||||
|
||||
interface FilterDescriptorParam {
|
||||
/**
|
||||
* A regex of the path to include
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* A tag
|
||||
*/
|
||||
tag?: string;
|
||||
|
||||
/**
|
||||
* A note type
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
/**
|
||||
* The title of the note
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* An expression to evaluate to JS, use `resource` to reference the resource object
|
||||
*/
|
||||
expression?: string;
|
||||
}
|
||||
|
||||
type ResourceFilter = (r: Resource) => boolean;
|
||||
|
||||
export function createFilter(
|
||||
filter: FilterDescriptor,
|
||||
enableCode: boolean
|
||||
): ResourceFilter {
|
||||
filter = filter ?? {};
|
||||
const expressionFn =
|
||||
enableCode && filter.expression
|
||||
? resource => eval(filter.expression) // eslint-disable-line no-eval
|
||||
: undefined;
|
||||
return resource => {
|
||||
if (expressionFn && !expressionFn(resource)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type && resource.type !== filter.type) {
|
||||
return false;
|
||||
}
|
||||
if (filter.title && !resource.title.match(filter.title)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.and) {
|
||||
return filter.and
|
||||
.map(pred => createFilter(pred, enableCode))
|
||||
.every(fn => fn(resource));
|
||||
}
|
||||
if (filter.or) {
|
||||
return filter.or
|
||||
.map(pred => createFilter(pred, enableCode))
|
||||
.some(fn => fn(resource));
|
||||
}
|
||||
if (filter.not) {
|
||||
return negate(createFilter(filter.not, enableCode))(resource);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Range } from '../model/range';
|
||||
import { Logger } from '../utils/log';
|
||||
import { applyTextEdit } from './apply-text-edit';
|
||||
import { TextEdit } from './text-edit';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
4. this is fourth line`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
44
packages/foam-vscode/src/core/services/text-edit.ts
Normal file
44
packages/foam-vscode/src/core/services/text-edit.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { Range } from '../model/range';
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export abstract class TextEdit {
|
||||
/**
|
||||
*
|
||||
* @param text text on which the textEdit will be applied
|
||||
* @param textEdit
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
public static apply(text: string, textEdit: TextEdit): string {
|
||||
const eol = detectNewline.graceful(text);
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
const endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
}
|
||||
}
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
@@ -21,7 +21,4 @@ export function isNumeric(value: string): boolean {
|
||||
}
|
||||
|
||||
export const hash = (text: string) =>
|
||||
crypto
|
||||
.createHash('sha1')
|
||||
.update(text)
|
||||
.digest('hex');
|
||||
crypto.createHash('sha1').update(text).digest('hex');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
export const HASHTAG_REGEX =
|
||||
/(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX =
|
||||
/(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
|
||||
export const extractHashtags = (
|
||||
text: string
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
deleteFile,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification', async () => {
|
||||
const config = isWindows
|
||||
? 'C:\\absolute_path\\journal'
|
||||
: '/absolute_path/journal';
|
||||
const expectedPath = isWindows
|
||||
? `${config}\\${isoDate}.md`
|
||||
: `${config}/${isoDate}.md`;
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Daily note template', () => {
|
||||
it('Uses the daily note variables in the template', async () => {
|
||||
const targetDate = new Date(2021, 8, 12);
|
||||
|
||||
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 uri = getDailyNotePath(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 () => {
|
||||
await cleanWorkspace();
|
||||
await closeEditors();
|
||||
});
|
||||
});
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
deleteFile,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification', async () => {
|
||||
const config = isWindows
|
||||
? 'C:\\absolute_path\\journal'
|
||||
: '/absolute_path/journal';
|
||||
const expectedPath = isWindows
|
||||
? `${config}\\${isoDate}.md`
|
||||
: `${config}/${isoDate}.md`;
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Daily note template', () => {
|
||||
it('Uses the daily note variables in the template', async () => {
|
||||
const targetDate = new Date(2021, 8, 12);
|
||||
|
||||
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 uri = getDailyNotePath(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 () => {
|
||||
await cleanWorkspace();
|
||||
await closeEditors();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,9 +86,7 @@ export async function createDailyNoteIfNotExists(targetDate: Date) {
|
||||
|
||||
const templateFallbackText = `---
|
||||
foam_template:
|
||||
filepath: "${workspace.asRelativePath(
|
||||
toVsCodeUri(pathFromLegacyConfiguration)
|
||||
)}"
|
||||
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
|
||||
---
|
||||
# ${dateFormat(targetDate, titleFormat, false)}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import { workspace, ExtensionContext, window, commands } from 'vscode';
|
||||
import { MarkdownResourceProvider } from './core/services/markdown-provider';
|
||||
import { bootstrap } from './core/model/foam';
|
||||
@@ -27,11 +29,8 @@ export async function activate(context: ExtensionContext) {
|
||||
|
||||
// Prepare Foam
|
||||
const excludes = getIgnoredFilesSetting().map(g => g.toString());
|
||||
const {
|
||||
matcher,
|
||||
dataStore,
|
||||
excludePatterns,
|
||||
} = await createMatcherAndDataStore(excludes);
|
||||
const { matcher, dataStore, excludePatterns } =
|
||||
await createMatcherAndDataStore(excludes);
|
||||
|
||||
Logger.info('Loading from directories:');
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('create-note-from-default-template command', () => {
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('create-note-from-default-template command', () => {
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toBeCalledWith({
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('create-note-from-template command', () => {
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(['Yes', 'No'], {
|
||||
expect(spy).toHaveBeenCalledWith(['Yes', 'No'], {
|
||||
placeHolder:
|
||||
'No templates available. Would you like to create one instead?',
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('create-note-from-template command', () => {
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({ label: 'template-a.md' }),
|
||||
expect.objectContaining({ label: 'template-b.md' }),
|
||||
@@ -71,7 +71,7 @@ Template A
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note-from-template');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
label: 'My Template',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
showInEditor,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { CREATE_NOTE_COMMAND } from './create-note';
|
||||
|
||||
describe('create-note command', () => {
|
||||
afterEach(() => {
|
||||
@@ -22,7 +23,7 @@ describe('create-note command', () => {
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve('Test note')));
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note');
|
||||
expect(spy).toBeCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
const target = asAbsoluteWorkspaceUri(URI.file('Test note.md'));
|
||||
expectSameUri(target, window.activeTextEditor?.document.uri);
|
||||
await deleteFile(target);
|
||||
@@ -124,7 +125,7 @@ describe('create-note command', () => {
|
||||
text: 'test ask',
|
||||
onFileExists: 'ask',
|
||||
});
|
||||
expect(spy).toBeCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
await deleteFile(target);
|
||||
});
|
||||
@@ -183,8 +184,22 @@ describe('create-note command', () => {
|
||||
text: 'test asking',
|
||||
onRelativeNotePath: 'ask',
|
||||
});
|
||||
expect(spy).toBeCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
await deleteFile(base);
|
||||
});
|
||||
});
|
||||
|
||||
describe('factories', () => {
|
||||
describe('forPlaceholder', () => {
|
||||
it('adds the .md extension to notes created for placeholders', async () => {
|
||||
await closeEditors();
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder('my-placeholder');
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
const doc = window.activeTextEditor.document;
|
||||
expect(doc.uri.path).toMatch(/my-placeholder.md$/);
|
||||
expect(doc.getText()).toMatch(/^# my-placeholder/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Foam } from '../../core/model/foam';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
|
||||
import { isSome } from '../../core/utils';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
|
||||
interface CreateNoteArgs {
|
||||
/**
|
||||
@@ -34,11 +35,15 @@ interface CreateNoteArgs {
|
||||
/**
|
||||
* Variables to use in the text or template
|
||||
*/
|
||||
variables?: Map<string, string>;
|
||||
variables?: { [key: string]: string };
|
||||
/**
|
||||
* The date used to resolve the FOAM_DATE_* variables. in YYYY-MM-DD format
|
||||
*/
|
||||
date?: string;
|
||||
/**
|
||||
* The title of the note (translates into the FOAM_TITLE variable)
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* What to do in case the target file already exists
|
||||
*/
|
||||
@@ -64,6 +69,9 @@ async function createNote(args: CreateNoteArgs) {
|
||||
new Map(Object.entries(args.variables ?? {})),
|
||||
date
|
||||
);
|
||||
if (args.title) {
|
||||
resolver.define('FOAM_TITLE', args.title);
|
||||
}
|
||||
const text = args.text ?? DEFAULT_NEW_NOTE_TEXT;
|
||||
const noteUri = args.notePath && URI.file(args.notePath);
|
||||
let templateUri: URI;
|
||||
@@ -101,12 +109,26 @@ async function createNote(args: CreateNoteArgs) {
|
||||
|
||||
export const CREATE_NOTE_COMMAND = {
|
||||
command: 'foam-vscode.create-note',
|
||||
title: 'Foam: Create Note',
|
||||
|
||||
asURI: (args: CreateNoteArgs) =>
|
||||
vscode.Uri.parse(`command:${CREATE_NOTE_COMMAND.command}`).with({
|
||||
query: encodeURIComponent(JSON.stringify(args)),
|
||||
}),
|
||||
forPlaceholder: (
|
||||
placeholder: string,
|
||||
extra: Partial<CreateNoteArgs> = {}
|
||||
): CommandDescriptor<CreateNoteArgs> => {
|
||||
const title = placeholder.endsWith('.md')
|
||||
? placeholder.replace(/\.md$/, '')
|
||||
: placeholder;
|
||||
const notePath = placeholder.endsWith('.md')
|
||||
? placeholder
|
||||
: placeholder + '.md';
|
||||
return {
|
||||
name: CREATE_NOTE_COMMAND.command,
|
||||
params: {
|
||||
title,
|
||||
notePath,
|
||||
...extra,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
|
||||
@@ -20,8 +20,8 @@ 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';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -130,8 +130,8 @@ async function runJanitor(foam: Foam) {
|
||||
// Note: The ordering matters. Definitions need to be inserted
|
||||
// before heading, since inserting a heading changes line numbers below
|
||||
let text = noteText;
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
text = definitions ? TextEdit.apply(text, definitions) : text;
|
||||
text = heading ? TextEdit.apply(text, heading) : text;
|
||||
|
||||
return workspace.fs.writeFile(toVsCodeUri(note.uri), Buffer.from(text));
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('open-daily-note-for-date command', () => {
|
||||
|
||||
await commands.executeCommand('foam-vscode.open-daily-note-for-date');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining([
|
||||
expect.objectContaining({
|
||||
label: expect.stringContaining(
|
||||
|
||||
100
packages/foam-vscode/src/features/commands/open-resource.spec.ts
Normal file
100
packages/foam-vscode/src/features/commands/open-resource.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
|
||||
import * as filter from '../../core/services/resource-filter';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { closeEditors, createFile } from '../../test/test-utils-vscode';
|
||||
import { deleteFile } from '../../services/editor';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
|
||||
describe('open-resource command', () => {
|
||||
beforeEach(async () => {
|
||||
await jest.resetAllMocks();
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('URI param has precedence over filter', async () => {
|
||||
const spy = jest.spyOn(filter, 'createFilter');
|
||||
const noteA = await createFile('Note A for open command');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: noteA.uri,
|
||||
filter: { title: 'note 1' },
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
await deleteFile(noteA.uri);
|
||||
});
|
||||
|
||||
it('URI param accept URI object, or path', async () => {
|
||||
const noteA = await createFile('Note A for open command');
|
||||
|
||||
const uriCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(uriCommand.name, uriCommand.params);
|
||||
waitForExpect(() => {
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
|
||||
await closeEditors();
|
||||
|
||||
const pathCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(pathCommand.name, pathCommand.params);
|
||||
waitForExpect(() => {
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
await deleteFile(noteA.uri);
|
||||
});
|
||||
|
||||
it('User is notified if no resource is found', async () => {
|
||||
const spy = jest.spyOn(window, 'showInformationMessage');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
filter: { title: 'note 1 with no existing title' },
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('filter with multiple results will show a quick pick', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
filter: { title: '.*' },
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,119 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { NoteFactory } from '../../services/templates';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import {
|
||||
createFilter,
|
||||
FilterDescriptor,
|
||||
} from '../../core/services/resource-filter';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { isSome, isNone } from '../../core/utils';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
|
||||
export interface OpenResourceArgs {
|
||||
/**
|
||||
* The URI of the resource to open.
|
||||
* If present the `filter` param is ignored
|
||||
*/
|
||||
uri?: URI | string | vscode.Uri;
|
||||
|
||||
/**
|
||||
* The filter object that describes which notes to consider
|
||||
* for opening
|
||||
*/
|
||||
filter?: FilterDescriptor;
|
||||
}
|
||||
|
||||
export const OPEN_COMMAND = {
|
||||
command: 'foam-vscode.open-resource',
|
||||
title: 'Foam: Open Resource',
|
||||
|
||||
asURI: (uri: URI) =>
|
||||
vscode.Uri.parse(`command:${OPEN_COMMAND.command}`).with({
|
||||
query: encodeURIComponent(JSON.stringify({ uri })),
|
||||
}),
|
||||
forURI: (uri: URI): CommandDescriptor<OpenResourceArgs> => {
|
||||
return {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: uri,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
|
||||
args = args ?? {};
|
||||
|
||||
let item: { uri: URI } | null = null;
|
||||
|
||||
if (args.uri) {
|
||||
const path = typeof args.uri === 'string' ? args.uri : args.uri.path;
|
||||
item = workspace.find(path);
|
||||
}
|
||||
|
||||
if (isNone(item) && args.filter) {
|
||||
const resources = workspace.list();
|
||||
const candidates = resources.filter(
|
||||
createFilter(args.filter, vscode.workspace.isTrusted)
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
'Foam: No note matches given filters.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
item =
|
||||
candidates.length === 1
|
||||
? candidates[0]
|
||||
: await vscode.window.showQuickPick(
|
||||
candidates.map(createQuickPickItemForResource)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSome(item)) {
|
||||
const targetUri =
|
||||
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
}
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
OPEN_COMMAND.command,
|
||||
async (params: { uri: URI }) => {
|
||||
const uri = new URI(params.uri);
|
||||
switch (uri.scheme) {
|
||||
case 'file': {
|
||||
const targetUri =
|
||||
uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(uri.asPlain());
|
||||
// if the doc is already open, reuse the same colunm
|
||||
const targetEditor = vscode.window.visibleTextEditors.find(
|
||||
ed => targetUri.path === ed.document.uri.path
|
||||
);
|
||||
const column = targetEditor?.viewColumn;
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
}
|
||||
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
|
||||
: vscode.window.activeTextEditor?.document.uri
|
||||
? vscode.window.activeTextEditor!.document.uri
|
||||
: undefined;
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
const target = fromVsCodeUri(basedir)
|
||||
.resolve(uri, true)
|
||||
.changeExtension('', '.md');
|
||||
await NoteFactory.createForPlaceholderWikilink(title, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
vscode.commands.registerCommand(OPEN_COMMAND.command, args => {
|
||||
return openResource(foam.workspace, args);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
interface ResourceItem extends vscode.QuickPickItem {
|
||||
label: string;
|
||||
description: string;
|
||||
uri: URI;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const createQuickPickItemForResource = (resource: Resource): ResourceItem => {
|
||||
const icon = 'file';
|
||||
const sections = resource.sections
|
||||
.map(s => s.label)
|
||||
.filter(l => l !== resource.title);
|
||||
const detail = sections.length > 0 ? 'Sections: ' + sections.join(', ') : '';
|
||||
return {
|
||||
label: `$(${icon}) ${resource.title}`,
|
||||
description: vscode.workspace.asRelativePath(resource.uri.toFsPath()),
|
||||
uri: resource.uri,
|
||||
detail: detail,
|
||||
};
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -70,7 +70,7 @@ async function createReferenceList(foam: FoamWorkspace) {
|
||||
|
||||
const refs = await generateReferenceList(foam, editor.document);
|
||||
if (refs && refs.length) {
|
||||
await editor.edit(function(editBuilder) {
|
||||
await editor.edit(function (editBuilder) {
|
||||
if (editor) {
|
||||
const spacing = hasEmptyTrailing(editor.document)
|
||||
? docConfig.eol
|
||||
@@ -193,7 +193,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
|
||||
public provideCodeLenses(
|
||||
document: TextDocument,
|
||||
_: CancellationToken
|
||||
): CodeLens[] | Thenable<CodeLens[]> {
|
||||
): CodeLens[] | Promise<CodeLens[]> {
|
||||
loadDocConfig();
|
||||
|
||||
const range = detectReferenceListRange(document);
|
||||
|
||||
@@ -14,33 +14,32 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const updateDecorations = (
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (!editor || editor.document.languageId !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(editor.document.uri),
|
||||
editor.document.getText()
|
||||
);
|
||||
const placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = workspace.resolveLink(note, link);
|
||||
if (linkUri.isPlaceholder()) {
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
|
||||
link.range.end.line,
|
||||
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
|
||||
)
|
||||
);
|
||||
const updateDecorations =
|
||||
(parser: ResourceParser, workspace: FoamWorkspace) =>
|
||||
(editor: vscode.TextEditor) => {
|
||||
if (!editor || editor.document.languageId !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
});
|
||||
editor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
};
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(editor.document.uri),
|
||||
editor.document.getText()
|
||||
);
|
||||
const placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = workspace.resolveLink(note, link);
|
||||
if (linkUri.isPlaceholder()) {
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
|
||||
link.range.end.line,
|
||||
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
editor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
|
||||
@@ -92,9 +92,7 @@ describe('Hover provider', () => {
|
||||
);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
|
||||
@@ -133,9 +131,7 @@ describe('Hover provider', () => {
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
|
||||
const ws = createWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
@@ -164,9 +160,7 @@ describe('Hover provider', () => {
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
|
||||
const ws = createWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
@@ -190,9 +184,7 @@ describe('Hover provider', () => {
|
||||
);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
@@ -220,9 +212,7 @@ The content of file B`);
|
||||
);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
const ws = createWorkspace().set(noteA).set(noteB);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Range } from '../core/model/range';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
|
||||
@@ -22,9 +23,8 @@ const feature: FoamFeature = {
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const isHoverEnabled: ConfigurationMonitor<boolean> = monitorFoamVsCodeConfig(
|
||||
CONFIG_KEY
|
||||
);
|
||||
const isHoverEnabled: ConfigurationMonitor<boolean> =
|
||||
monitorFoamVsCodeConfig(CONFIG_KEY);
|
||||
|
||||
const foam = await foamPromise;
|
||||
|
||||
@@ -87,7 +87,7 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
);
|
||||
|
||||
const links = sources.slice(0, 10).map(ref => {
|
||||
const command = OPEN_COMMAND.asURI(ref);
|
||||
const command = commandAsURI(OPEN_COMMAND.forURI(ref));
|
||||
return `- [${this.workspace.get(ref).title}](${command.toString()})`;
|
||||
});
|
||||
|
||||
@@ -109,27 +109,14 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
: this.workspace.get(targetUri).title;
|
||||
}
|
||||
|
||||
// If placeholder, offer to create a new note from template (compared to default link provider - not from template)
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? vscode.workspace.workspaceFolders[0].uri
|
||||
: vscode.window.activeTextEditor?.document.uri
|
||||
? vscode.window.activeTextEditor!.document.uri
|
||||
: undefined;
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
const target = fromVsCodeUri(basedir)
|
||||
.resolve(targetUri, true)
|
||||
.changeExtension('', '.md');
|
||||
const args = {
|
||||
text: target.getName(),
|
||||
notePath: target.path,
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(targetUri.path, {
|
||||
askForTemplate: true,
|
||||
};
|
||||
const command = CREATE_NOTE_COMMAND.asURI(args);
|
||||
onFileExists: 'open',
|
||||
});
|
||||
const newNoteFromTemplate = new vscode.MarkdownString(
|
||||
`[Create note from template for '${targetUri.getName()}'](${command})`
|
||||
`[Create note from template for '${targetUri.getName()}'](${commandAsURI(
|
||||
command
|
||||
).toString()})`
|
||||
);
|
||||
newNoteFromTemplate.isTrusted = true;
|
||||
|
||||
|
||||
@@ -64,15 +64,11 @@ export const completionCursorMove: FoamFeature = {
|
||||
.lineAt(changedPosition.line)
|
||||
.text.charAt(changedPosition.character - 1);
|
||||
|
||||
const {
|
||||
character: selectionChar,
|
||||
line: selectionLine,
|
||||
} = e.selections[0].active;
|
||||
const { character: selectionChar, line: selectionLine } =
|
||||
e.selections[0].active;
|
||||
|
||||
const {
|
||||
line: completionLine,
|
||||
character: completionChar,
|
||||
} = currentPosition;
|
||||
const { line: completionLine, character: completionChar } =
|
||||
currentPosition;
|
||||
|
||||
const inCompleteBySectionDivider =
|
||||
linkCommitCharacters.includes(preChar) &&
|
||||
@@ -102,7 +98,8 @@ export const completionCursorMove: FoamFeature = {
|
||||
};
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem>
|
||||
{
|
||||
constructor(private ws: FoamWorkspace) {}
|
||||
|
||||
provideCompletionItems(
|
||||
@@ -162,7 +159,8 @@ export class SectionCompletionProvider
|
||||
}
|
||||
|
||||
export class WikilinkCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem>
|
||||
{
|
||||
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
|
||||
|
||||
provideCompletionItems(
|
||||
@@ -293,9 +291,8 @@ class ResourceCompletionItem extends vscode.CompletionItem {
|
||||
}
|
||||
|
||||
function getCompletionLabelSetting() {
|
||||
const labelStyle: 'path' | 'title' | 'identifier' = getFoamVsCodeConfig(
|
||||
'completion.label'
|
||||
);
|
||||
const labelStyle: 'path' | 'title' | 'identifier' =
|
||||
getFoamVsCodeConfig('completion.label');
|
||||
return labelStyle;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
|
||||
describe('Document navigation', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
@@ -82,7 +84,11 @@ describe('Document navigation', () => {
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(URI.placeholder('a placeholder'))
|
||||
commandAsURI(
|
||||
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', {
|
||||
onFileExists: 'open',
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 './commands/open-resource';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Resource, ResourceLink, ResourceParser } from '../core/model/note';
|
||||
@@ -10,6 +9,8 @@ import { URI } from '../core/model/uri';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { Position } from '../core/model/position';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -57,7 +58,8 @@ export class NavigationProvider
|
||||
implements
|
||||
vscode.DefinitionProvider,
|
||||
vscode.DocumentLinkProvider,
|
||||
vscode.ReferenceProvider {
|
||||
vscode.ReferenceProvider
|
||||
{
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
@@ -162,7 +164,9 @@ export class NavigationProvider
|
||||
return targets
|
||||
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
|
||||
.map(o => {
|
||||
const command = OPEN_COMMAND.asURI(o.target);
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(o.target.path, {
|
||||
onFileExists: 'open',
|
||||
});
|
||||
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
@@ -171,7 +175,7 @@ export class NavigationProvider
|
||||
o.link.range.end.line,
|
||||
o.link.range.end.character - 2
|
||||
),
|
||||
command
|
||||
commandAsURI(command)
|
||||
);
|
||||
documentLink.tooltip = `Create note for '${o.target.path}'`;
|
||||
return documentLink;
|
||||
|
||||
@@ -44,9 +44,7 @@ describe('Backlinks panel', () => {
|
||||
uri: './note-c.md',
|
||||
links: [{ slug: 'note-a' }],
|
||||
});
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC);
|
||||
ws.set(noteA).set(noteB).set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(ws, graph);
|
||||
|
||||
@@ -37,7 +37,8 @@ const feature: FoamFeature = {
|
||||
export default feature;
|
||||
|
||||
export class BacklinksTreeDataProvider
|
||||
implements vscode.TreeDataProvider<BacklinkPanelTreeItem> {
|
||||
implements vscode.TreeDataProvider<BacklinkPanelTreeItem>
|
||||
{
|
||||
public target?: URI = undefined;
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeDataEmitter = new vscode.EventEmitter<BacklinkPanelTreeItem | undefined | void>();
|
||||
@@ -53,7 +54,7 @@ export class BacklinksTreeDataProvider
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(item?: ResourceTreeItem): Thenable<BacklinkPanelTreeItem[]> {
|
||||
getChildren(item?: ResourceTreeItem): Promise<BacklinkPanelTreeItem[]> {
|
||||
const uri = this.target;
|
||||
if (item) {
|
||||
const resource = item.resource;
|
||||
@@ -61,10 +62,7 @@ export class BacklinksTreeDataProvider
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
this.workspace
|
||||
.resolveLink(resource, link)
|
||||
.asPlain()
|
||||
.isEqual(uri)
|
||||
this.workspace.resolveLink(resource, link).asPlain().isEqual(uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
@@ -105,9 +103,9 @@ export class BacklinksTreeDataProvider
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.sort(Resource.sortByTitle)
|
||||
.map(note => {
|
||||
const connections = backlinksByResourcePath[
|
||||
note.uri.path
|
||||
].sort((a, b) => Range.isBefore(a.link.range, b.link.range));
|
||||
const connections = backlinksByResourcePath[note.uri.path].sort(
|
||||
(a, b) => Range.isBefore(a.link.range, b.link.range)
|
||||
);
|
||||
const item = new ResourceTreeItem(
|
||||
note,
|
||||
this.workspace,
|
||||
|
||||
@@ -67,7 +67,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: TagItem): Thenable<TagTreeItem[]> {
|
||||
getChildren(element?: TagItem): Promise<TagTreeItem[]> {
|
||||
if (element) {
|
||||
const nestedTagItems: TagTreeItem[] = this.tags
|
||||
.filter(item => item.tag.indexOf(element.title + TAG_SEPARATOR) > -1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
@@ -20,7 +22,11 @@ const feature: FoamFeature = {
|
||||
markdownItFoamTags,
|
||||
markdownItWikilinkNavigation,
|
||||
markdownItRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, foam.workspace), md);
|
||||
].reduce(
|
||||
(acc, extension) =>
|
||||
extension(acc, foam.workspace, foam.services.parser),
|
||||
md
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
|
||||
export const markdownItRemoveLinkReferences = (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Displaying included notes in preview', () => {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
@@ -51,7 +51,7 @@ describe('Displaying included notes in preview', () => {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-a]]`);
|
||||
expect(res).toContain('This is the root node');
|
||||
@@ -68,19 +68,19 @@ describe('Displaying included notes in preview', () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note D
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note D
|
||||
This is the second section of note E
|
||||
|
||||
# Section 3
|
||||
This is the third section of note D
|
||||
This is the third section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
@@ -93,7 +93,7 @@ This is the third section of note D
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><h1>Section 2</h1>
|
||||
<p>This is the second section of note D</p>
|
||||
<p>This is the second section of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
@@ -102,8 +102,48 @@ This is the third section of note D
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section in container mode', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note E
|
||||
|
||||
# Section 3
|
||||
This is the third section of note E
|
||||
`,
|
||||
['note-e-container.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(
|
||||
`This is the root node. ![[note-e-container#Section 3]]`
|
||||
);
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('Section 3');
|
||||
expect(res).toContain('This is the third section of note E');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), new FoamWorkspace());
|
||||
const md = markdownItWikilinkEmbed(
|
||||
MarkdownIt(),
|
||||
new FoamWorkspace(),
|
||||
parser
|
||||
);
|
||||
|
||||
expect(md.render(`This is the root node. ![[non-existing-note]]`)).toMatch(
|
||||
`<p>This is the root node. ![[non-existing-note]]</p>`
|
||||
@@ -122,12 +162,12 @@ This is the third section of note D
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(noteA.uri, noteA.content))
|
||||
.set(parser.parse(noteB.uri, noteB.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink: note-a');
|
||||
expect(res).toContain('Cyclic link detected for wikilink');
|
||||
|
||||
deleteFile(noteA);
|
||||
deleteFile(noteB);
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { readFileSync } from 'fs';
|
||||
import { workspace as vsWorkspace } from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isSome } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { Resource, ResourceParser } from '../../core/model/note';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { readFileSync } from 'fs';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Position } from '../../core/model/position';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
|
||||
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
|
||||
const refsStack: string[] = [];
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
workspace: FoamWorkspace,
|
||||
parser: ResourceParser
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
@@ -39,9 +47,22 @@ export const markdownItWikilinkEmbed = (
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
const noteText = readFileSync(
|
||||
includedNote.uri.toFsPath()
|
||||
).toString();
|
||||
let noteText = readFileSync(includedNote.uri.toFsPath()).toString();
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = noteText.split('\n');
|
||||
noteText = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(
|
||||
noteText,
|
||||
parser,
|
||||
workspace
|
||||
);
|
||||
content = getFoamVsCodeConfig(CONFIG_EMBED_NOTE_IN_CONTAINER)
|
||||
? `<div class="embed-container-note">${md.render(noteText)}</div>`
|
||||
: noteText;
|
||||
@@ -60,16 +81,6 @@ Embed for attachments is not supported
|
||||
)}</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;
|
||||
@@ -84,4 +95,32 @@ Embed for attachments is not supported
|
||||
});
|
||||
};
|
||||
|
||||
function withLinksRelativeToWorkspaceRoot(
|
||||
noteText: string,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) {
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(vsWorkspace.workspaceFolders[0].uri),
|
||||
noteText
|
||||
);
|
||||
const edits = note.links
|
||||
.map(link => {
|
||||
const info = MarkdownLink.analyzeLink(link);
|
||||
const resource = workspace.find(info.target);
|
||||
const pathFromRoot = vsWorkspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
);
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: pathFromRoot,
|
||||
});
|
||||
})
|
||||
.sort((a, b) => Position.compareTo(b.range.start, a.range.start));
|
||||
const text = edits.reduce(
|
||||
(text, edit) => TextEdit.apply(text, edit),
|
||||
noteText
|
||||
);
|
||||
return text;
|
||||
}
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { isNone } from '../../utils';
|
||||
@@ -21,6 +23,7 @@ export const markdownItWikilinkNavigation = (
|
||||
rawText: '[[' + wikilink + ']]',
|
||||
type: 'wikilink',
|
||||
range: Range.create(0, 0),
|
||||
isEmbed: false,
|
||||
});
|
||||
const formattedSection = section ? `#${section}` : '';
|
||||
const label = isEmpty(alias) ? `${target}${formattedSection}` : alias;
|
||||
|
||||
@@ -20,7 +20,16 @@ const feature: FoamFeature = {
|
||||
return;
|
||||
}
|
||||
const renameEdits = new vscode.WorkspaceEdit();
|
||||
e.files.forEach(({ oldUri, newUri }) => {
|
||||
for (const { oldUri, newUri } of e.files) {
|
||||
if (
|
||||
(await vscode.workspace.fs.stat(oldUri)).type ===
|
||||
vscode.FileType.Directory
|
||||
) {
|
||||
vscode.window.showWarningMessage(
|
||||
'Foam: Updating links on directory rename is not supported.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const connections = foam.graph.getBacklinks(fromVsCodeUri(oldUri));
|
||||
connections.forEach(async connection => {
|
||||
const { target } = MarkdownLink.analyzeLink(connection.link);
|
||||
@@ -36,7 +45,7 @@ const feature: FoamFeature = {
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
@@ -53,14 +62,14 @@ const feature: FoamFeature = {
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (renameEdits.size > 0) {
|
||||
|
||||
@@ -93,4 +93,18 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a markdown heading #1182', async () => {
|
||||
const { uri } = await createFile('# primary heading 1');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 7)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { HASHTAG_REGEX } from '../core/utils/hashtags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { SECTION_REGEX } from './link-completion';
|
||||
|
||||
export const TAG_REGEX = /#(.*)/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -24,7 +22,8 @@ const feature: FoamFeature = {
|
||||
};
|
||||
|
||||
export class TagCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem>
|
||||
{
|
||||
constructor(private foamTags: FoamTags) {}
|
||||
|
||||
provideCompletionItems(
|
||||
@@ -35,8 +34,7 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete =
|
||||
cursorPrefix.match(TAG_REGEX) && !cursorPrefix.match(SECTION_REGEX);
|
||||
const requiresAutocomplete = cursorPrefix.match(HASHTAG_REGEX);
|
||||
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
|
||||
@@ -135,9 +135,7 @@ export function asAbsoluteWorkspaceUri(uri: URI): URI {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function createMatcherAndDataStore(
|
||||
excludes: string[]
|
||||
): Promise<{
|
||||
export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
matcher: IMatcher;
|
||||
dataStore: IDataStore;
|
||||
excludePatterns: Map<string, string[]>;
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('Create note from template', () => {
|
||||
new Resolver(new Map(), new Date()),
|
||||
fileA.uri
|
||||
);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: `Enter the path for the new note`,
|
||||
})
|
||||
|
||||
@@ -95,10 +95,8 @@ export async function getTemplateInfo(
|
||||
templateText
|
||||
);
|
||||
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const [templateMetadata, templateWithFoamFrontmatterRemoved] =
|
||||
extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
|
||||
return {
|
||||
metadata: templateMetadata,
|
||||
@@ -209,62 +207,61 @@ function sortTemplatesMetadata(
|
||||
return nameSortOrder || pathSortOrder;
|
||||
}
|
||||
|
||||
const createFnForOnRelativePathStrategy = (
|
||||
onRelativePath: OnRelativePathStrategy | undefined
|
||||
) => async (existingFile: URI) => {
|
||||
// Get the default from the configuration
|
||||
if (isNone(onRelativePath)) {
|
||||
onRelativePath =
|
||||
getFoamVsCodeConfig('files.newNotePath') === 'root'
|
||||
? 'resolve-from-root'
|
||||
: 'resolve-from-current-dir';
|
||||
}
|
||||
|
||||
if (typeof onRelativePath === 'function') {
|
||||
return onRelativePath(existingFile);
|
||||
}
|
||||
|
||||
switch (onRelativePath) {
|
||||
case 'resolve-from-current-dir':
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
case 'resolve-from-root':
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
case 'cancel':
|
||||
return undefined;
|
||||
case 'ask':
|
||||
default: {
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
const createFnForOnRelativePathStrategy =
|
||||
(onRelativePath: OnRelativePathStrategy | undefined) =>
|
||||
async (existingFile: URI) => {
|
||||
// Get the default from the configuration
|
||||
if (isNone(onRelativePath)) {
|
||||
onRelativePath =
|
||||
getFoamVsCodeConfig('files.newNotePath') === 'root'
|
||||
? 'resolve-from-root'
|
||||
: 'resolve-from-current-dir';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createFnForOnFileExistsStrategy = (
|
||||
onFileExists: OnFileExistStrategy
|
||||
) => async (existingFile: URI) => {
|
||||
if (typeof onFileExists === 'function') {
|
||||
return onFileExists(existingFile);
|
||||
}
|
||||
switch (onFileExists) {
|
||||
case 'open':
|
||||
await commands.executeCommand('vscode.open', toVsCodeUri(existingFile));
|
||||
return;
|
||||
case 'overwrite':
|
||||
await deleteFile(existingFile);
|
||||
return existingFile;
|
||||
case 'cancel':
|
||||
return undefined;
|
||||
case 'ask':
|
||||
default: {
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
if (typeof onRelativePath === 'function') {
|
||||
return onRelativePath(existingFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (onRelativePath) {
|
||||
case 'resolve-from-current-dir':
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
case 'resolve-from-root':
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
case 'cancel':
|
||||
return undefined;
|
||||
case 'ask':
|
||||
default: {
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createFnForOnFileExistsStrategy =
|
||||
(onFileExists: OnFileExistStrategy) => async (existingFile: URI) => {
|
||||
if (typeof onFileExists === 'function') {
|
||||
return onFileExists(existingFile);
|
||||
}
|
||||
switch (onFileExists) {
|
||||
case 'open':
|
||||
await commands.executeCommand('vscode.open', toVsCodeUri(existingFile));
|
||||
return;
|
||||
case 'overwrite':
|
||||
await deleteFile(existingFile);
|
||||
return existingFile;
|
||||
case 'cancel':
|
||||
return undefined;
|
||||
case 'ask':
|
||||
default: {
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const NoteFactory = {
|
||||
createNote: async (
|
||||
@@ -279,9 +276,8 @@ export const NoteFactory = {
|
||||
const onRelativePath = createFnForOnRelativePathStrategy(
|
||||
onRelativePathStrategy
|
||||
);
|
||||
const onFileExists = createFnForOnFileExistsStrategy(
|
||||
onFileExistsStrategy
|
||||
);
|
||||
const onFileExists =
|
||||
createFnForOnFileExistsStrategy(onFileExistsStrategy);
|
||||
|
||||
/**
|
||||
* Make sure the path is absolute and doesn't exist
|
||||
@@ -353,14 +349,15 @@ export const NoteFactory = {
|
||||
resolver
|
||||
);
|
||||
|
||||
const newFilePath = asAbsoluteWorkspaceUri(
|
||||
template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: isSome(filepathFallbackURI)
|
||||
? filepathFallbackURI
|
||||
: await getPathFromTitle(resolver)
|
||||
);
|
||||
let newFilePath = template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: isSome(filepathFallbackURI)
|
||||
? filepathFallbackURI
|
||||
: await getPathFromTitle(resolver);
|
||||
|
||||
if (!newFilePath.path.startsWith('./')) {
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath);
|
||||
}
|
||||
return NoteFactory.createNote(
|
||||
newFilePath,
|
||||
template.text,
|
||||
|
||||
@@ -59,12 +59,13 @@ export function run(): Promise<void> {
|
||||
[rootDir]
|
||||
);
|
||||
|
||||
const failures = results.testResults.reduce((acc, res) => {
|
||||
if (res.failureMessage) {
|
||||
acc.push(res as any);
|
||||
}
|
||||
return acc;
|
||||
}, [] as jest.TestResult[]);
|
||||
const failures = results.testResults.filter(t => t.failureMessage);
|
||||
// const failures = results.testResults.reduce((acc, res) => {
|
||||
// if (res.failureMessage) {
|
||||
// acc.push(res as any);
|
||||
// }
|
||||
// return acc;
|
||||
// }, []);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('Some Foam tests failed: ', failures.length);
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import micromatch from 'micromatch';
|
||||
import { promisify } from 'util';
|
||||
import { glob } from 'glob';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { IDataStore, IMatcher } from '../core/services/datastore';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { isWindows } from '../core/common/platform';
|
||||
import { asAbsolutePaths } from '../core/utils/path';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
function getFiles(directory: string) {
|
||||
const files = [];
|
||||
getFilesFromDir(files, directory);
|
||||
return files;
|
||||
}
|
||||
function getFilesFromDir(files: string[], directory: string) {
|
||||
fs.readdirSync(directory).forEach(file => {
|
||||
const absolute = path.join(directory, file);
|
||||
if (fs.statSync(absolute).isDirectory()) {
|
||||
getFilesFromDir(files, absolute);
|
||||
} else {
|
||||
files.push(absolute);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
@@ -19,7 +32,7 @@ export class FileDataStore implements IDataStore {
|
||||
) {}
|
||||
|
||||
async list(): Promise<URI[]> {
|
||||
const res = await findAllFiles([this.basedir, '**/*'].join('/'));
|
||||
const res = getFiles(this.basedir);
|
||||
return res.map(URI.file);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,11 +53,12 @@ export const createTestNote = (params: {
|
||||
text?: string;
|
||||
sections?: string[];
|
||||
root?: URI;
|
||||
type?: string;
|
||||
}): Resource => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: root.resolve(params.uri),
|
||||
type: 'note',
|
||||
type: params.type ?? 'note',
|
||||
properties: {},
|
||||
title: params.title ?? strToUri(params.uri).getBasename(),
|
||||
definitions: params.definitions ?? [],
|
||||
@@ -88,11 +89,13 @@ export const createTestNote = (params: {
|
||||
type: 'wikilink',
|
||||
range: range,
|
||||
rawText: `[[${link.slug}]]`,
|
||||
isEmbed: false,
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
range: range,
|
||||
rawText: `[link text](${link.to})`,
|
||||
isEmbed: false,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
|
||||
20
packages/foam-vscode/src/utils/commands.ts
Normal file
20
packages/foam-vscode/src/utils/commands.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Uri } from 'vscode';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
export interface CommandDescriptor<T> {
|
||||
name: string;
|
||||
params: T;
|
||||
}
|
||||
|
||||
export function describeCommand<T>(
|
||||
base: CommandDescriptor<T>,
|
||||
...extra: Partial<T>[]
|
||||
) {
|
||||
return merge(base, ...extra.map(e => ({ params: e })));
|
||||
}
|
||||
|
||||
export function commandAsURI<T>(command: CommandDescriptor<T>) {
|
||||
return Uri.parse(`command:${command.name}`).with({
|
||||
query: encodeURIComponent(JSON.stringify(command.params)),
|
||||
});
|
||||
}
|
||||
@@ -37,7 +37,8 @@ import { IMatcher } from '../core/services/datastore';
|
||||
* @implements {vscode.TreeDataProvider<GroupedResourceTreeItem>}
|
||||
*/
|
||||
export class GroupedResourcesTreeDataProvider
|
||||
implements vscode.TreeDataProvider<GroupedResourceTreeItem> {
|
||||
implements vscode.TreeDataProvider<GroupedResourceTreeItem>
|
||||
{
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<GroupedResourceTreeItem | undefined | void> = new vscode.EventEmitter<GroupedResourceTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
@@ -129,7 +130,7 @@ export class GroupedResourcesTreeDataProvider
|
||||
|
||||
getChildren(
|
||||
directory?: DirectoryTreeItem
|
||||
): Thenable<GroupedResourceTreeItem[]> {
|
||||
): Promise<GroupedResourceTreeItem[]> {
|
||||
if (this.groupBy === GroupedResoucesConfigGroupBy.Folder) {
|
||||
if (isSome(directory)) {
|
||||
return Promise.resolve(directory.children.sort(sortByTreeItemLabel));
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "out",
|
||||
"lib": ["ES2019", "es2020.string"],
|
||||
"lib": ["ES2019", "es2020.string", "DOM"],
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"downlevelIteration": true
|
||||
|
||||
2
packages/foam-vscode/types/utils.d.ts
vendored
2
packages/foam-vscode/types/utils.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'remark-wiki-link';
|
||||
declare module 'remark-wiki-link';
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
[](https://foambubble.github.io/join-discord/g)
|
||||
|
||||
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
|
||||
@@ -98,7 +99,7 @@ Foam also supports hierarchical tags.
|
||||
|
||||
### Orphans and Placeholder Panels
|
||||
|
||||
Orphans are notes that have no inbound nor outbound links.
|
||||
Orphans are notes that have no inbound nor outbound links.
|
||||
Placeholders are dangling links, or notes without content.
|
||||
Keep them under control, and your knowledge base in a better state, by using this panel.
|
||||
|
||||
@@ -146,7 +147,7 @@ Whether you want to build a [Second Brain](https://www.buildingasecondbrain.com/
|
||||
|
||||
You can also use our Foam template:
|
||||
|
||||
1. Log in on your GitHub account.
|
||||
1. Log in on your GitHub account.
|
||||
2. [Create a GitHub repository from foam-template](https://github.com/foambubble/foam-template/generate). If you want to keep your thoughts to yourself, remember to set the repository private.
|
||||
3. Clone the repository and open it in VS Code.
|
||||
4. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).
|
||||
@@ -338,6 +339,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://yongliangliu.com"><img src="https://avatars.githubusercontent.com/u/41845017?v=4?s=60" width="60px;" alt="Liu YongLiang"/><br /><sub><b>Liu YongLiang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tlylt" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://scottakerman.com"><img src="https://avatars.githubusercontent.com/u/15224439?v=4?s=60" width="60px;" alt="Scott Akerman"/><br /><sub><b>Scott Akerman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skakerman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user