Compare commits

...

103 Commits

Author SHA1 Message Date
Carlos Pita
5aff88827f Fix link autocompletion with tags 2021-12-22 20:06:51 -03:00
Riccardo Ferretti
4195797024 v0.17.2 2021-12-22 23:33:10 +01:00
Riccardo Ferretti
fa405f5f65 Preparation for 0.17.2 2021-12-22 23:32:27 +01:00
Riccardo Ferretti
4fd573b9e4 Fixed VS Code settings file 2021-12-22 23:11:19 +01:00
Riccardo Ferretti
f613e1b9e2 Fix issue when applying edits to last line
Authored by: @memeplex

See also #860
2021-12-22 23:11:03 +01:00
Riccardo Ferretti
0ada7d8e2c chore: minor change around test function 2021-12-22 22:53:51 +01:00
memeplex
8b39bcdf16 Update yarn.lock (#883) 2021-12-21 21:54:26 +01:00
memeplex
6073dc246d Remove legacy github slugger (#872) 2021-12-21 21:32:40 +01:00
memeplex
5b671d59a8 Use syntax injection for wikilinks (#876)
* Use syntax injection for wikilinks

* Configurable placeholder color

* Highlight only contents
2021-12-21 21:08:39 +01:00
memeplex
8abea48b5c Improve testing experience (#881)
* Improve testing experience
* Support vscode-jest for unit tests
2021-12-21 21:08:09 +01:00
Riccardo Ferretti
2eeb2e156b Fix #878 - Added support for (wiki)links in titles 2021-12-16 16:46:13 +01:00
Riccardo Ferretti
dc76660a63 v0.17.1 2021-12-16 13:24:29 +01:00
Riccardo Ferretti
e8eeffa4ca Prepare 0.17.1 2021-12-16 13:24:07 +01:00
memeplex
7d4f5e1532 Graph improvements: light theme, zoom to fit canvas, dat.gui layout (#875)
* Improve dat.gui theme
* Zoom to fit canvas at start
2021-12-15 10:48:48 +01:00
memeplex
e7749cd52b Better support dendron-style names (#870)
* Better support dendron-style names

* Add test for non-markdown resource
2021-12-13 17:20:04 +01:00
memeplex
c6a4eab744 Unify isWindows implementation (#873) 2021-12-13 00:08:22 +01:00
allcontributors[bot]
c88bd6f2f0 docs: add jimt as a contributor for doc (#869)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-12-12 16:27:34 +01:00
Jim Tittsler
304a803310 docs: fix typos (#866) 2021-12-12 16:26:51 +01:00
allcontributors[bot]
632c41ac5f docs: add iam-yan as a contributor for doc (#868)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-12-12 16:23:00 +01:00
Yan
ec636809d8 Add recipe for creating wikilink to sections. The guide is added in the original wikilinks recipe. (#867)
Co-authored-by: juuyan <hello@juuyan.com>
2021-12-12 16:22:03 +01:00
memeplex
af43a31ae8 Refactor URI and path related code (#858)
* Refactor uri/path-related code
* Clarify usage of uri vs fs paths
* OO URI API with some path methods
* Fix open command uri
* Document path API
2021-12-12 16:18:43 +01:00
Riccardo Ferretti
7235af70dd fix #859 - force focus on note only if it already exists
This way it will not interfere with the template placeholder logic
2021-12-11 15:48:56 +01:00
Riccardo Ferretti
de84541692 fix for #857 - decorate only markdown files 2021-12-11 15:04:37 +01:00
Riccardo Ferretti
84fab168ce Improved replacement range for link completion 2021-12-08 23:22:38 +01:00
Riccardo Ferretti
4f116cfc88 v0.17.0 2021-12-08 09:29:20 +01:00
Riccardo Ferretti
fd71dbe557 Prepare 0.17.0 2021-12-08 09:28:40 +01:00
Riccardo Ferretti
df4bf5a5cb Fixed graph update bug 2021-12-08 09:20:29 +01:00
Riccardo
122db20695 Add support for sections (#856)
* Added support for sections/subsections in `Resource`

* Added support for sections in navigation and definitions

* Section completion

* Diagnostics and quick actions for sections

* Added support for section embeds in preview

* Added reference to sections support in readme file

* Add support for sections in direct links

* Added support for sections in identifier computation

* Support for section wikilinks within same file

* Tweaks
2021-12-04 19:05:13 +01:00
Riccardo Ferretti
3b40e26a83 fix for #726 - account for both absolute and relative paths when creating files from placeholders 2021-12-03 19:33:05 +01:00
Riccardo Ferretti
bbe44ea21b added documentation for releasing Foam 2021-12-02 16:02:48 +01:00
Riccardo Ferretti
59bb2eb38f updated docs from @memeplex comments in PR #841 2021-12-02 11:15:05 +01:00
Riccardo Ferretti
97f87692b6 v0.16.1 2021-11-30 19:22:01 +01:00
Riccardo Ferretti
4f76a6b24a Prepare for 0.16.1 2021-11-30 19:21:35 +01:00
Riccardo Ferretti
c822589733 fix for #851 - fixed listing resources by ID when files had same suffix 2021-11-30 19:20:23 +01:00
Riccardo Ferretti
b748629c68 v0.16.0 2021-11-24 14:58:32 +01:00
Riccardo Ferretti
b1aa182fac Prepare for 0.16.0 2021-11-24 14:58:00 +01:00
Riccardo
c7155d3956 Completion provider support for unique identifiers (#845) 2021-11-24 14:31:09 +01:00
Riccardo
91385fc937 Added diagnostic with quick fix actions (#844) 2021-11-23 19:35:19 +01:00
Riccardo
9f42893d61 Add support for wikilinks disambiguation (#841)
* using different approach to store/look-up references in FoamWorkspace that also supports better wikilink matching

* added documentation for Foam wikilinks

* added changelog
2021-11-23 17:14:00 +01:00
Riccardo Ferretti
65497ba6d3 v0.15.9 2021-11-23 13:18:57 +01:00
Riccardo Ferretti
f5ad5245b4 prepare 0.15.9 2021-11-23 13:18:44 +01:00
Riccardo
d1a6412cb7 fixed #842 - corrected property name in template metadata, and added test case (#843) 2021-11-23 13:16:55 +01:00
Riccardo Ferretti
e03fcf5dfa v0.15.8 2021-11-22 00:29:08 +01:00
Riccardo Ferretti
f174aa7162 prepare 0.15.8 2021-11-22 00:28:25 +01:00
Riccardo
2d9e1f5903 Fix #836 - make references also links (#840) 2021-11-22 00:24:32 +01:00
Riccardo Ferretti
cf5daa4d22 added screenshots 2021-11-21 23:10:22 +01:00
Riccardo Ferretti
e9eb3032e8 v0.15.7 2021-11-21 19:54:10 +01:00
Riccardo Ferretti
a8a418824f moved screenshots in foam-vscode package 2021-11-21 19:53:18 +01:00
Riccardo Ferretti
dd06d0b805 Prepare 0.15.7 2021-11-21 19:47:19 +01:00
Riccardo
11af331694 Make preview navigation test more robust (#838)
* create test note inside workspace dir

* lint
2021-11-21 19:45:02 +01:00
Riccardo
5da1012fab Fix recent issues with templates (#837)
* Fix #831 - fixed glob used to look for templates

* Fix #834 - ask for note title when creating from template
2021-11-21 18:01:41 +01:00
Martin Laws
8015a35f39 Update @martinlaws all-contributors info (#832) 2021-11-19 17:28:53 +01:00
Riccardo Ferretti
587466a210 v0.15.6 2021-11-18 11:01:24 +01:00
Riccardo
52bc1ba13d fix preview navigation (#830)
Fixes #787
2021-11-17 16:08:08 +01:00
Riccardo
8f045a3ff4 Improve readme (#829)
* Updated display name and description

* Updated readme with screenshots
2021-11-17 15:54:37 +01:00
Riccardo Ferretti
b2be5a7311 Made template tests more robust 2021-11-15 22:44:29 +01:00
Riccardo Ferretti
87e2400070 Link reference definitions are now off by default 2021-11-15 22:43:52 +01:00
Riccardo Ferretti
78e946c177 v0.15.5 2021-11-15 22:16:08 +01:00
Riccardo Ferretti
80e46f7898 Prepare 0.15.5 2021-11-15 22:14:51 +01:00
Zero King
5f89a59b07 Use forEach() consistently in test suite (#826) 2021-11-15 21:21:53 +01:00
Riccardo
f921c095aa Refactored note templates code (#825)
* refactored note templates code

* more tests for "Create from template" commands

* inject resolver

* implemented feedback from PR #827 (Authored by @l2dy)
2021-11-15 21:21:32 +01:00
Riccardo Ferretti
a51e0613ea moved tags-tree-view out of directory 2021-11-11 00:12:16 +01:00
Riccardo
9df71adb64 Removed FoamConfig as not used (#823)
Simplifying the Foam abstractions.
In the end `FoamConfig` was only used by the `Matcher`, so we get rid of it and use the matcher instead
2021-11-11 00:08:20 +01:00
Riccardo
17c216736b Implemented navigation provider for links, definitions and references (#821)
- introduce definition and references support
- changes links to only be used for placeholders
- simplifies configuration

Co-authored-by: Jonas Sprenger <sprengerjo@gmail.com>
2021-11-10 23:58:38 +01:00
Riccardo
66a8c3bd49 In hover provider show one source despite number of links from it (#822) 2021-11-10 13:20:59 +01:00
Riccardo Ferretti
5f7b3b7c02 v0.15.4 2021-11-09 00:34:18 +01:00
Riccardo Ferretti
9ed0d6e18e prepare 0.15.4 2021-11-09 00:33:45 +01:00
Riccardo Ferretti
0140748550 improved URI.toFsPath 2021-11-09 00:24:53 +01:00
Riccardo
356dcc5579 Consolidate use of Foam URI (#820)
* always convert vscode.Uri to foam.URI

* Improve handling on Windows paths in URI

- convert to upper case drive letter
- normalize use of Windows conversion in URI
- added more test cases

* Fixed tests
2021-11-08 23:39:01 +01:00
Riccardo Ferretti
265afdee19 v0.15.3 2021-11-08 11:34:18 +01:00
Riccardo Ferretti
de7c686f75 Prepare 0.15.3 2021-11-08 11:34:05 +01:00
Riccardo
8dfc5bd2ff Throw exception instead of process.exit (#819) 2021-11-08 11:12:15 +01:00
Riccardo
b3c5e75aa2 Fixing some test issues (#818)
* renamed test scripts

* improved hover provider tests

* removed buffering of log lines in test suite
2021-11-06 17:48:27 +01:00
Paul de Raaij
000da4bd1c Allow inclusion of note when using reference definitions (#808)
* Allow inclusion of note when using reference definitions

* Add additional comments
2021-11-04 20:17:03 +01:00
allcontributors[bot]
86749940c2 docs: add AndreiD049 as a contributor for code (#815)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-11-04 13:09:09 +01:00
AndreiD049
27f9a08870 replaced vscode uri with foam uri when generating references (#814) 2021-11-04 11:58:15 +01:00
Riccardo Ferretti
e791726692 fixed logging in test suite 2021-11-03 10:54:15 +01:00
Riccardo Ferretti
a3c00744ca fixed linting errors 2021-11-03 10:52:27 +01:00
allcontributors[bot]
00220b1f6c docs: add memeplex as a contributor for code (#812)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-11-03 10:31:56 +01:00
memeplex
759f4f1963 Avoid delaying decorations on editor switch (#811) 2021-11-03 10:31:20 +01:00
Riccardo Ferretti
d86fc7f433 removed outdated use of links 2021-11-01 20:04:37 +01:00
Riccardo
bd9c6806fa tweaks to test suite (#804) 2021-10-28 23:13:20 +02:00
Riccardo Ferretti
4c9a9cec56 v0.15.2 2021-10-27 12:11:02 +02:00
Riccardo Ferretti
8a91a6ab36 Prepare v0.15.2 2021-10-27 12:05:39 +02:00
Paul de Raaij
667037bc14 Improve generation of link reference definitions (#786)
* Fixes the removal of explicitly defined link references

* Add use case of explicit & implicit
2021-10-27 10:58:10 +02:00
allcontributors[bot]
30cc9fc9f0 docs: add eltociear as a contributor for doc (#801)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-27 10:52:41 +02:00
Ikko Ashimine
abed7be3ec Fix typo in write-your-notes-in-github-gist.md (#798)
recieve -> receive
2021-10-27 10:51:27 +02:00
Riccardo
d31e094358 Added support for target date variables in daily note template (#781)
* added support for target date variables in daily note template

* added FOAM_DATE_* variables to resolver

* Document `FOAM_DATE_*` template variables

* Add CHANGELOG entry

Co-authored-by: Michael Overmeyer <michael.overmeyer@shopify.com>
2021-10-27 10:50:58 +02:00
Riccardo
f320af05c5 Improve graph performance by batching painting (#795) 2021-10-26 13:01:19 +02:00
Paul de Raaij
ee229dac84 Apply entire tag regex to the preview window (#785) 2021-10-25 19:56:51 +02:00
allcontributors[bot]
877d843f60 docs: add Laptop765 as a contributor for doc (#796)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-25 19:51:22 +02:00
Paul
af65e4d5f7 Add some basic details about multi-root workspaces (#775)
* Include details about multi-root workspaces

* Reword and clarify
2021-10-25 19:50:47 +02:00
Riccardo Ferretti
dd9fa0af79 Link decorations now enabled by default 2021-10-25 19:22:32 +02:00
Riccardo Ferretti
14f68aea30 minor fixes around VS Code test configurations and suite reporting 2021-10-25 19:22:00 +02:00
Riccardo Ferretti
f68c2ab6db Color graph filter control based on style of node type 2021-10-22 12:33:19 +02:00
Riccardo Ferretti
bae99a6184 fixed dependencies and types 2021-10-21 10:09:45 +02:00
Riccardo Ferretti
861f7dbba7 v0.15.1 2021-10-21 09:01:19 +02:00
Riccardo Ferretti
6a4b90d6d7 Prepare 0.15.1 2021-10-21 08:59:34 +02:00
Riccardo
f73ddb88d4 Fixed test suite
* tweaking error propagation

* updated xvfb-action version

* improved logging

* only adding actually failing tests to failures (no comment)
2021-10-19 12:42:41 +02:00
Riccardo Ferretti
41ca70f23c better handling of output streams in tests and small change around computing which tests to run 2021-10-19 10:53:19 +02:00
Riccardo
7cf7811b85 Include other connected notes into link hovers (#780)
* Include other connected notes into link hovers

* reorganized hover provider tests and added some
2021-10-11 11:59:15 +02:00
Riccardo
eef0aa7f0b Improved filtering controls on graph visualization (#782)
* improved handling of filters in graph view
* code clean up
2021-10-11 11:55:54 +02:00
Riccardo
d222cfbbec Consolidate foam-core within foam-vscode (#774)
* moved `foam-core` inside `foam-vscode`

* updated contribution guide to reflect new modules setup

* improved testing

* consolidate to root yarn.lock files

* tweaking CI workflow && using github secrets to force cache refresh

* improved linting configuration. `core` module cannot depend on other parts of the `foam-vscode` package
2021-10-09 11:09:02 +02:00
197 changed files with 6124 additions and 16366 deletions

View File

@@ -752,6 +752,60 @@
"contributions": [
"code"
]
},
{
"login": "Laptop765",
"name": "Paul",
"avatar_url": "https://avatars.githubusercontent.com/u/1468359?v=4",
"profile": "https://github.com/Laptop765",
"contributions": [
"doc"
]
},
{
"login": "eltociear",
"name": "Ikko Ashimine",
"avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4",
"profile": "https://bandism.net/",
"contributions": [
"doc"
]
},
{
"login": "memeplex",
"name": "memeplex",
"avatar_url": "https://avatars.githubusercontent.com/u/2845433?v=4",
"profile": "https://github.com/memeplex",
"contributions": [
"code"
]
},
{
"login": "AndreiD049",
"name": "AndreiD049",
"avatar_url": "https://avatars.githubusercontent.com/u/52671223?v=4",
"profile": "https://github.com/AndreiD049",
"contributions": [
"code"
]
},
{
"login": "iam-yan",
"name": "Yan",
"avatar_url": "https://avatars.githubusercontent.com/u/48427014?v=4",
"profile": "https://github.com/iam-yan",
"contributions": [
"doc"
]
},
{
"login": "jimt",
"name": "Jim Tittsler",
"avatar_url": "https://avatars.githubusercontent.com/u/180326?v=4",
"profile": "https://WikiEducator.org/User:JimTittsler",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7,

View File

@@ -1,20 +1,18 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/class-name-casing": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off",
"require-await": "warn"
}
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "import"],
"rules": {
"@typescript-eslint/class-name-casing": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off",
"require-await": "warn"
}
}

View File

@@ -3,17 +3,15 @@ name: CI
on:
push:
branches:
- '**'
# The following will also make the workflow run on all PRs, internal and external.
# This would create duplicate runs, that we are skipping by adding the "if" to the jobs below.
# See https://github.community/t/duplicate-checks-on-push-and-pull-request-simultaneous-event/18012
- master
pull_request:
branches:
- master
jobs:
lint:
name: Lint
runs-on: ubuntu-18.04
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
steps:
- uses: actions/checkout@v1
- name: Setup Node
@@ -27,7 +25,7 @@ jobs:
node_modules
*/*/node_modules
packages/foam-vscode/.vscode-test
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', 'packages/foam-vscode/src/test/run-tests.ts') }}
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', 'packages/foam-vscode/src/test/run-tests.ts') }}-${{ secrets.CACHE_VERSION }}
- name: Install Dependencies
run: yarn
- name: Check Lint Rules
@@ -39,7 +37,6 @@ jobs:
matrix:
os: [macos-10.15, ubuntu-18.04, windows-2019]
runs-on: ${{ matrix.os }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
env:
OS: ${{ matrix.os }}
steps:
@@ -55,12 +52,12 @@ jobs:
node_modules
*/*/node_modules
packages/foam-vscode/.vscode-test
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', 'packages/foam-vscode/src/test/run-tests.ts') }}
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', 'packages/foam-vscode/src/test/run-tests.ts') }}-${{ secrets.CACHE_VERSION }}
- name: Install Dependencies
run: yarn
- name: Build Packages
run: yarn build
- name: Run Tests
uses: GabrielBB/xvfb-action@v1.0
uses: GabrielBB/xvfb-action@v1.4
with:
run: yarn test

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ dist
docs/_site
docs/.sass-cache
docs/.jekyll-metadata
.test-workspace

122
.vscode/launch.json vendored
View File

@@ -3,83 +3,51 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"inputs": [
"version": "0.2.0",
"configurations": [
{
"id": "packageName",
"type": "pickString",
"description": "Select the package in which this test is located",
"options": ["foam-core", "foam-vscode"],
"default": "foam-core"
"name": "Debug Jest Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"${workspaceFolder}/packages/foam-vscode/.test-workspace",
"--disable-extensions",
"--disable-workspace-trust",
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode",
"--extensionTestsPath=${workspaceFolder}/packages/foam-vscode/out/test/suite"
],
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Run VSCode Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode"
],
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"cwd": "${workspaceFolder}/packages/foam-vscode",
"runtimeExecutable": "yarn",
"args": [
"jest",
"--runInBand",
"--watchAll=false"
]
}
],
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"runtimeArgs": ["workspace", "${input:packageName}", "run", "test"], // ${yarnWorkspaceName} is what we're missing
"args": [
"--runInBand"
],
"runtimeExecutable": "yarn",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/tsdx",
"test",
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/packages/foam-core",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Run VSCode Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode"
],
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
// @NOTE: This task is broken. VSCode e2e tests are currently disabled
// due to incompability of jest and mocha inside a typescript monorepo
// Contributions to fix this are welcome!
{
"name": "Test VSCode Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode",
"--extensionTestsPath=${workspaceFolder}/packages/foam-vscode/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Test Core",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/tsdx/dist/index.js",
"args": ["test"],
"cwd": "${workspaceFolder}/packages/foam-core",
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "${defaultBuildTask}"
}
]
]
}

View File

@@ -24,9 +24,9 @@
"prettier.requireConfig": true,
"editor.formatOnSave": true,
"editor.tabSize": 2,
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
"jest.autoRun": "off",
"jest.rootPath": "packages/foam-vscode",
"jest.jestCommandLine": "yarn jest",
"gitdoc.enabled": false,
"jest.autoEnable": false,
"jest.runAllTestsFirst": false,
"search.mode": "reuseEditor"
}

54
.vscode/tasks.json vendored
View File

@@ -1,31 +1,31 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"label": "watch: foam-vscode",
"type": "npm",
"script": "start:vscode",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "always"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "test: all packages",
"type": "npm",
"script": "test",
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
},
}
]
"version": "2.0.0",
"tasks": [
{
"label": "watch: foam-vscode",
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "always"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "test: all packages",
"type": "npm",
"script": "test",
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
}
}
]
}

1
.yarnrc Normal file
View File

@@ -0,0 +1 @@
--ignore-engines true

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 593 KiB

After

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -18,8 +18,6 @@ Before you start contributing we recommend that you read the following links:
We understand that diving in an unfamiliar codebase may seem scary,
to make it easier for new contributors we provide some resources:
- [[architecture]] - This document describes the architecture of Foam and how the repository is structured.
You can also see [existing issues](https://github.com/foambubble/foam/issues) and help out!
Finally, the easiest way to help, is to use it and provide feedback by [submitting issues](https://github.com/foambubble/foam/issues/new/choose) or participating in the [Foam Community Discord](https://foambubble.github.io/join-discord/g)!
@@ -35,22 +33,48 @@ If you're interested in contributing, this short guide will help you get things
`yarn install`
3. This project uses [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). `foam-vscode` relies on `foam-core`. This means we need to compile it before we do any extension development. From the root, run the command:
3. From the root, run the command:
`yarn build`
You should now be ready to start working!
### Structure of the project
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
Exceptions to the monorepo are:
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
- All other [[recommended-extensions]] live in their respective GitHub repos
This project uses [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/).
Originally Foam had:
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VS Code plugin.
To improve DX we have moved the `foam-core` module into `packages/foam-vscode/src/core`, but from a development point of view it's useful to think of the `foam-vscode/src/core` "submodule" as something that might be extracted in the future.
For all intents and purposes this means two things:
1. nothing in `foam-vscode/src/core` should depend on files outside of this directory
2. code in `foam-vscode/src/core` should NOT depend on `vscode` library
We have kept the yarn workspace for the time being as we might use it to pull out `foam-core` in the future, or we might need it for other packages that the VS Code plugin could depend upon (e.g. currently the graph visualization is inside the module, but it might be pulled out if its complexity increases).
### Testing
Code needs to come with tests.
We use the following convention in Foam:
- *.test.ts are unit tests
- *.spec.ts are integration tests
- `*.test.ts` are unit tests
- `*.spec.ts` are integration tests
Also, note that tests in `foam-core` live in the `test` directory.
Tests in `foam-vscode` live alongside the code in `src`.
Tests live alongside the code in `src`.
### The VS Code Extension
@@ -71,5 +95,6 @@ Feel free to modify and submit a PR if this guide is out-of-date or contains err
[//begin]: # "Autogenerated link references for markdown compatibility"
[principles]: principles.md "Principles"
[code-of-conduct]: code-of-conduct.md "Code of Conduct"
[architecture]: dev/architecture.md "Architecture"
[recipes]: recipes/recipes.md "Recipes"
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
[//end]: # "Autogenerated link references"

View File

@@ -1,23 +0,0 @@
---
tags: architecture
---
# Architecture
This document aims to provide a quick overview of the Foam architecture!
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VSCode plugin.
Exceptions to the monorepo are:
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
- All other [[recommended-extensions]] live in their respective GitHub repos.
- [foam-cli](https://github.com/foambubble/foam-cli) - The Foam CLI tool.
[//begin]: # "Autogenerated link references for markdown compatibility"
[recipes]: ../recipes/recipes.md "Recipes"
[recommended-extensions]: ../recommended-extensions.md "Recommended Extensions"
[//end]: # "Autogenerated link references"

View File

@@ -0,0 +1,27 @@
# Releasing Foam
1. Get to the latest code
- `git checkout master && git fetch && git rebase`
2. Sanity checks
- `yarn reset`
- `yarn test`
3. Update change log
- `./packages/foam-vscode/CHANGELOG.md`
- `git add *`
- `git commit -m"Preparation for next release"`
4. Update version
- `$ cd packages/foam-vscode`
- `foam-vscode$ yarn lerna version <version>` (where `version` is `patch/minor/major`)
- `cd ../..`
5. Package extension
- `$ yarn vscode:package-extension`
6. Publish extension
- `$ yarn vscode:publish-extension`
7. Update the release notes in GitHub
- in GitHub, top right, click on "releases"
- select "tags" in top left
- select the tag that was just released, click "edit" and copy release information from changelog
- publish (no need to attach artifacts)
8. Annouce on Discord
Steps 1 to 6 should really be replaced by a GitHub action...

View File

@@ -41,12 +41,42 @@ Templates can use all the variables available in [VS Code Snippets](https://code
In addition, you can also use variables provided by Foam:
| Name | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new note. |
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| Name | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new note. |
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables.
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables. See [#693](https://github.com/foambubble/foam/issues/693).
### `FOAM_DATE_*` variables
Foam defines its own set of datetime variables that have a similar behaviour as [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
For example, `FOAM_DATE_YEAR` has the same behaviour as VS Code's `CURRENT_YEAR`, `FOAM_DATE_SECONDS_UNIX` has the same behaviour as `CURRENT_SECONDS_UNIX`, etc.
By default, prefer using the `FOAM_DATE_` versions. The datetime used to compute the values will be the same for both `FOAM_DATE_` and VS Code's variables, with the exception of the creation notes using the daily note template.
#### Relative daily notes
When referring to daily notes, you can use the relative snippets (`/+1d`, `/tomorrow`, etc.). In these cases, the new notes will be created with the daily note template, but the datetime used should be the relative datetime, not the current datetime.
By using the `FOAM_DATE_` versions of the variables, the correct relative date will populate the variables, instead of the current datetime.
For example, given this daily note template (`.foam/templates/daily-note.md`):
```markdown
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE
## Here's what I'm going to do today
* Thing 1
* Thing 2
```
When the `/tomorrow` snippet is used, `FOAM_DATE_` variables will be populated with tomorrow's date, as expected.
If instead you were to use the VS Code versions of these variables, they would be populated with today's date, not tomorrow's, causing unexpected behaviour.
When creating notes in any other scenario, the `FOAM_DATE_` values are computed using the same datetime as the VS Code ones, so the `FOAM_DATE_` versions can be used in all scenarios by default.
## Metadata

View File

@@ -72,6 +72,8 @@ These instructions assume you have a GitHub account, and you have Visual Studio
After setting up the repository, open `.vscode/settings.json` and edit, add or remove any settings you'd like for your Foam workspace.
* *If using a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) as noted above, make sure that your **Foam** directory is first in the list. There are some settings that will need to be migrated from `.vscode/settings.json` to your `.code-workspace` file.*
To learn more about how to use **Foam**, read the [[recipes]].
Getting stuck in the setup? Read the [[frequently-asked-questions]].
@@ -210,6 +212,14 @@ If that sounds like something you're interested in, I'd love to have you along o
<td align="center"><a href="https://github.com/theowenyoung"><img src="https://avatars.githubusercontent.com/u/62473795?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Owen Young</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=theowenyoung" title="Documentation">📖</a> <a href="#content-theowenyoung" title="Content">🖋</a></td>
<td align="center"><a href="http://www.prashu.com"><img src="https://avatars.githubusercontent.com/u/476729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Prashanth Subrahmanyam</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ksprashu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/JonasSprenger"><img src="https://avatars.githubusercontent.com/u/25108895?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonas SPRENGER</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=JonasSprenger" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Laptop765"><img src="https://avatars.githubusercontent.com/u/1468359?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Laptop765" title="Documentation">📖</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/memeplex"><img src="https://avatars.githubusercontent.com/u/2845433?v=4?s=60" width="60px;" alt=""/><br /><sub><b>memeplex</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=memeplex" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
</tr>
</table>

View File

@@ -0,0 +1,93 @@
# Wikilinks in Foam
Foam supports standard wikilinks in the format `[[wikilink]]`.
Wikilinks can refer to any note or attachment in the repo: `[[note.md]]`, `[[doc.pdf]]`, `[[image.jpg]]`.
The usual wikilink syntax without extension refers to notes: `[[wikilink]]` and `[[wikilink.md]]` are equivalent.
The goal of wikilinks is to uniquely identify a file in a repo, no matter in which directory it lives.
Sometimes in a repo you can have files with the same name in different directories.
Foam allows you to identify those files using the minimum effort needed to disambiguate them.
This is achieved by adding as many directories above the file needed to uniquely identify the link, e.g. `[[house/todo]]`.
See below for more details.
## Goals for wikilinks in Foam
Wikilinks in Foam are meant to satisfy the following:
- make it easy for users to identify a resource
- make it interoperable with other similar note taking systems (Obsidian, Dendron, ...)
- be easy to get started with, but satisfy growing needs
## Types of wikilinks supported in Foam
Foam supports two types of keys inside a wikilink: a **path** reference and an **identifier** reference:
- `[[./file]]` and `[[../to/another/file]]` are **path** links to a resource, relative _from the source_
- `[[/path/to/file]]` is a **path** link to a resource, relative _from the repo root_
- `[[file]]` is an **identifier** of a resource (based on the filename)
- `[[path/to/file]]` is an **identifier** of a resource (based on the path), the same is true for `[[to/file]]`
It's important to note that sometimes identifier keys can't uniquely locale a resource.
A more concrete example will help:
```
/
projects/
house/
todo.md
buy-car/
todo.md
cars.md
work/
todo.md
notes.md
```
In the above repo:
- `[[cars]]` is a unique identifier of a resource - it can be used anywhere in the repo
- `[[todo]]` is an non-unique identifier as it can refer to multiple resources
- `[[house/todo]]` is a unique identifier of a resource - it can be used anywhere in the repo
- `[[projects/house/todo]]` is a unique identifier of a resource - it can be used anywhere in the repo
- `[[/projects/house/todo]]` is a path reference to a resource
- `[[./todo]]` is a path reference to a resource (e.g. from `/projects/buy-car/cars.md`)
Basically we could say as a rule:
- if the link starts with `/` or `.` we consider it a **path** reference, in the first case from the repo root, otherwise from the source note
- if a link doesn't start with `/` or `.` it is an **identifier**
- generally speaking we use the shortest identifier available to identify a resource, **but all are valid**
- `[[projects/buy-car/cars]]`, `[[buy-car/cars]]`, `[[cars]]` are all unique identifier to the same resource, and are all valid in a document
- the same can be said for `[[projects/house/todo]]` and `[[house/todo]]` - but not for `[[todo]]`, because it can refer to more than one resource
## Compatibility with other apps
| Scenario | Obsidian | Foam |
| --------------------------- | ------------------------------- | ------------------------------- |
| 1 `[[notes]]` | ✔ unique identifier in repo | ✔ unique identifier in repo |
| 2 `[[/work/notes]]` | ✔ valid path from repo root | ✔ valid path from repo root |
| 3 `[[work/notes]]` | ✔ valid path from repo root | ✔ valid identifier in repo |
| 4 `[[project/house/todo]]` | ✔ valid path from repo root | ✔ valid unique identifier |
| 5 `[[/project/house/todo]]` | ✔ valid path from repo root | ✔ valid path from repo root |
| 6 `[[house/todo]]` | ✔ valid unique identifier | ✔ valid unique identifier |
| 7 `[[todo]]` | ✘ ambiguous identifier | ✘ ambiguous identifier |
| 8 `[[/house/todo]]` | ✘ incorrect path from repo root | ✘ incorrect path from repo root |
## Non-unique identifiers
We can't prevent non-unique identifiers from occurring in Foam (first and foremost because a file could be edited with another editor) but we can flag them.
Therefore Foam follows the following strategy instead:
1. there is a clear resolution mechanism (alphabetic) so that if nothing changes a non-unique identifier will always return the same note. Resolution has to be deterministic
2. a diagnostic entry (warning or error) is showed to the user for non-unique identifiers, so she knows that she's using a "risky" identifier
1. The quick resolution for this item will show the available unique identifiers matching the non-unique one
## Thanks
Thanks to [@memplex](https://github.com/memeplex) for helping with the thinking around this proposal.

View File

@@ -34,7 +34,7 @@ Once you've opened/created the Foam repository, it will appear in the `Repositor
## Editing your workspace
When you create or open a page, you can edit the markdown content as usual, as well as [paste images](https://github.com/vsls-contrib/gistpad#pasting-images-1), and create [`[[links]]` to other pages](https://github.com/vsls-contrib/gistpad#links). When you type `[[`, you'll recieve auto-completion for the existing pages in your workspace, and you can also automatically create new pages by simply creating a link to it.
When you create or open a page, you can edit the markdown content as usual, as well as [paste images](https://github.com/vsls-contrib/gistpad#pasting-images-1), and create [`[[links]]` to other pages](https://github.com/vsls-contrib/gistpad#links). When you type `[[`, you'll receive auto-completion for the existing pages in your workspace, and you can also automatically create new pages by simply creating a link to it.
Since you're using the Visual Studio Code markdown editor, you can benefit from all of the rich language services (e.g. syntax highlighting, header collapsing), as well as the extension ecosystem (e.g. [Emojisense](https://marketplace.visualstudio.com/items?itemName=bierner.emojisense)).

View File

@@ -3,7 +3,6 @@
Foam enables you to Link pages together using `[[file-name]]` annotations (i.e. `[[MediaWiki]]` links).
- Type `[[` and start typing a file name for autocompletion.
- Note that your file names should be in `lower-dash-case.md`, and your wikilinks should reference file names exactly: `[[lower-dash-case]]`, not `[[Lower Dash Case]]`.
- See [[link-formatting-and-autocompletion]] for more information, and how to setup your link autocompletions to make this easier.
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on file name to navigate to file (`F12` also works while your cursor is on the file name)
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on non-existent file to create that file in the workspace.
@@ -11,6 +10,10 @@ Foam enables you to Link pages together using `[[file-name]]` annotations (i.e.
> If the `F12` shortcut feels unnatural you can rebind it at File > Preferences > Keyboard Shortcuts by searching for `editor.action.revealDefinition`.
## Support for sections
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
## Markdown compatibility
The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) extension automatically generates [[link-reference-definitions]] at the bottom of the file to make wikilinks compatible with Markdown tools and parsers.

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.15.0"
"version": "0.17.2"
}

View File

@@ -9,10 +9,6 @@
"packages/*"
],
"scripts": {
"start:vscode": "yarn workspace foam-vscode vscode:start-debugging",
"build:core": "yarn workspace foam-core build",
"watch:core": "yarn workspace foam-core start",
"test:core": "yarn workspace foam-core test",
"vscode:package-extension": "yarn workspace foam-vscode package-extension",
"vscode:install-extension": "yarn workspace foam-vscode install-extension",
"vscode:publish-extension": "yarn workspace foam-vscode publish-extension",

View File

@@ -1,3 +0,0 @@
module.exports = {
plugins: [['@babel/plugin-transform-runtime', { helpers: false }]],
};

View File

@@ -1,21 +0,0 @@
# Foam Core
This module contains the core functions, model, and API of Foam.
It is used by its clients to integrate Foam in various use cases, from VsCode extension, to CLI, to CI integrations.
## Local Development
Below is a list of commands you will probably find useful.
### `yarn watch`
Runs the project in development/watch mode. Your project will be rebuilt upon changes.
### `yarn build`
Bundles the package to the `dist` folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
### `yarn test`
Runs the test watcher (Jest) in an interactive mode.
By default, runs tests related to files changed since the last commit.

View File

@@ -1,49 +0,0 @@
{
"name": "foam-core",
"repository": "https://github.com/foambubble/foam",
"version": "0.15.0",
"license": "MIT",
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist",
"build": "tsdx build --tsconfig ./tsconfig.build.json",
"test": "tsdx test",
"lint": "tsdx lint src test",
"watch": "tsdx watch",
"prepare": "tsdx build --tsconfig ./tsconfig.build.json"
},
"devDependencies": {
"@babel/core": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@types/github-slugger": "^1.3.0",
"@types/lodash": "^4.14.157",
"@types/micromatch": "^4.0.1",
"@types/picomatch": "^2.2.1",
"husky": "^4.2.5",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.9.5"
},
"dependencies": {
"detect-newline": "^3.1.0",
"fast-array-diff": "^1.0.1",
"github-slugger": "^1.3.0",
"glob": "^7.1.6",
"lodash": "^4.17.21",
"micromatch": "^4.0.2",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",
"remark-wiki-link": "^0.0.4",
"replace-ext": "^2.0.0",
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2",
"yaml": "^1.10.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts"
}

View File

@@ -1,75 +0,0 @@
import { readFileSync } from 'fs';
import { merge } from 'lodash';
import { Logger } from './utils/log';
import { URI } from './model/uri';
export interface FoamConfig {
workspaceFolders: URI[];
includeGlobs: string[];
ignoreGlobs: string[];
get<T>(path: string): T | undefined;
get<T>(path: string, defaultValue: T): T;
}
const DEFAULT_INCLUDES = ['**/*'];
const DEFAULT_IGNORES = ['**/node_modules/**'];
export const createConfigFromObject = (
workspaceFolders: URI[],
include: string[],
ignore: string[],
settings: any
) => {
const config: FoamConfig = {
workspaceFolders: workspaceFolders,
includeGlobs: include,
ignoreGlobs: ignore,
get: <T>(path: string, defaultValue?: T) => {
const tokens = path.split('.');
const value = tokens.reduce((acc, t) => acc?.[t], settings);
return value ?? defaultValue;
},
};
return config;
};
export const createConfigFromFolders = (
workspaceFolders: URI[] | URI,
options: {
include?: string[];
ignore?: string[];
} = {}
): FoamConfig => {
if (!Array.isArray(workspaceFolders)) {
workspaceFolders = [workspaceFolders];
}
const workspaceConfig: any = workspaceFolders.reduce(
(acc, f) => merge(acc, parseConfig(URI.joinPath(f, 'config.json'))),
{}
);
// For security reasons local plugins can only be
// activated via user config
if ('experimental' in workspaceConfig) {
delete workspaceConfig['experimental']['localPlugins'];
}
const userConfig = parseConfig(URI.file(`~/.foam/config.json`));
const settings = merge(workspaceConfig, userConfig);
return createConfigFromObject(
workspaceFolders,
options.include ?? DEFAULT_INCLUDES,
options.ignore ?? DEFAULT_IGNORES,
settings
);
};
const parseConfig = (path: URI) => {
try {
return JSON.parse(readFileSync(URI.toFsPath(path), 'utf8'));
} catch {
Logger.debug('Could not read configuration from ' + URI.toString(path));
}
};

View File

@@ -1,64 +0,0 @@
import {
Resource,
ResourceLink,
NoteLinkDefinition,
ResourceParser,
Tag,
} from './model/note';
import { FoamConfig } from './config';
import {
IDataStore,
FileDataStore,
Matcher,
IMatcher,
} from './services/datastore';
import { ILogger } from './utils/log';
import { IDisposable, isDisposable } from './common/lifecycle';
import { FoamWorkspace } from './model/workspace';
import { FoamGraph } from '../src/model/graph';
import { URI } from './model/uri';
import { FoamTags } from '../src/model/tags';
export { Position } from './model/position';
export { Range } from './model/range';
export { IDataStore, FileDataStore, Matcher, IMatcher };
export { ILogger };
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
export { Event, Emitter } from './common/event';
export { FoamConfig };
export { ResourceProvider } from './model/provider';
export { IDisposable, isDisposable };
export {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
createMarkdownParser,
MarkdownResourceProvider,
} from './markdown-provider';
export {
TextEdit,
generateHeading,
generateLinkReferences,
getKebabCaseFileName,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './janitor';
export { applyTextEdit } from './janitor/apply-text-edit';
export { createConfigFromFolders } from './config';
export { Foam, Services, bootstrap } from './model/foam';
export {
Resource,
ResourceLink,
URI,
Tag,
FoamWorkspace,
FoamGraph,
FoamTags,
NoteLinkDefinition,
ResourceParser,
};

View File

@@ -1,178 +0,0 @@
import * as path from 'path';
import { Resource, ResourceLink } from './note';
import { URI } from './uri';
import { isSome, isNone } from '../utils';
import { Emitter } from '../common/event';
import { IDisposable } from '../index';
import { ResourceProvider } from './provider';
export function getReferenceType(
reference: URI | string
): 'uri' | 'absolute-path' | 'relative-path' | 'key' {
if (URI.isUri(reference)) {
return 'uri';
}
const isPath = reference.split('/').length > 1;
if (!isPath) {
return 'key';
}
const isAbsPath = isPath && reference.startsWith('/');
return isAbsPath ? 'absolute-path' : 'relative-path';
}
const pathToResourceId = (pathValue: string) => {
const { ext } = path.parse(pathValue);
return ext.length > 0 ? pathValue : pathValue + '.md';
};
const uriToResourceId = (uri: URI) => pathToResourceId(uri.path);
const pathToResourceName = (pathValue: string) =>
path.parse(pathValue).name.toLowerCase();
export const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
export class FoamWorkspace implements IDisposable {
private onDidAddEmitter = new Emitter<Resource>();
private onDidUpdateEmitter = new Emitter<{ old: Resource; new: Resource }>();
private onDidDeleteEmitter = new Emitter<Resource>();
onDidAdd = this.onDidAddEmitter.event;
onDidUpdate = this.onDidUpdateEmitter.event;
onDidDelete = this.onDidDeleteEmitter.event;
private providers: ResourceProvider[] = [];
/**
* Resources by key / slug
*/
private resourcesByName: Map<string, string[]> = new Map();
/**
* Resources by URI
*/
private resources: Map<string, Resource> = new Map();
registerProvider(provider: ResourceProvider) {
this.providers.push(provider);
return provider.init(this);
}
set(resource: Resource) {
const id = uriToResourceId(resource.uri);
const old = this.find(resource.uri);
const name = uriToResourceName(resource.uri);
this.resources.set(id, resource);
if (!this.resourcesByName.has(name)) {
this.resourcesByName.set(name, []);
}
this.resourcesByName.get(name)?.push(id);
isSome(old)
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
: this.onDidAddEmitter.fire(resource);
return this;
}
delete(uri: URI) {
const id = uriToResourceId(uri);
const deleted = this.resources.get(id);
this.resources.delete(id);
const name = uriToResourceName(uri);
this.resourcesByName.set(
name,
this.resourcesByName.get(name)?.filter(resId => resId !== id) ?? []
);
if (this.resourcesByName.get(name)?.length === 0) {
this.resourcesByName.delete(name);
}
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
return deleted ?? null;
}
public exists(uri: URI): boolean {
return (
!URI.isPlaceholder(uri) &&
isSome(this.resources.get(uriToResourceId(uri)))
);
}
public list(): Resource[] {
return Array.from(this.resources.values());
}
public get(uri: URI): Resource {
const note = this.find(uri);
if (isSome(note)) {
return note;
} else {
throw new Error('Resource not found: ' + uri.path);
}
}
public find(resourceId: URI | string, reference?: URI): Resource | null {
const refType = getReferenceType(resourceId);
switch (refType) {
case 'uri':
const uri = resourceId as URI;
return this.exists(uri)
? this.resources.get(uriToResourceId(uri)) ?? null
: null;
case 'key':
const name = pathToResourceName(resourceId as string);
let paths = this.resourcesByName.get(name);
if (isNone(paths) || paths.length === 0) {
paths = this.resourcesByName.get(resourceId as string);
}
if (isNone(paths) || paths.length === 0) {
return null;
}
// prettier-ignore
const sortedPaths = paths.length === 1
? paths
: paths.sort((a, b) => a.localeCompare(b));
return this.resources.get(sortedPaths[0]) ?? null;
case 'absolute-path':
const resourceUri = URI.file(resourceId as string);
return this.resources.get(uriToResourceId(resourceUri)) ?? null;
case 'relative-path':
if (isNone(reference)) {
return null;
}
const relativePath = resourceId as string;
const targetUri = URI.computeRelativeURI(reference, relativePath);
return this.resources.get(uriToResourceId(targetUri)) ?? null;
default:
throw new Error('Unexpected reference type: ' + refType);
}
}
public resolveLink(resource: Resource, link: ResourceLink): URI {
// TODO add tests
const provider = this.providers.find(p => p.supports(resource.uri));
return (
provider?.resolveLink(this, resource, link) ??
URI.placeholder(link.target)
);
}
public read(uri: URI): Promise<string | null> {
const provider = this.providers.find(p => p.supports(uri));
return provider?.read(uri) ?? Promise.resolve(null);
}
public readAsMarkdown(uri: URI): Promise<string | null> {
const provider = this.providers.find(p => p.supports(uri));
return provider?.readAsMarkdown(uri) ?? Promise.resolve(null);
}
public dispose(): void {
this.onDidAddEmitter.dispose();
this.onDidDeleteEmitter.dispose();
this.onDidUpdateEmitter.dispose();
}
}

View File

@@ -1,5 +0,0 @@
import GithubSlugger from 'github-slugger';
import { URI } from '../model/uri';
export const uriToSlug = (uri: URI): string =>
GithubSlugger.slug(URI.getBasename(uri));

View File

@@ -1,44 +0,0 @@
import { createConfigFromFolders } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/model/uri';
Logger.setLevel('error');
const testFolder = URI.joinPath(URI.file(__dirname), 'test-config');
describe('Foam configuration', () => {
it('can read settings from config.json', () => {
const config = createConfigFromFolders([
URI.joinPath(testFolder, 'folder1'),
]);
expect(config.get('feature1.setting1.value')).toBeTruthy();
expect(config.get('feature2.value')).toEqual(12);
const section = config.get<{ value: boolean }>('feature1.setting1');
expect(section!.value).toBeTruthy();
});
it('can merge settings from multiple foam folders', () => {
const config = createConfigFromFolders([
URI.joinPath(testFolder, 'folder1'),
URI.joinPath(testFolder, 'folder2'),
]);
// override value
expect(config.get('feature1.setting1.value')).toBe(false);
// this was not overridden
expect(config.get('feature1.setting1.extraValue')).toEqual('go foam');
// new value from second config file
expect(config.get('feature1.setting1.value2')).toBe('hello');
// this whole section doesn't exist in second file
expect(config.get('feature2.value')).toEqual(12);
});
it('cannot activate local plugins from workspace config', () => {
const config = createConfigFromFolders([
URI.joinPath(testFolder, 'enable-plugins'),
]);
expect(config.get('experimental.localPlugins.enabled')).toBeUndefined();
});
});

View File

@@ -1,99 +0,0 @@
import path from 'path';
import { FoamWorkspace } from '../src';
import { NoteLinkDefinition, Resource } from '../src/model/note';
import { IDataStore, Matcher } from '../src/services/datastore';
import { MarkdownResourceProvider } from '../src/markdown-provider';
import { Range } from '../src/model/range';
import { URI } from '../src/model/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const position = Range.create(0, 0, 0, 100);
const documentStart = position.start;
const documentEnd = position.end;
const eol = '\n';
/**
* Turns a string into a URI
* The goal of this function is to make sure we are consistent in the
* way we generate URIs (and therefore IDs) across the tests
*/
export const strToUri = URI.file;
export const noOpDataStore = (): IDataStore => ({
read: _ => Promise.resolve(''),
list: _ => Promise.resolve([]),
});
export const createTestWorkspace = () => {
const workspace = new FoamWorkspace();
const matcher = new Matcher([URI.file('/')], ['**/*']);
const provider = new MarkdownResourceProvider(
matcher,
undefined,
undefined,
noOpDataStore()
);
workspace.registerProvider(provider);
return workspace;
};
export const createTestNote = (params: {
uri: string;
title?: string;
definitions?: NoteLinkDefinition[];
links?: Array<{ slug: string } | { to: string }>;
text?: string;
root?: URI;
tags?: string[];
}): Resource => {
const root = params.root ?? URI.file('/');
return {
uri: URI.resolve(params.uri, root),
type: 'note',
properties: {},
title: params.title ?? path.parse(strToUri(params.uri).path).base,
definitions: params.definitions ?? [],
tags:
params.tags?.map(t => ({
label: t,
range: Range.create(0, 0, 0, 0),
})) ?? [],
links: params.links
? params.links.map((link, index) => {
const range = Range.create(
position.start.line + index,
position.start.character,
position.start.line + index,
position.end.character
);
return 'slug' in link
? {
type: 'wikilink',
target: link.slug,
label: link.slug,
range: range,
rawText: 'link text',
}
: {
type: 'link',
target: link.to,
label: 'link text',
range: range,
};
})
: [],
source: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
text: params.text ?? '',
},
};
};
describe('Test utils', () => {
it('are happy', () => {});
});

View File

@@ -1,51 +0,0 @@
import { Logger } from '../src';
import { URI } from '../src/model/uri';
import { uriToSlug } from '../src/utils/slug';
Logger.setLevel('error');
describe('Foam URIs', () => {
describe('URI parsing', () => {
const base = URI.file('/path/to/file.md');
test.each([
['https://www.google.com', URI.parse('https://www.google.com')],
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
['../relative/file.md', URI.file('/path/relative/file.md')],
['#section', URI.create({ ...base, fragment: 'section' })],
[
'../relative/file.md#section',
URI.parse('file:/path/relative/file.md#section'),
],
])('URI Parsing (%s) - %s', (input, exp) => {
const result = URI.resolve(input, base);
expect(result.scheme).toEqual(exp.scheme);
expect(result.authority).toEqual(exp.authority);
expect(result.path).toEqual(exp.path);
expect(result.query).toEqual(exp.query);
expect(result.fragment).toEqual(exp.fragment);
});
});
it('supports various cases', () => {
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
'no-directory'
);
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
'manydotsname'
);
});
it('computes a relative uri using a slug', () => {
expect(
URI.computeRelativeURI(URI.file('/my/file.md'), '../hello.md')
).toEqual(URI.file('/hello.md'));
expect(URI.computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
URI.file('/hello.md')
);
expect(
URI.computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
).toEqual(URI.file('/hello.markdown'));
});
});

View File

@@ -1,6 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext"
}
}

View File

@@ -1,29 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "src",
"composite": true,
"esModuleInterop": true,
"importHelpers": true,
"downlevelIteration": true,
// commonjs module format is used so that the incremental
// tsc build-mode ran during development can replace individual
// files (as opposed to generate the .cjs.development.js bundle.
//
// this is overridden in tsconfig.build.json for distribution
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "./src",
"sourceMap": true,
"strict": true,
"lib": [
"ES2019", "es2020.string"
]
},
"include": [
"src",
"types"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,126 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [0.17.2] - 2021-12-22
Fixes and Improvements:
- Improved support for wikilinks in titles (#878)
- Use syntax injection for wikilinks (#876 - thanks @memeplex)
- Fix when applying text edits in last line
Internal:
- DX: Clean up of testing setup (#881 - thanks @memeplex)
## [0.17.1] - 2021-12-16
Fixes and Improvements:
- Decorate markdown files only (#857)
- Fix template placeholders issue (#859)
- Improved replacement range for link completion
Internal:
- Major URI/path handling refactoring (#858 - thanks @memeplex)
## [0.17.0] - 2021-12-08
Features:
- Added first class support for sections (#856)
- Sections can be referred to in wikilinks
- Sections can be embedded
- Autocompletion for sections
- Diagnostic for sections
- Embed sections
## [0.16.1] - 2021-11-30
Fixes and Improvements:
- Fixed diagnostic bug triggered when file had same suffix (#851)
## [0.16.0] - 2021-11-24
Features:
- Added support for unique wikilink identifiers (#841)
- This change allows files that have the same name to be uniquely referenced as wikilinks
- BREAKING CHANGE: wikilinks to attachments must now include the extension
- Added diagnostics for ambiguous wikilinks, with quick fixes available (#844)
- Added support for unique wikilinks in autocompletion (#845)
## [0.15.9] - 2021-11-23
Fixes and Improvements:
- Fixed filepath retrieval when creating note from template (#843)
## [0.15.8] - 2021-11-22
Fixes and Improvements:
- Re-enable link navigation for wikilinks (#840)
## [0.15.7] - 2021-11-21
Fixes and Improvements:
- Fixed template listing (#831)
- Fixed note creation from template (#834)
## [0.15.6] - 2021-11-18
Fixes and Improvements:
- Link Reference Generation is now OFF by default
- Fixed preview navigation (#830)
## [0.15.5] - 2021-11-15
Fixes and Improvements:
- Major improvement in navigation. Use link definitions and link references (#821)
- Fixed bug showing in hover reference the same more than once when it had multiple links to another (#822)
Internal:
- Foam URI refactoring (#820)
- Template service refactoring (#825)
## [0.15.4] - 2021-11-09
Fixes and Improvements:
- Detached Foam URI from VS Code URI. This should improve several path related issues in Windows. Given how core this change is, the release is just about this refactoring to easily detect possible side effects.
## [0.15.3] - 2021-11-08
Fixes and Improvements:
- Avoid delaying decorations on editor switch (#811 - thanks @memeplex)
- Fix preview issue when embedding a note and using reference definitions (#808 - thanks @pderaaij)
## [0.15.2] - 2021-10-27
Features:
- Added `FOAM_DATE_*` template variables (#781)
Fixes and Improvements:
- Dataviz: apply note type color to filter item label
- Dataviz: optimized rendering of graph to reduce load on CPU (#795)
- Preview: improved tag highlight in preview (#785 - thanks @pderaaij)
- Better handling of link reference definition (#786 - thanks @pderaaij)
- Link decorations are now enabled by default (can be turned off in settings)
## [0.15.1] - 2021-10-21
Fixes and Improvements:
- Improved filtering controls for graph (#782)
- Link Hover: Include other connected notes to link target
## [0.15.0] - 2021-10-04
Features:
@@ -18,7 +138,7 @@ Fixes and Improvements:
## [0.14.2] - 2021-07-24
Features:
Features:
- Autocompletion for tags (#708 - thanks @pderaaij)
- Use templates for new note created from wikilink (#712 - thanks @movermeyer)
@@ -35,7 +155,7 @@ Fixes and Improvements:
## [0.14.0] - 2021-07-13
Features:
Features:
- Create new note from selection (#666 - thanks @pderaaij)
- Use templates for daily notes (#700 - thanks @movermeyer)
@@ -63,9 +183,9 @@ Fixes and Improvements:
- Fixed #667, incorrect resolution of foam-core library
Internal:
Internal:
- BREAKING CHANGE: Removed Foam local plugins
- BREAKING CHANGE: Removed Foam local plugins
If you were previously using the alpha feature of Foam local plugins you will soon be able to migrate the functionality to the V1 API
## [0.13.6] - 2021-06-05

View File

@@ -5,15 +5,127 @@
[![Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
> ⚠️ This is an early stage software. Use at your own peril.
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VsCode, which means you can pair it with your favorite extensions for a great editing experience.
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VS Code, which means you can pair it with your favorite extensions for a great editing experience.
Foam is open source, and allows you to create a local first, markdown based, personal knowledge base. You can also use it to publish your notes.
Foam is also meant to be extensible, so you can integrate with its internals to customize your knowledge base.
## Features
### Graph Visualization
See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command.
![Graph Visualization](./assets/screenshots/feature-show-graph.gif)
### Link Autocompletion
Foam helps you create the connections between your notes, and your placeholders as well.
![Link Autocompletion](./assets/screenshots/feature-link-autocompletion.gif)
### Unique identifiers across directories
Foam supports files with the same name in multiple directories.
It will use the minimum identifier required, and even report and help you fix existing ambiguous wikilinks.
![Unique identifier autocompletion](./assets/screenshots/feature-unique-wikilink-completion.gif)
![Wikilink diagnostic](./assets/screenshots/feature-wikilink-diagnostics.gif)
### Link Preview and Navigation
![Link Preview and Navigation](./assets/screenshots/feature-navigation.gif)
### Go to definition, Peek References
See where a note is being referenced in your knowledge base.
![Go to Definition, Peek References](./assets/screenshots/feature-definition-references.gif)
### Navigation in Preview
Navigate your rendered notes in the VS Code preview panel.
![Navigation in Preview](./assets/screenshots/feature-preview-navigation.gif)
### Note embed
Embed the content from other notes.
![Note Embed](./assets/screenshots/feature-note-embed.gif)
### Support for sections
Foam supports autocompletion, navigation, embedding and diagnostics for note sections.
Just use the standard wiki syntax of `[[resource#Section Title]]`.
### Link Alias
Foam supports link aliasing, so you can have a `[[wikilink]]`, or a `[[wikilink|alias]]`.
### Templates
Use [custom templates](https://foambubble.github.io/foam/features/note-templates) to have avoid repetitve work on your notes.
![Templates](./assets/screenshots/feature-templates.gif)
### Backlinks Panel
Quickly check which notes are referencing the currently active note.
See for each occurrence the context in which it lives, as well as a preview of the note.
![Backlinks Panel](./assets/screenshots/feature-backlinks-panel.gif)
### Tag Explorer Panel
Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags).
Foam also supports hierarchical tags.
![Tag Explorer Panel](./assets/screenshots/feature-tags-panel.gif)
### Orphans and Placeholder Panels
Orphans are note that have no inbound nor outbound links.
Placeholders are dangling links, or notes without content.
Keep them under control, and your knowledge base in better state, by using this panel.
![Orphans and Placeholder Panels](./assets/screenshots/feature-placeholder-orphan-panel.gif)
### Syntax highlight
Foam highlights wikilinks and placeholder differently, to help you visualize your knowledge base.
![Syntax Highlight](./assets/screenshots/feature-syntax-highlight.png)
### Daily note
Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes).
![Daily Note](./assets/screenshots/feature-daily-note.gif)
### Generate references for your wikilinks
Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wikilinks]]`, to use your notes in a non-Foam workspace.
With references you can also make your notes navigable both in GitHub UI as well as GitHub Pages.
![Generate references](./assets/screenshots/feature-definitions-generation.gif)
### Commands
- Explore your knowledge base with the `Foam: Open Random Note` command
- Access your daily note with the `Foam: Open Daily Note` command
- Create a new note with the `Foam: Create New Note` command
- This becomes very powerful when combined with [note templates](https://foambubble.github.io/foam/features/note-templates) and the `Foam: Create New Note from Template` command
- See your workspace as a connected graph with the `Foam: Show Graph` command
## Recipes
People use Foam in different ways for different use cases, check out the [recipes](https://foambubble.github.io/foam/recipes/recipes) page for inspiration!
## Getting started
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
@@ -22,24 +134,13 @@ You really, _really_, **really** should read [Foam documentation](https://foambu
2. Clone the repository and open it in VS Code.
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).
This will also install `Foam for VSCode`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
## Features
- Connect your notes using [`[[wikilinks]]`](https://foambubble.github.io/foam/features/backlinking)
- Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wikilinks]]`, to use your notes in a non-foam workspace
- See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command
- Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags)
- Make your notes navigable both in GitHub UI as well as GitHub Pages
- Use [custom templates](https://foambubble.github.io/foam/features/note-templates) for your notes
- Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes)
- Explore your knowledge base with the `Foam: Open Random Note` command
This will also install `Foam`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
## Requirements
High tolerance for alpha-grade software.
Foam is still a Work in Progress.
Rest assured it will never lock you in, nor compromise your files, but sometimes some features might break ;)
## Known Issues

View File

Before

Width:  |  Height:  |  Size: 604 KiB

After

Width:  |  Height:  |  Size: 604 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -1,7 +0,0 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript"
],
plugins: [["@babel/plugin-transform-runtime", { helpers: false }]]
};

View File

@@ -82,7 +82,7 @@ module.exports = {
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: ['.vscode-test'],
// Activates notifications for test results
// notify: false,
@@ -91,7 +91,7 @@ module.exports = {
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
@@ -126,13 +126,13 @@ module.exports = {
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
setupFilesAfterEnv: ['jest-extended'],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node"
testEnvironment: 'node',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
@@ -152,7 +152,10 @@ module.exports = {
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This is overridden in every runCLI invocation but it's here as the default
// for vscode-jest. We only want unit tests in the test explorer (sidebar),
// since spec tests require the entire extension host to be launched before.
testRegex: ['\\.test\\.ts$'],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,

View File

@@ -1,20 +1,20 @@
{
"name": "foam-vscode",
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
"description": "Generate markdown reference lists from wikilinks in a workspace",
"displayName": "Foam",
"description": "VS Code + Markdown + Wikilinks for your note taking and knowledge base",
"private": true,
"repository": {
"url": "https://github.com/foambubble/foam",
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.15.0",
"version": "0.17.2",
"license": "MIT",
"publisher": "foam",
"engines": {
"vscode": "^1.47.1"
},
"icon": "icon/FOAM_ICON_256.png",
"icon": "assets/icon/FOAM_ICON_256.png",
"categories": [
"Other"
],
@@ -37,6 +37,26 @@
"markdown.previewStyles": [
"./static/preview/style.css"
],
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "foam.wikilink.injection",
"injectTo": [
"text.html.markdown"
]
}
],
"colors": [
{
"id": "foam.placeholder",
"description": "Color of foam placeholders.",
"defaults": {
"dark": "editorWarning.foreground",
"light": "editorWarning.foreground",
"highContrast": "editorWarning.foreground"
}
}
],
"views": {
"explorer": [
{
@@ -223,7 +243,7 @@
},
"foam.edit.linkReferenceDefinitions": {
"type": "string",
"default": "withoutExtensions",
"default": "off",
"enum": [
"withExtensions",
"withoutExtensions",
@@ -235,11 +255,6 @@
"Disable wikilink definitions generation"
]
},
"foam.links.navigation.enable": {
"description": "Enable navigation through links",
"type": "boolean",
"default": true
},
"foam.links.hover.enable": {
"description": "Enable displaying note content on hover links",
"type": "boolean",
@@ -248,7 +263,7 @@
"foam.decorations.links.enable": {
"description": "Enable decorations for links",
"type": "boolean",
"default": false
"default": true
},
"foam.openDailyNote.onStartup": {
"type": "boolean",
@@ -361,9 +376,13 @@
"build": "tsc -p ./",
"pretest": "yarn build",
"test": "node ./out/test/run-tests.js",
"lint": "tsdx lint",
"pretest:unit": "yarn build",
"test:unit": "node ./out/test/run-tests.js --unit",
"pretest:e2e": "yarn build",
"test:e2e": "node ./out/test/run-tests.js --e2e",
"lint": "tsdx lint src",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --watch",
"watch": "tsc --build ./tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
"vscode:prepublish": "yarn npm-install && yarn run build",
"npm-install": "rimraf node_modules && npm i",
@@ -376,34 +395,50 @@
},
"devDependencies": {
"@babel/core": "^7.11.0",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-typescript": "^7.10.4",
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.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",
"babel-jest": "^26.2.2",
"eslint": "^6.8.0",
"glob": "^7.1.6",
"eslint-plugin-import": "^2.24.2",
"husky": "^4.2.5",
"jest": "^26.2.2",
"jest-environment-vscode": "^1.0.0",
"jest-extended": "^0.11.5",
"markdown-it": "^12.0.4",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"typescript": "^3.8.3",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.9.5",
"vscode-test": "^1.3.0"
},
"dependencies": {
"dateformat": "^3.0.3",
"foam-core": "^0.15.0",
"detect-newline": "^3.1.0",
"fast-array-diff": "^1.0.1",
"glob": "^7.1.6",
"gray-matter": "^4.0.2",
"lodash": "^4.17.21",
"markdown-it-regex": "^0.2.0",
"micromatch": "^4.0.2",
"remove-markdown": "^0.3.0"
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",
"remark-wiki-link": "^0.0.4",
"remove-markdown": "^0.3.0",
"replace-ext": "^2.0.0",
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2",
"yaml": "^1.10.0"
}
}

View File

@@ -0,0 +1,36 @@
{
"rules": {
"no-restricted-imports": [
"error",
{
"name": "vscode",
"message": "Core submodule must not depend on VS Code."
}
]
// Ideally we would also prevent the core module from depending on other modules
// but I have been struggling to get it to work.
// For future reference, below are some configurations I think should achieve this
// (but I couldn't manage to get working).
//
// "import/no-internal-modules": [
// "error",
// {
// "allow": ["./src/core"]
// }
// ]
// "import/no-restricted-paths": [
// "error",
// {
// "zones": [
// {
// "target": "./src/core",
// "from": "./src/(!core)",
// "message": "Core module can't have outside dependencies."
// }
// ]
// }
// ]
// "import/no-relative-parent-imports": "error"
// note: https://github.com/import-js/eslint-plugin-import/issues/1610
}
}

View File

@@ -1,6 +1,6 @@
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
import { Range } from '../../src/model/range';
import { Logger } from '../../src/utils/log';
import { Range } from '../model/range';
import { Logger } from '../utils/log';
import { applyTextEdit } from './apply-text-edit';
Logger.setLevel('error');

View File

@@ -1,7 +1,7 @@
import os from 'os';
import detectNewline from 'detect-newline';
import { Position } from '../model/position';
import { TextEdit } from '../index';
import { TextEdit } from '.';
/**
*
@@ -34,5 +34,5 @@ const getOffset = (
offset = offset + lines[i].length + eolLen;
i++;
}
return offset + Math.min(position.character, lines[i].length);
return offset + Math.min(position.character, lines[i]?.length ?? 0);
};

View File

@@ -1,13 +1,12 @@
import * as path from 'path';
import { generateHeading } from '../../src/janitor';
import { createConfigFromFolders } from '../../src/config';
import { Resource } from '../../src/model/note';
import { FileDataStore, Matcher } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { FoamWorkspace } from '../../src/model/workspace';
import { URI } from '../../src/model/uri';
import { Range } from '../../src/model/range';
import { MarkdownResourceProvider, bootstrap } from '../../src';
import { generateHeading } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../markdown-provider';
import { bootstrap } from '../model/foam';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import { FoamWorkspace } from '../model/workspace';
import { FileDataStore, Matcher } from '../services/datastore';
import { Logger } from '../utils/log';
Logger.setLevel('error');
@@ -16,21 +15,13 @@ describe('generateHeadings', () => {
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => URI.getBasename(res.uri) === slug) as Resource;
.find(res => res.uri.getName() === slug) as Resource;
};
beforeAll(async () => {
const config = createConfigFromFolders([
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const mdProvider = new MarkdownResourceProvider(
new Matcher(
config.workspaceFolders,
config.includeGlobs,
config.ignoreGlobs
)
);
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
const mdProvider = new MarkdownResourceProvider(matcher);
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
_workspace = foam.workspace;
});

View File

@@ -1,41 +1,33 @@
import * as path from 'path';
import { generateLinkReferences } from '../../src/janitor';
import { createConfigFromFolders } from '../../src/config';
import { FileDataStore, Matcher } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { FoamWorkspace } from '../../src/model/workspace';
import { URI } from '../../src/model/uri';
import { Resource } from '../../src/model/note';
import { Range } from '../../src/model/range';
import { MarkdownResourceProvider, bootstrap } from '../../src';
import { generateLinkReferences } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../markdown-provider';
import { bootstrap } from '../model/foam';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import { FoamWorkspace } from '../model/workspace';
import { FileDataStore, Matcher } from '../services/datastore';
import { Logger } from '../utils/log';
Logger.setLevel('error');
describe('generateLinkReferences', () => {
let _workspace: FoamWorkspace;
// TODO slug must be reserved for actual slugs, not file names
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => URI.getBasename(res.uri) === slug) as Resource;
.find(res => res.uri.getName() === slug) as Resource;
};
beforeAll(async () => {
const config = createConfigFromFolders([
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const mdProvider = new MarkdownResourceProvider(
new Matcher(
config.workspaceFolders,
config.includeGlobs,
config.ignoreGlobs
)
);
const foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
const mdProvider = new MarkdownResourceProvider(matcher);
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
_workspace = foam.workspace;
});
it('initialised test graph correctly', () => {
expect(_workspace.list().length).toEqual(6);
expect(_workspace.list().length).toEqual(10);
});
it('should add link references to a file that does not have them', () => {
@@ -104,6 +96,54 @@ describe('generateLinkReferences', () => {
expect(actual).toEqual(expected);
});
it('should put links with spaces in angel brackets', () => {
const note = findBySlug('angel-reference');
const expected = {
newText: textForNote(
note,
`
[//begin]: # "Autogenerated link references for markdown compatibility"
[Note being refered as angel]: <Note being refered as angel> "Note being refered as angel"
[//end]: # "Autogenerated link references"`
),
range: Range.create(3, 0, 3, 0),
};
const actual = generateLinkReferences(note, _workspace, false);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
expect(actual!.newText).toEqual(expected.newText);
});
it('should not remove explicitly entered link references', () => {
const note = findBySlug('file-with-explicit-link-references');
const expected = null;
const actual = generateLinkReferences(note, _workspace, false);
expect(actual).toEqual(expected);
});
it('should not remove explicitly entered link references and have an implicit link', () => {
const note = findBySlug('file-with-explicit-and-implicit-link-references');
const expected = {
newText: textForNote(
note,
`[^footerlink]: https://foambubble.github.io/
[linkrefenrece]: https://foambubble.github.io/
[//begin]: # "Autogenerated link references for markdown compatibility"
[first-document]: first-document "First Document"
[//end]: # "Autogenerated link references"`
),
range: Range.create(5, 0, 10, 42),
};
const actual = generateLinkReferences(note, _workspace, false);
expect(actual).toEqual(expected);
});
});
/**

View File

@@ -1,4 +1,3 @@
import GithubSlugger from 'github-slugger';
import { Resource } from '../model/note';
import { Range } from '../model/range';
import {
@@ -7,13 +6,10 @@ import {
} from '../markdown-provider';
import { getHeadingFromFileName } from '../utils';
import { FoamWorkspace } from '../model/workspace';
import { uriToSlug } from '../utils/slug';
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
const slugger = new GithubSlugger();
export interface TextEdit {
range: Range;
newText: string;
@@ -59,17 +55,78 @@ export const generateLinkReferences = (
} else {
const first = note.definitions[0];
const last = note.definitions[note.definitions.length - 1];
var nonGeneratedReferenceDefinitions = note.definitions;
// if we have more definitions then referenced pages AND the page refers to a page
// we expect non-generated link definitions to be present
// Collect all non-generated definitions, by removing the generated ones
if (
note.definitions.length > markdownReferences.length &&
markdownReferences.length > 0
) {
// remove all autogenerated definitions
const beginIndex = note.definitions.findIndex(
({ label }) => label === '//begin'
);
const endIndex = note.definitions.findIndex(
({ label }) => label === '//end'
);
const generatedDefinitions = [...note.definitions].splice(
beginIndex,
endIndex - beginIndex + 1
);
nonGeneratedReferenceDefinitions = note.definitions.filter(
x => !generatedDefinitions.includes(x)
);
}
// When we only have explicitly defined link definitions &&
// no indication of previously defined generated links &&
// there is no reference to another page, return null
if (
nonGeneratedReferenceDefinitions.length > 0 &&
note.definitions.findIndex(({ label }) => label === '//begin') < 0 &&
markdownReferences.length === 0
) {
return null;
}
// Format link definitions for non-generated links
const nonGeneratedReferences = nonGeneratedReferenceDefinitions
.map(stringifyMarkdownLinkReferenceDefinition)
.join(note.source.eol);
const oldReferences = note.definitions
.map(stringifyMarkdownLinkReferenceDefinition)
.join(note.source.eol);
if (oldReferences === newReferences) {
// When the newly formatted references match the old ones, OR
// when non-generated references are present, but no new ones are generated
// return null
if (
oldReferences === newReferences ||
(nonGeneratedReferenceDefinitions.length > 0 &&
newReferences === '' &&
markdownReferences.length > 0)
) {
return null;
}
var fullReferences = `${newReferences}`;
// If there are any non-generated definitions, add those to the output as well
if (
nonGeneratedReferenceDefinitions.length > 0 &&
markdownReferences.length > 0
) {
fullReferences = `${nonGeneratedReferences}${note.source.eol}${newReferences}`;
}
return {
// @todo: do we need to ensure new lines?
newText: `${newReferences}`,
newText: `${fullReferences}`,
range: Range.createFromPosition(first.range!.start, last.range!.end),
};
}
@@ -107,7 +164,7 @@ export const generateHeading = (note: Resource): TextEdit | null => {
return {
newText: `${paddingStart}# ${getHeadingFromFileName(
uriToSlug(note.uri)
note.uri.getName()
)}${paddingEnd}`,
range: Range.createFromPosition(
note.source.contentStart,
@@ -115,14 +172,3 @@ export const generateHeading = (note: Resource): TextEdit | null => {
),
};
};
/**
*
* @param fileName
* @returns null if file name is already in kebab case otherise returns
* the kebab cased file name
*/
export const getKebabCaseFileName = (fileName: string) => {
const kebabCasedFileName = slugger.slug(fileName);
return kebabCasedFileName === fileName ? null : kebabCasedFileName;
};

View File

@@ -2,14 +2,13 @@ import {
createMarkdownParser,
createMarkdownReferences,
ParserPlugin,
} from '../src/markdown-provider';
import { DirectLink, WikiLink } from '../src/model/note';
import { Logger } from '../src/utils/log';
import { uriToSlug } from '../src/utils/slug';
import { URI } from '../src/model/uri';
import { FoamGraph } from '../src/model/graph';
import { createTestWorkspace } from './core.test';
import { Range } from '../src/model/range';
} from './markdown-provider';
import { DirectLink, WikiLink } from './model/note';
import { Logger } from './utils/log';
import { URI } from './model/uri';
import { FoamGraph } from './model/graph';
import { Range } from './model/range';
import { createTestWorkspace, getRandomURI } from '../test/test-utils';
Logger.setLevel('error');
@@ -40,50 +39,45 @@ const pageE = `
# Page E
`;
const createNoteFromMarkdown = (path: string, content: string) =>
createMarkdownParser([]).parse(URI.file(path), content);
const createNoteFromMarkdown = (content: string, path?: string) =>
createMarkdownParser([]).parse(
path ? URI.file(path) : getRandomURI(),
content
);
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
const workspace = createTestWorkspace();
workspace.set(createNoteFromMarkdown('/page-a.md', pageA));
workspace.set(createNoteFromMarkdown('/page-b.md', pageB));
workspace.set(createNoteFromMarkdown('/page-c.md', pageC));
workspace.set(createNoteFromMarkdown('/page-d.md', pageD));
workspace.set(createNoteFromMarkdown('/page-e.md', pageE));
workspace.set(createNoteFromMarkdown(pageA, '/page-a.md'));
workspace.set(createNoteFromMarkdown(pageB, '/page-b.md'));
workspace.set(createNoteFromMarkdown(pageC, '/page-c.md'));
workspace.set(createNoteFromMarkdown(pageD, '/page-d.md'));
workspace.set(createNoteFromMarkdown(pageE, '/page-e.md'));
expect(
workspace
.list()
.map(n => n.uri)
.map(uriToSlug)
.map(n => n.uri.getName())
.sort()
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
});
it('Ingores external links', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
`
this is a [link to google](https://www.google.com)
`
`this is a [link to google](https://www.google.com)`
);
expect(note.links.length).toEqual(0);
});
it('Ignores references to sections in the same file', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
`
this is a [link to intro](#introduction)
`
`this is a [link to intro](#introduction)`
);
expect(note.links.length).toEqual(0);
});
it('Parses internal links correctly', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
'this is a [link to page b](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
@@ -95,7 +89,6 @@ this is a [link to intro](#introduction)
it('Parses links that have formatting in label', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
'this is [**link** with __formatting__](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
@@ -107,11 +100,11 @@ this is a [link to intro](#introduction)
it('Parses wikilinks correctly', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('/page-a.md', pageA);
const noteB = createNoteFromMarkdown('/page-b.md', pageB);
const noteC = createNoteFromMarkdown('/page-c.md', pageC);
const noteD = createNoteFromMarkdown('/Page D.md', pageD);
const noteE = createNoteFromMarkdown('/page e.md', pageE);
const noteA = createNoteFromMarkdown(pageA, '/page-a.md');
const noteB = createNoteFromMarkdown(pageB, '/page-b.md');
const noteC = createNoteFromMarkdown(pageC, '/page-c.md');
const noteD = createNoteFromMarkdown(pageD, '/Page D.md');
const noteE = createNoteFromMarkdown(pageE, '/page e.md');
workspace
.set(noteA)
@@ -134,7 +127,6 @@ this is a [link to intro](#introduction)
it('Parses backlinks with an alias', () => {
const note = createNoteFromMarkdown(
'/path/to/page-a.md',
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
);
expect(note.links.length).toEqual(2);
@@ -151,9 +143,7 @@ this is a [link to intro](#introduction)
});
it('Skips wikilinks in codeblocks', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
\`\`\`
@@ -161,8 +151,7 @@ this is inside a [[codeblock]]
\`\`\`
this is some text with our [[second-wikilink]].
`
);
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
@@ -170,16 +159,13 @@ this is some text with our [[second-wikilink]].
});
it('Skips wikilinks in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
this is \`inside a [[codeblock]]\`
this is some text with our [[second-wikilink]].
`
);
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
@@ -189,71 +175,71 @@ this is some text with our [[second-wikilink]].
describe('Note Title', () => {
it('should initialize note title if heading exists', () => {
const note = createNoteFromMarkdown(
'/page-a.md',
`
const note = createNoteFromMarkdown(`
# Page A
this note has a title
`
);
`);
expect(note.title).toBe('Page A');
});
it('should support wikilinks and urls in title', () => {
const note = createNoteFromMarkdown(`
# Page A with [[wikilink]] and a [url](https://google.com)
this note has a title
`);
expect(note.title).toBe('Page A with wikilink and a url');
});
it('should default to file name if heading does not exist', () => {
const note = createNoteFromMarkdown(
'/page-d.md',
`
This file has no heading.
`
`This file has no heading.`,
'/page-d.md'
);
expect(note.title).toEqual('page-d');
});
it('should give precedence to frontmatter title over other headings', () => {
const note = createNoteFromMarkdown(
'/page-e.md',
`
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title
`
);
`);
expect(note.title).toBe('Note Title');
});
it('should support numbers', () => {
const note1 = createNoteFromMarkdown('/157.md', `hello`);
const note1 = createNoteFromMarkdown(`hello`, '/157.md');
expect(note1.title).toBe('157');
const note2 = createNoteFromMarkdown('/157.md', `# 158`);
const note2 = createNoteFromMarkdown(`# 158`, '/157.md');
expect(note2.title).toBe('158');
const note3 = createNoteFromMarkdown(
'/157.md',
`
---
title: 159
---
# 158
`
`,
'/157.md'
);
expect(note3.title).toBe('159');
});
it('should not break on empty titles (see #276)', () => {
const note = createNoteFromMarkdown(
'/Hello Page.md',
`
#
this note has an empty title line
`
`,
'/Hello Page.md'
);
expect(note.title).toEqual('Hello Page');
});
@@ -261,47 +247,38 @@ this note has an empty title line
describe('frontmatter', () => {
it('should parse yaml frontmatter', () => {
const note = createNoteFromMarkdown(
'/page-e.md',
`
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title`
);
# Other Note Title`);
expect(note.properties.title).toBe('Note Title');
expect(note.properties.date).toBe('20-12-12');
});
it('should parse empty frontmatter', () => {
const note = createNoteFromMarkdown(
'/page-f.md',
`
const note = createNoteFromMarkdown(`
---
---
# Empty Frontmatter
`
);
`);
expect(note.properties).toEqual({});
});
it('should not fail when there are issues with parsing frontmatter', () => {
const note = createNoteFromMarkdown(
'/page-f.md',
`
const note = createNoteFromMarkdown(`
---
title: - one
- two
- #
---
`
);
`);
expect(note.properties).toEqual({});
});
@@ -310,11 +287,11 @@ title: - one
describe('wikilinks definitions', () => {
it('can generate links without file extension when includeExtension = false', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
const noExtRefs = createMarkdownReferences(workspace, noteA.uri, false);
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
@@ -322,11 +299,11 @@ describe('wikilinks definitions', () => {
it('can generate links with file extension when includeExtension = true', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
@@ -334,11 +311,11 @@ describe('wikilinks definitions', () => {
it('use relative paths', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown('/dir2/page-b.md', pageB))
.set(createNoteFromMarkdown('/dir3/page-c.md', pageC));
.set(createNoteFromMarkdown(pageB, '/dir2/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir3/page-c.md'));
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual([
@@ -350,13 +327,10 @@ describe('wikilinks definitions', () => {
describe('tags plugin', () => {
it('can find tags in the text of the note', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
# this is a #heading
#this is some #text that includes #tags we #care-about.
`
);
`);
expect(noteA.tags).toEqual([
{ label: 'heading', range: Range.create(1, 12, 1, 20) },
{ label: 'this', range: Range.create(2, 0, 2, 5) },
@@ -367,16 +341,13 @@ describe('tags plugin', () => {
});
it('will skip tags in codeblocks', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
\`\`\`
this is a #codeblock
\`\`\`
`
);
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
@@ -385,13 +356,9 @@ this is a #codeblock
});
it('will skip tags in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
this is a \`inlined #codeblock\`
`
);
this is a \`inlined #codeblock\` `);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
@@ -399,16 +366,13 @@ this is a \`inlined #codeblock\`
]);
});
it('can find tags as text in yaml', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
---
tags: hello, world this_is_good
---
# this is a heading
this is some #text that includes #tags we #care-about.
`
);
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
@@ -420,16 +384,13 @@ this is some #text that includes #tags we #care-about.
});
it('can find tags as array in yaml', () => {
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some #text that includes #tags we #care-about.
`
);
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
@@ -444,16 +405,13 @@ this is some #text that includes #tags we #care-about.
// For now it's enough to just get the YAML block range
// in the future we might want to be more specific
const noteA = createNoteFromMarkdown(
'/dir1/page-a.md',
`
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some text
`
);
`);
expect(noteA.tags[0]).toEqual({
label: 'hello',
range: Range.create(1, 0, 3, 3),
@@ -461,6 +419,45 @@ this is some text
});
});
describe('Sections plugin', () => {
it('should find sections within the note', () => {
const note = createNoteFromMarkdown(`
# Section 1
This is the content of section 1.
## Section 1.1
This is the content of section 1.1.
# Section 2
This is the content of section 2.
`);
expect(note.sections).toHaveLength(3);
expect(note.sections[0].label).toEqual('Section 1');
expect(note.sections[0].range).toEqual(Range.create(1, 0, 9, 0));
expect(note.sections[1].label).toEqual('Section 1.1');
expect(note.sections[1].range).toEqual(Range.create(5, 0, 9, 0));
expect(note.sections[2].label).toEqual('Section 2');
expect(note.sections[2].range).toEqual(Range.create(9, 0, 13, 0));
});
it('should support wikilinks and links in the section label', () => {
const note = createNoteFromMarkdown(`
# Section with [[wikilink]]
This is the content of section with wikilink
## Section with [url](https://google.com)
This is the content of section with url`);
expect(note.sections).toHaveLength(2);
expect(note.sections[0].label).toEqual('Section with wikilink');
expect(note.sections[1].label).toEqual('Section with url');
});
});
describe('parser plugins', () => {
const testPlugin: ParserPlugin = {
visit: (node, note) => {

View File

@@ -17,19 +17,13 @@ import {
} from './model/note';
import { Position } from './model/position';
import { Range } from './model/range';
import {
dropExtension,
extractHashtags,
extractTagsFromProp,
isNone,
isSome,
} from './utils';
import { extractHashtags, extractTagsFromProp, isNone, isSome } from './utils';
import { Logger } from './utils/log';
import { URI } from './model/uri';
import { FoamWorkspace } from './model/workspace';
import { ResourceProvider } from 'model/provider';
import { IDataStore, FileDataStore, IMatcher } from './services/datastore';
import { IDisposable } from 'common/lifecycle';
import { IDisposable } from './common/lifecycle';
import { ResourceProvider } from './model/provider';
const ALIAS_DIVIDER_CHAR = '|';
@@ -69,7 +63,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
await Promise.all(
files.map(async uri => {
Logger.info('Found: ' + URI.toString(uri));
Logger.info('Found: ' + uri.toString());
const content = await this.dataStore.read(uri);
if (isSome(content)) {
workspace.set(this.parser.parse(uri, content));
@@ -100,15 +94,26 @@ export class MarkdownResourceProvider implements ResourceProvider {
}
supports(uri: URI) {
return URI.isMarkdownFile(uri);
return uri.isMarkdown();
}
read(uri: URI): Promise<string | null> {
return this.dataStore.read(uri);
}
readAsMarkdown(uri: URI): Promise<string | null> {
return this.dataStore.read(uri);
async readAsMarkdown(uri: URI): Promise<string | null> {
let content = await this.dataStore.read(uri);
if (isSome(content) && uri.fragment) {
const resource = this.parser.parse(uri, content);
const section = Resource.findSection(resource, uri.fragment);
if (isSome(section)) {
const rows = content.split('\n');
content = rows
.slice(section.range.start.line, section.range.end.line)
.join('\n');
}
}
return content;
}
async fetch(uri: URI) {
@@ -128,21 +133,32 @@ export class MarkdownResourceProvider implements ResourceProvider {
def => def.label === link.target
)?.url;
if (isSome(definitionUri)) {
const definedUri = URI.resolve(definitionUri, resource.uri);
const definedUri = resource.uri.resolve(definitionUri);
targetUri =
workspace.find(definedUri, resource.uri)?.uri ??
URI.placeholder(definedUri.path);
} else {
const [target, section] = link.target.split('#');
targetUri =
workspace.find(link.target, resource.uri)?.uri ??
URI.placeholder(link.target);
target === ''
? resource.uri
: workspace.find(target, resource.uri)?.uri ??
URI.placeholder(link.target);
if (section) {
targetUri = targetUri.withFragment(section);
}
}
break;
case 'link':
const [target, section] = link.target.split('#');
targetUri =
workspace.find(link.target, resource.uri)?.uri ??
URI.placeholder(URI.resolve(link.target, resource.uri).path);
workspace.find(target, resource.uri)?.uri ??
URI.placeholder(resource.uri.resolve(link.target).path);
if (section && !targetUri.isPlaceholder()) {
targetUri = targetUri.withFragment(section);
}
break;
}
return targetUri;
@@ -161,9 +177,9 @@ export class MarkdownResourceProvider implements ResourceProvider {
*/
const getTextFromChildren = (root: Node): string => {
let text = '';
visit(root, 'text', node => {
if (node.type === 'text') {
text = text + node.value;
visit(root, node => {
if (node.type === 'text' || node.type === 'wikiLink') {
text = text + ((node as any).value || '');
}
});
return text;
@@ -201,12 +217,63 @@ const tagsPlugin: ParserPlugin = {
},
};
let sectionStack: Array<{ label: string; level: number; start: Position }> = [];
const sectionsPlugin: ParserPlugin = {
name: 'section',
onWillVisitTree: () => {
sectionStack = [];
},
visit: (node, note) => {
if (node.type === 'heading') {
const level = (node as any).depth;
const label = getTextFromChildren(node);
if (!label || !level) {
return;
}
const start = astPositionToFoamRange(node.position!).start;
// Close all the sections that are not parents of the current section
while (
sectionStack.length > 0 &&
sectionStack[sectionStack.length - 1].level >= level
) {
const section = sectionStack.pop();
note.sections.push({
label: section.label,
range: Range.createFromPosition(section.start, start),
});
}
// Add the new section to the stack
sectionStack.push({ label, level, start });
}
},
onDidVisitTree: (tree, note) => {
const end = Position.create(note.source.end.line + 1, 0);
// Close all the remainig sections
while (sectionStack.length > 0) {
const section = sectionStack.pop();
note.sections.push({
label: section.label,
range: { start: section.start, end },
});
}
note.sections.sort((a, b) =>
Position.compareTo(a.range.start, b.range.start)
);
},
};
const titlePlugin: ParserPlugin = {
name: 'title',
visit: (node, note) => {
if (note.title === '' && node.type === 'heading' && node.depth === 1) {
note.title =
((node as Parent)!.children?.[0]?.value as string) || note.title;
if (
note.title === '' &&
node.type === 'heading' &&
(node as any).depth === 1
) {
const title = getTextFromChildren(node);
note.title = title.length > 0 ? title : note.title;
}
},
onDidFindProperties: (props, note) => {
@@ -215,7 +282,7 @@ const titlePlugin: ParserPlugin = {
},
onDidVisitTree: (tree, note) => {
if (note.title === '') {
note.title = URI.getBasename(note.uri);
note.title = note.uri.getName();
}
},
};
@@ -224,7 +291,7 @@ const wikilinkPlugin: ParserPlugin = {
name: 'wikilink',
visit: (node, note, noteSource) => {
if (node.type === 'wikiLink') {
const text = node.value as string;
const text = (node as any).value;
const alias = node.data?.alias as string;
const literalContent = noteSource.substring(
node.position!.start.offset!,
@@ -250,7 +317,7 @@ const wikilinkPlugin: ParserPlugin = {
}
if (node.type === 'link') {
const targetUri = (node as any).url;
const uri = URI.resolve(targetUri, note.uri);
const uri = note.uri.resolve(targetUri);
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
return;
}
@@ -270,9 +337,9 @@ const definitionsPlugin: ParserPlugin = {
visit: (node, note) => {
if (node.type === 'definition') {
note.definitions.push({
label: node.label as string,
url: node.url as string,
title: node.title as string,
label: (node as any).label,
url: (node as any).url,
title: (node as any).title,
range: astPositionToFoamRange(node.position!),
});
}
@@ -291,7 +358,7 @@ const handleError = (
const name = plugin.name || '';
Logger.warn(
`Error while executing [${fnName}] in plugin [${name}]. ${
uri ? 'for file [' + URI.toString(uri) : ']'
uri ? 'for file [' + uri.toString() : ']'
}.`,
e
);
@@ -310,6 +377,7 @@ export function createMarkdownParser(
wikilinkPlugin,
definitionsPlugin,
tagsPlugin,
sectionsPlugin,
...extraPlugins,
];
@@ -323,7 +391,7 @@ export function createMarkdownParser(
const foamParser: ResourceParser = {
parse: (uri: URI, markdown: string): Resource => {
Logger.debug('Parsing:', URI.toString(uri));
Logger.debug('Parsing:', uri.toString());
markdown = plugins.reduce((acc, plugin) => {
try {
return plugin.onWillParseMarkdown?.(acc) || acc;
@@ -340,6 +408,7 @@ export function createMarkdownParser(
type: 'note',
properties: {},
title: '',
sections: [],
tags: [],
links: [],
definitions: [],
@@ -361,7 +430,7 @@ export function createMarkdownParser(
visit(tree, node => {
if (node.type === 'yaml') {
try {
const yamlProperties = parseYAML(node.value as string) ?? {};
const yamlProperties = parseYAML((node as any).value) ?? {};
note.properties = {
...note.properties,
...yamlProperties,
@@ -380,10 +449,7 @@ export function createMarkdownParser(
}
}
} catch (e) {
Logger.warn(
`Error while parsing YAML for [${URI.toString(uri)}]`,
e
);
Logger.warn(`Error while parsing YAML for [${uri.toString()}]`, e);
}
}
@@ -437,7 +503,9 @@ function getFoamDefinitions(
export function stringifyMarkdownLinkReferenceDefinition(
definition: NoteLinkDefinition
) {
let text = `[${definition.label}]: ${definition.url}`;
let url =
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
let text = `[${definition.label}]: ${url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
@@ -453,9 +521,8 @@ export function createMarkdownReferences(
// Should never occur since we're already in a file,
if (source?.type !== 'note') {
console.warn(
`Note ${URI.toString(
noteUri
)} note found in workspace when attempting to generate markdown reference list`
`Note ${noteUri.toString()} note found in workspace when attempting \
to generate markdown reference list`
);
return [];
}
@@ -467,9 +534,7 @@ export function createMarkdownReferences(
const target = workspace.find(targetUri);
if (isNone(target)) {
Logger.warn(
`Link ${URI.toString(targetUri)} in ${URI.toString(
noteUri
)} is not valid.`
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
);
return null;
}
@@ -478,10 +543,10 @@ export function createMarkdownReferences(
return null;
}
const relativePath = URI.relativePath(noteUri, target.uri);
const pathToNote = includeExtension
? relativePath
: dropExtension(relativePath);
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
if (!includeExtension) {
relativeUri = relativeUri.changeExtension('*', '');
}
// [wikilink-text]: path/to/file.md "Page title"
return {
@@ -489,7 +554,7 @@ export function createMarkdownReferences(
link.rawText.indexOf('[[') > -1
? link.rawText.substring(2, link.rawText.length - 2)
: link.rawText || link.label,
url: pathToNote,
url: relativeUri.path,
title: target.title,
};
})

View File

@@ -1,6 +1,5 @@
import { IDisposable } from '../common/lifecycle';
import { IDataStore, IMatcher, Matcher } from '../services/datastore';
import { FoamConfig } from '../config';
import { IDataStore, IMatcher } from '../services/datastore';
import { FoamWorkspace } from './workspace';
import { FoamGraph } from './graph';
import { ResourceParser } from './note';
@@ -18,21 +17,15 @@ export interface Foam extends IDisposable {
services: Services;
workspace: FoamWorkspace;
graph: FoamGraph;
config: FoamConfig;
tags: FoamTags;
}
export const bootstrap = async (
config: FoamConfig,
matcher: IMatcher,
dataStore: IDataStore,
initialProviders: ResourceProvider[]
) => {
const parser = createMarkdownParser([]);
const matcher = new Matcher(
config.workspaceFolders,
config.includeGlobs,
config.ignoreGlobs
);
const workspace = new FoamWorkspace();
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
@@ -43,7 +36,6 @@ export const bootstrap = async (
workspace,
graph,
tags,
config,
services: {
dataStore,
parser,

View File

@@ -2,9 +2,9 @@ import { diff } from 'fast-array-diff';
import { isEqual } from 'lodash';
import { Resource, ResourceLink } from './note';
import { URI } from './uri';
import { IDisposable } from '../index';
import { FoamWorkspace, uriToResourceName } from './workspace';
import { FoamWorkspace } from './workspace';
import { Range } from './range';
import { IDisposable } from '../common/lifecycle';
export type Connection = {
source: URI;
@@ -99,16 +99,19 @@ export class FoamGraph implements IDisposable {
private updateLinksRelatedToAddedResource(resource: Resource) {
// check if any existing connection can be filled by new resource
const name = uriToResourceName(resource.uri);
const placeholder = this.placeholders.get(name);
if (placeholder) {
this.placeholders.delete(name);
const resourcesToUpdate = this.backlinks.get(placeholder.path) ?? [];
resourcesToUpdate.forEach(res =>
this.resolveResource(this.workspace.get(res.source))
);
let resourcesToUpdate: URI[] = [];
for (const placeholderId of this.placeholders.keys()) {
// quick and dirty check for affected resources
if (resource.uri.path.endsWith(placeholderId + '.md')) {
resourcesToUpdate.push(
...this.backlinks.get(placeholderId).map(c => c.source)
);
// resourcesToUpdate.push(resource);
}
}
resourcesToUpdate.forEach(res =>
this.resolveResource(this.workspace.get(res))
);
// resolve the resource
this.resolveResource(resource);
}
@@ -170,7 +173,7 @@ export class FoamGraph implements IDisposable {
this.backlinks.get(target.path)?.push(connection);
if (URI.isPlaceholder(target)) {
if (target.isPlaceholder()) {
this.placeholders.set(uriToPlaceholderId(target), target);
}
return this;
@@ -190,7 +193,7 @@ export class FoamGraph implements IDisposable {
const connectionsToKeep =
link === true
? (c: Connection) =>
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
!source.isEqual(c.source) || !target.isEqual(c.target)
: (c: Connection) => !isSameConnection({ source, target, link }, c);
this.links.set(
@@ -206,7 +209,7 @@ export class FoamGraph implements IDisposable {
);
if (this.backlinks.get(target.path)?.length === 0) {
this.backlinks.delete(target.path);
if (URI.isPlaceholder(target)) {
if (target.isPlaceholder()) {
this.placeholders.delete(uriToPlaceholderId(target));
}
}
@@ -232,8 +235,8 @@ export class FoamGraph implements IDisposable {
// TODO move these utility fns to appropriate places
const isSameConnection = (a: Connection, b: Connection) =>
URI.isEqual(a.source, b.source) &&
URI.isEqual(a.target, b.target) &&
a.source.isEqual(b.source) &&
a.target.isEqual(b.target) &&
isSameLink(a.link, b.link);
const isSameLink = (a: ResourceLink, b: ResourceLink) =>

View File

@@ -38,12 +38,17 @@ export interface Tag {
range: Range;
}
export interface Section {
label: string;
range: Range;
}
export interface Resource {
uri: URI;
type: string;
title: string;
properties: any;
// sections: NoteSection[]
sections: Section[];
tags: Tag[];
links: ResourceLink[];
@@ -66,7 +71,7 @@ export abstract class Resource {
return false;
}
return (
URI.isUri((thing as Resource).uri) &&
(thing as Resource).uri instanceof URI &&
typeof (thing as Resource).title === 'string' &&
typeof (thing as Resource).type === 'string' &&
typeof (thing as Resource).properties === 'object' &&
@@ -74,4 +79,11 @@ export abstract class Resource {
typeof (thing as Resource).links === 'object'
);
}
public static findSection(resource: Resource, label: string): Section | null {
if (label) {
return resource.sections.find(s => s.label === label) ?? null;
}
return null;
}
}

View File

@@ -1,6 +1,6 @@
import { IDisposable } from 'common/lifecycle';
import { ResourceLink, URI } from 'index';
import { Resource } from './note';
import { IDisposable } from '../common/lifecycle';
import { Resource, ResourceLink } from './note';
import { URI } from './uri';
import { FoamWorkspace } from './workspace';
export interface ResourceProvider extends IDisposable {

View File

@@ -1,5 +1,5 @@
import { FoamTags } from '../src/model/tags';
import { createTestNote, createTestWorkspace } from './core.test';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
import { FoamTags } from './tags';
describe('FoamTags', () => {
it('Collects tags from a list of resources', () => {

View File

@@ -1,7 +1,7 @@
import { FoamWorkspace } from './workspace';
import { URI } from './uri';
import { IDisposable } from '../index';
import { Resource } from './note';
import { IDisposable } from '../common/lifecycle';
export class FoamTags implements IDisposable {
public readonly tags: Map<string, URI[]> = new Map();
@@ -68,7 +68,7 @@ export class FoamTags implements IDisposable {
if (this.tags.has(tag)) {
const remainingLocations = this.tags
.get(tag)
?.filter(uri => !URI.isEqual(uri, resource.uri));
?.filter(uri => !uri.isEqual(resource.uri));
if (remainingLocations && remainingLocations.length > 0) {
this.tags.set(tag, remainingLocations);

View File

@@ -0,0 +1,75 @@
import { Logger } from '../utils/log';
import { URI } from './uri';
Logger.setLevel('error');
describe('Foam URI', () => {
describe('URI parsing', () => {
const base = URI.file('/path/to/file.md');
test.each([
['https://www.google.com', URI.parse('https://www.google.com')],
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
['../relative/file.md', URI.file('/path/relative/file.md')],
['#section', base.withFragment('section')],
[
'../relative/file.md#section',
URI.parse('file:/path/relative/file.md#section'),
],
])('URI Parsing (%s)', (input, exp) => {
const result = base.resolve(input);
expect(result.scheme).toEqual(exp.scheme);
expect(result.authority).toEqual(exp.authority);
expect(result.path).toEqual(exp.path);
expect(result.query).toEqual(exp.query);
expect(result.fragment).toEqual(exp.fragment);
});
it('normalizes the Windows drive letter to upper case', () => {
const upperCase = URI.parse('file:///C:/this/is/a/Path');
const lowerCase = URI.parse('file:///c:/this/is/a/Path');
expect(upperCase.path).toEqual('/C:/this/is/a/Path');
expect(lowerCase.path).toEqual('/C:/this/is/a/Path');
expect(upperCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
expect(lowerCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
});
it('consistently parses file paths', () => {
const win1 = URI.file('c:\\this\\is\\a\\path');
const win2 = URI.parse('c:\\this\\is\\a\\path');
expect(win1).toEqual(win2);
const unix1 = URI.file('/this/is/a/path');
const unix2 = URI.parse('/this/is/a/path');
expect(unix1).toEqual(unix2);
});
it('correctly parses file paths', () => {
const winUri = URI.file('c:\\this\\is\\a\\path');
const unixUri = URI.file('/this/is/a/path');
expect(winUri).toEqual(
new URI({
scheme: 'file',
path: '/C:/this/is/a/path',
})
);
expect(unixUri).toEqual(
new URI({
scheme: 'file',
path: '/this/is/a/path',
})
);
});
});
it('supports computing relative paths', () => {
expect(URI.file('/my/file.md').resolve('../hello.md')).toEqual(
URI.file('/hello.md')
);
expect(URI.file('/my/file.md').resolve('../hello')).toEqual(
URI.file('/hello.md')
);
expect(URI.file('/my/file.markdown').resolve('../hello')).toEqual(
URI.file('/hello.markdown')
);
});
});

View File

@@ -4,9 +4,8 @@
// Some code in this file comes from https://github.com/microsoft/vscode/main/src/vs/base/common/uri.ts
// See LICENSE for details
import * as paths from 'path';
import { CharCode } from '../common/charCode';
import { isWindows } from '../common/platform';
import * as pathUtils from '../utils/path';
/**
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
@@ -24,240 +23,134 @@ import { isWindows } from '../common/platform';
* urn:example:animal:ferret:nose
* ```
*/
export interface URI {
scheme: string;
authority: string;
path: string;
query: string;
fragment: string;
}
const { posix } = paths;
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
export abstract class URI {
static create(from: Partial<URI>): URI {
return {
scheme: from.scheme ?? _empty,
authority: from.authority ?? _empty,
path: from.path ?? _empty,
query: from.query ?? _empty,
fragment: from.fragment ?? _empty,
};
export class URI {
readonly scheme: string;
readonly authority: string;
readonly path: string;
readonly query: string;
readonly fragment: string;
constructor(from: Partial<URI> = {}) {
this.scheme = from.scheme ?? _empty;
this.authority = from.authority ?? _empty;
this.path = from.path ?? _empty; // We assume the path is already posix
this.query = from.query ?? _empty;
this.fragment = from.fragment ?? _empty;
}
static parse(value: string): URI {
const match = _regexp.exec(value);
if (!match) {
return URI.create({});
return new URI();
}
return URI.create({
return new URI({
scheme: match[2] || 'file',
authority: percentDecode(match[4] ?? _empty),
path: percentDecode(match[5] ?? _empty),
path: pathUtils.fromFsPath(percentDecode(match[5] ?? _empty))[0],
query: percentDecode(match[7] ?? _empty),
fragment: percentDecode(match[9] ?? _empty),
});
}
/**
* Parses a URI from value, taking into consideration possible relative paths.
*
* @param reference the URI to use as reference in case value is a relative path
* @param value the value to parse for a URI
* @returns the URI from the given value. In case of a relative path, the URI will take into account
* the reference from which it is computed
*/
static resolve(value: string, reference: URI): URI {
let uri = URI.parse(value);
if (uri.scheme === 'file' && !value.startsWith('/')) {
const [path, fragment] = value.split('#');
uri =
path.length > 0 ? URI.computeRelativeURI(reference, path) : reference;
if (fragment) {
uri = URI.create({
...uri,
fragment: fragment,
});
static file(value: string): URI {
let [path, authority] = pathUtils.fromFsPath(value);
return new URI({ scheme: 'file', authority, path });
}
static placeholder(path: string): URI {
return new URI({ scheme: 'placeholder', path: path });
}
resolve(value: string | URI, isDirectory = false): URI {
const uri = value instanceof URI ? value : URI.parse(value);
if (!uri.isAbsolute()) {
if (uri.scheme === 'file' || uri.scheme === 'placeholder') {
let newUri = this.withFragment(uri.fragment);
if (uri.path) {
newUri = (isDirectory ? newUri : newUri.getDirectory())
.joinPath(uri.path)
.changeExtension('', this.getExtension());
}
return newUri;
}
}
return uri;
}
static computeRelativeURI(reference: URI, relativeSlug: string): URI {
// if no extension is provided, use the same extension as the source file
const slug =
posix.extname(relativeSlug) !== ''
? relativeSlug
: `${relativeSlug}${posix.extname(reference.path)}`;
return URI.create({
...reference,
path: posix.join(posix.dirname(reference.path), slug),
});
isAbsolute(): boolean {
return pathUtils.isAbsolute(this.path);
}
static file(path: string): URI {
let authority = _empty;
// normalize to fwd-slashes on windows,
// on other systems bwd-slashes are valid
// filename character, eg /f\oo/ba\r.txt
if (isWindows) {
if (path.startsWith(_slash)) {
path = `${path.replace(/\\/g, _slash)}`;
} else {
path = `/${path.replace(/\\/g, _slash)}`;
}
}
// check for authority as used in UNC shares
// or use the path as given
if (path[0] === _slash && path[1] === _slash) {
const idx = path.indexOf(_slash, 2);
if (idx === -1) {
authority = path.substring(2);
path = _slash;
} else {
authority = path.substring(2, idx);
path = path.substring(idx) || _slash;
}
}
return URI.create({ scheme: 'file', authority, path });
getDirectory(): URI {
const path = pathUtils.getDirectory(this.path);
return new URI({ ...this, path });
}
static placeholder(key: string): URI {
return URI.create({
scheme: 'placeholder',
path: key,
});
getBasename(): string {
return pathUtils.getBasename(this.path);
}
static relativePath(source: URI, target: URI): string {
const relativePath = posix.relative(
posix.dirname(source.path),
target.path
getName(): string {
return pathUtils.getName(this.path);
}
getExtension(): string {
return pathUtils.getExtension(this.path);
}
changeExtension(from: string, to: string): URI {
const path = pathUtils.changeExtension(this.path, from, to);
return new URI({ ...this, path });
}
joinPath(...paths: string[]) {
const path = pathUtils.joinPath(this.path, ...paths);
return new URI({ ...this, path });
}
relativeTo(uri: URI) {
const path = pathUtils.relativeTo(this.path, uri.path);
return new URI({ ...this, path });
}
withFragment(fragment: string): URI {
return new URI({ ...this, fragment });
}
isPlaceholder(): boolean {
return this.scheme === 'placeholder';
}
toFsPath() {
return pathUtils.toFsPath(
this.path,
this.scheme === 'file' ? this.authority : ''
);
return relativePath;
}
static getBasename(uri: URI) {
return posix.parse(uri.path).name;
toString(): string {
return encode(this, false);
}
static getDir(uri: URI) {
return URI.file(posix.dirname(uri.path));
isMarkdown(): boolean {
const ext = this.getExtension();
return ext === '.md' || ext === '.markdown';
}
static getFileNameWithoutExtension(uri: URI) {
return URI.getBasename(uri).replace(/\.[^.]+$/, '');
}
/**
* Uses a placeholder URI, and a reference directory, to generate
* the URI of the corresponding resource
*
* @param placeholderUri the placeholder URI
* @param basedir the dir to be used as reference
* @returns the target resource URI
*/
static createResourceUriFromPlaceholder(
basedir: URI,
placeholderUri: URI
): URI {
const tokens = placeholderUri.path.split('/');
const path = tokens.slice(0, -1);
const filename = tokens.slice(-1);
return URI.joinPath(basedir, ...path, `${filename}.md`);
}
/**
* Join a URI path with path fragments and normalizes the resulting path.
*
* @param uri The input URI.
* @param pathFragment The path fragment to add to the URI path.
* @returns The resulting URI.
*/
static joinPath(uri: URI, ...pathFragment: string[]): URI {
if (!uri.path) {
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
}
let newPath: string;
if (isWindows && uri.scheme === 'file') {
newPath = URI.file(paths.win32.join(URI.toFsPath(uri), ...pathFragment))
.path;
} else {
newPath = paths.posix.join(uri.path, ...pathFragment);
}
return URI.create({ ...uri, path: newPath });
}
static toFsPath(uri: URI, keepDriveLetterCasing = true): string {
let value: string;
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
// unc path: file://shares/c$/far/boo
value = `//${uri.authority}${uri.path}`;
} else if (
uri.path.charCodeAt(0) === CharCode.Slash &&
((uri.path.charCodeAt(1) >= CharCode.A &&
uri.path.charCodeAt(1) <= CharCode.Z) ||
(uri.path.charCodeAt(1) >= CharCode.a &&
uri.path.charCodeAt(1) <= CharCode.z)) &&
uri.path.charCodeAt(2) === CharCode.Colon
) {
if (!keepDriveLetterCasing) {
// windows drive letter: file:///c:/far/boo
value = uri.path[1].toLowerCase() + uri.path.substr(2);
} else {
value = uri.path.substr(1);
}
} else {
// other path
value = uri.path;
}
if (isWindows) {
value = value.replace(/\//g, '\\');
}
return value;
}
static toString(uri: URI): string {
return encode(uri, false);
}
// --- utility
static isUri(thing: any): thing is URI {
if (!thing) {
return false;
}
isEqual(uri: URI): boolean {
return (
typeof (thing as URI).authority === 'string' &&
typeof (thing as URI).fragment === 'string' &&
typeof (thing as URI).path === 'string' &&
typeof (thing as URI).query === 'string' &&
typeof (thing as URI).scheme === 'string'
this.authority === uri.authority &&
this.scheme === uri.scheme &&
this.path === uri.path &&
this.fragment === uri.fragment &&
this.query === uri.query
);
}
static isPlaceholder(uri: URI): boolean {
return uri.scheme === 'placeholder';
}
static isEqual(a: URI, b: URI): boolean {
return (
a.authority === b.authority &&
a.scheme === b.scheme &&
a.path === b.path &&
a.fragment === b.fragment &&
a.query === b.query
);
}
static isMarkdownFile(uri: URI): boolean {
return uri.path.endsWith('.md');
}
}
// --- encode / decode
@@ -331,20 +224,20 @@ function encode(uri: URI, skipEncoding: boolean): string {
}
}
if (path) {
// lower-case windows drive letters in /C:/fff or C:/fff
// upper-case windows drive letters in /c:/fff or c:/fff
if (
path.length >= 3 &&
path.charCodeAt(0) === CharCode.Slash &&
path.charCodeAt(2) === CharCode.Colon
) {
const code = path.charCodeAt(1);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
if (code >= CharCode.a && code <= CharCode.z) {
path = `/${String.fromCharCode(code - 32)}:${path.substr(3)}`; // "/C:".length === 3
}
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
const code = path.charCodeAt(0);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
if (code >= CharCode.a && code <= CharCode.z) {
path = `${String.fromCharCode(code - 32)}:${path.substr(2)}`; // "/C:".length === 3
}
}
// encode the rest of the path

View File

@@ -1,29 +1,11 @@
import { getReferenceType } from '../src/model/workspace';
import { FoamGraph } from '../src/model/graph';
import { Logger } from '../src/utils/log';
import { createTestNote, createTestWorkspace } from './core.test';
import { URI } from '../src/model/uri';
import { FoamWorkspace } from './workspace';
import { FoamGraph } from './graph';
import { Logger } from '../utils/log';
import { URI } from './uri';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
Logger.setLevel('error');
describe('Reference types', () => {
it('Detects absolute references', () => {
expect(getReferenceType('/hello')).toEqual('absolute-path');
expect(getReferenceType('/hello/there')).toEqual('absolute-path');
});
it('Detects relative references', () => {
expect(getReferenceType('../hello')).toEqual('relative-path');
expect(getReferenceType('./hello')).toEqual('relative-path');
expect(getReferenceType('./hello/there')).toEqual('relative-path');
});
it('Detects key references', () => {
expect(getReferenceType('hello')).toEqual('key');
});
it('Detects URIs', () => {
expect(getReferenceType(URI.file('/path/to/file.md'))).toEqual('uri');
});
});
describe('Workspace resources', () => {
it('Adds notes to workspace', () => {
const ws = createTestWorkspace();
@@ -71,6 +53,41 @@ describe('Workspace resources', () => {
ws.set(noteA);
expect(ws.list()).toEqual([noteA]);
});
it('#851 - listing by ID should not return files with same suffix', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'test-file.md' }))
.set(createTestNote({ uri: 'file.md' }));
expect(ws.listByIdentifier('file').length).toEqual(1);
});
it('Support dendron-style names', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'note.pdf' }))
.set(createTestNote({ uri: 'note.md' }))
.set(createTestNote({ uri: 'note.yo.md' }))
.set(createTestNote({ uri: 'note2.md' }));
for (const [reference, path] of [
['note', '/note.md'],
['note.md', '/note.md'],
['note.yo', '/note.yo.md'],
['note.yo.md', '/note.yo.md'],
['note.pdf', '/note.pdf'],
['note2', '/note2.md'],
]) {
expect(ws.listByIdentifier(reference)[0].uri.path).toEqual(path);
expect(ws.find(reference).uri.path).toEqual(path);
}
});
it('Should include fragment when finding resource URI', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'test-file.md' }))
.set(createTestNote({ uri: 'file.md' }));
const res = ws.find('test-file#my-section');
expect(res.uri.fragment).toEqual('my-section');
});
});
describe('Graph', () => {
@@ -146,6 +163,86 @@ describe('Graph', () => {
});
});
describe('Identifier computation', () => {
it('should compute the minimum identifier to resolve a name clash', () => {
const first = createTestNote({
uri: '/path/to/page-a.md',
});
const second = createTestNote({
uri: '/another/way/for/page-a.md',
});
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
expect(ws.getIdentifier(third.uri)).toEqual('path/for/page-a');
});
it('should support sections in identifier computation', () => {
const first = createTestNote({
uri: '/path/to/page-a.md',
});
const second = createTestNote({
uri: '/another/way/for/page-a.md',
});
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
'to/page-a#section name'
);
});
const needle = '/project/car/todo';
test.each([
[['/project/home/todo', '/other/todo', '/something/else'], 'car/todo'],
[['/family/car/todo', '/other/todo'], 'project/car/todo'],
[[], 'todo'],
])('Find shortest identifier', (haystack, id) => {
expect(FoamWorkspace.getShortestIdentifier(needle, haystack)).toEqual(id);
});
it('should ignore same string in haystack', () => {
const haystack = [
needle,
'/project/home/todo',
'/other/todo',
'/something/else',
];
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
expect(identifier).toEqual('car/todo');
});
it('should return best guess when no solution is possible', () => {
/**
* In this case there is no way to uniquely identify the element,
* our fallback is to just return the "least wrong" result, basically
* a full identifier
* This is an edge case that should never happen in a real repo
*/
const haystack = [
'/parent/' + needle,
'/project/home/todo',
'/other/todo',
'/something/else',
];
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
expect(identifier).toEqual('project/car/todo');
});
});
describe('Wikilinks', () => {
it('Can be defined with basename, relative path, absolute path, extension', () => {
const noteA = createTestNote({
@@ -313,15 +410,14 @@ describe('Wikilinks', () => {
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
// Attachments require extension
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([]);
});
it('Resolves conflicts alphabetically - part 1', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a' }],
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
@@ -343,7 +439,7 @@ describe('Wikilinks', () => {
it('Resolves conflicts alphabetically - part 2', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a' }],
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
@@ -362,21 +458,7 @@ describe('Wikilinks', () => {
]);
});
it('Allows for dendron-style wikilinks, including a dot', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'dendron.style' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/dendron.style.md' });
const ws = createTestWorkspace();
ws.set(noteA).set(noteB1);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB1.uri]);
});
it('Handles capatalization of files and wikilinks correctly', () => {
it('Handles capitalization of files and wikilinks correctly', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
@@ -408,7 +490,7 @@ describe('Wikilinks', () => {
});
});
describe('markdown direct links', () => {
describe('Markdown direct links', () => {
it('Support absolute and relative path', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',

View File

@@ -0,0 +1,199 @@
import { Resource, ResourceLink } from './note';
import { URI } from './uri';
import { isAbsolute, getExtension, changeExtension } from '../utils/path';
import { isSome } from '../utils';
import { Emitter } from '../common/event';
import { ResourceProvider } from './provider';
import { IDisposable } from '../common/lifecycle';
export class FoamWorkspace implements IDisposable {
private onDidAddEmitter = new Emitter<Resource>();
private onDidUpdateEmitter = new Emitter<{ old: Resource; new: Resource }>();
private onDidDeleteEmitter = new Emitter<Resource>();
onDidAdd = this.onDidAddEmitter.event;
onDidUpdate = this.onDidUpdateEmitter.event;
onDidDelete = this.onDidDeleteEmitter.event;
private providers: ResourceProvider[] = [];
/**
* Resources by path
*/
private resources: Map<string, Resource> = new Map();
registerProvider(provider: ResourceProvider) {
this.providers.push(provider);
return provider.init(this);
}
set(resource: Resource) {
const old = this.find(resource.uri);
this.resources.set(normalize(resource.uri.path), resource);
isSome(old)
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
: this.onDidAddEmitter.fire(resource);
return this;
}
delete(uri: URI) {
const deleted = this.resources.get(normalize(uri.path));
this.resources.delete(normalize(uri.path));
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
return deleted ?? null;
}
public exists(uri: URI): boolean {
return isSome(this.find(uri));
}
public list(): Resource[] {
return Array.from(this.resources.values());
}
public get(uri: URI): Resource {
const note = this.find(uri);
if (isSome(note)) {
return note;
} else {
throw new Error('Resource not found: ' + uri.path);
}
}
public listByIdentifier(identifier: string): Resource[] {
let needle = normalize('/' + identifier);
let mdNeedle = getExtension(needle) !== '.md' ? needle + '.md' : undefined;
let resources = [];
for (const key of this.resources.keys()) {
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
resources.push(this.resources.get(normalize(key)));
}
}
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
}
/**
* Returns the minimal identifier for the given resource
*
* @param forResource the resource to compute the identifier for
*/
public getIdentifier(forResource: URI): string {
const amongst = [];
const basename = forResource.getBasename();
for (const res of this.resources.values()) {
// Just a quick optimization to only add the elements that might match
if (res.uri.path.endsWith(basename)) {
if (!res.uri.isEqual(forResource)) {
amongst.push(res.uri);
}
}
}
let identifier = FoamWorkspace.getShortestIdentifier(
forResource.path,
amongst.map(uri => uri.path)
);
identifier = changeExtension(identifier, '.md', '');
if (forResource.fragment) {
identifier += `#${forResource.fragment}`;
}
return identifier;
}
public find(reference: URI | string, baseUri?: URI): Resource | null {
if (reference instanceof URI) {
return this.resources.get(normalize((reference as URI).path)) ?? null;
}
let resource: Resource | null = null;
let [path, fragment] = (reference as string).split('#');
if (FoamWorkspace.isIdentifier(path)) {
resource = this.listByIdentifier(path)[0];
} else {
if (isAbsolute(path) || isSome(baseUri)) {
if (getExtension(path) !== '.md') {
const uri = baseUri.resolve(path + '.md');
resource = uri ? this.resources.get(normalize(uri.path)) : null;
}
if (!resource) {
const uri = baseUri.resolve(path);
resource = uri ? this.resources.get(normalize(uri.path)) : null;
}
}
}
if (resource && fragment) {
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
}
return resource ?? null;
}
public resolveLink(resource: Resource, link: ResourceLink): URI {
// TODO add tests
const provider = this.providers.find(p => p.supports(resource.uri));
return (
provider?.resolveLink(this, resource, link) ??
URI.placeholder(link.target)
);
}
public read(uri: URI): Promise<string | null> {
const provider = this.providers.find(p => p.supports(uri));
return provider?.read(uri) ?? Promise.resolve(null);
}
public readAsMarkdown(uri: URI): Promise<string | null> {
const provider = this.providers.find(p => p.supports(uri));
return provider?.readAsMarkdown(uri) ?? Promise.resolve(null);
}
public dispose(): void {
this.onDidAddEmitter.dispose();
this.onDidDeleteEmitter.dispose();
this.onDidUpdateEmitter.dispose();
}
static isIdentifier(path: string): boolean {
return !(
path.startsWith('/') ||
path.startsWith('./') ||
path.startsWith('../')
);
}
/**
* Returns the minimal identifier for the given string amongst others
*
* @param forPath the value to compute the identifier for
* @param amongst the set of strings within which to find the identifier
*/
static getShortestIdentifier(forPath: string, amongst: string[]): string {
const needleTokens = forPath.split('/').reverse();
const haystack = amongst
.filter(value => value !== forPath)
.map(value => value.split('/').reverse());
let tokenIndex = 0;
let res = needleTokens;
while (tokenIndex < needleTokens.length) {
for (let j = haystack.length - 1; j >= 0; j--) {
if (
haystack[j].length < tokenIndex ||
needleTokens[tokenIndex] !== haystack[j][tokenIndex]
) {
haystack.splice(j, 1);
}
}
if (haystack.length === 0) {
res = needleTokens.splice(0, tokenIndex + 1);
break;
}
tokenIndex++;
}
const identifier = res
.filter(token => token.trim() !== '')
.reverse()
.join('/');
return identifier;
}
}
const normalize = (v: string) => v.toLocaleLowerCase();

Some files were not shown because too many files have changed in this diff Show More