Compare commits

...

20 Commits

Author SHA1 Message Date
Jani Eväkallio
ac9aa741ef Prettier 2020-08-22 00:36:36 +01:00
Jani Eväkallio
8b7e6983bf Add support for basic link aliases 2020-08-21 23:58:40 +01:00
zbw8388
19140488ae Fixed Note ${noteId} was not added to NoteGraph
After #208, id for each note in NoteGraph is hashed. However, in generateReferenceList in file wikilink-reference-generation.ts, the id is still given by the file path, which compromised the core functionality. This PR fixed this bug.
2020-08-11 11:46:12 +02:00
Martin Laws
3297d8bd67 Fixed typos in foam workspace link (#214) 2020-08-07 07:57:19 +01:00
ingalless
5fabe08afa Add pre-defined user snippets to roadmap (#192)
Co-authored-by: Jonathan Ingall <jonny@mondago.com>
2020-08-05 18:35:50 +01:00
Riccardo
87f172d217 Decoupled note ID from note slug (#208)
* decoupled ID from note slug

* added vscode debug configuration to work with vscode-jest extension

* made things prettier

* Add a test for parsing empty frontmatter

* Use astPositionToVsCodePosition util function

* fixed a TODOs around URIs and added tests

* added tests around notes querying

* removing unused imports

* Fix typo

* implemented PR feedback

* fixed lint warnings and renamed method for consistency

* removed unnecessary checks and fix compilation error in tests

Co-authored-by: Ankit Tiwari <ankitt255@gmail.com>
2020-08-05 18:37:35 +02:00
allcontributors[bot]
3bebbcc45a docs: add dshemetov as a contributor (#210)
* 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>
2020-08-04 13:28:08 -07:00
Dmitry Shemetov
b4224659b7 Fill out [[Migrating from OneNote]] stub. (#200)
* Fill out Migrating from OneNote stub

* Update Summary and improve grammatic consistency

* Added terminal navigation instructions

* kebab-case and update roadmap links
2020-08-04 13:26:33 -07:00
Joe Previte
7f3ba7853e feat(foam-vscode): add jest for testing (#204)
* feat: setup jest for foam-vscode

* Add failing test (fails due to missing vscode dependency)

* feat: add vscode mock

Co-authored-by: Jani Eväkallio <jani.evakallio@gmail.com>
2020-08-04 13:27:11 +01:00
Ankit Tiwari
bcab41917a Parse frontmatter and use frontmatter.title as heading (fixes #174) (#191)
* Add frontmatter data to NoteGraph node

* Store document start position inside NoteGraph node

* Add heading below the frontmatter

* Show a progress bar while running Janitor

* Fix tests

* Update note start position to make it 1-indexed

* Initialize frontmatter by an empty object instead of null

* Remove console logs
2020-08-03 18:41:12 +02:00
allcontributors[bot]
f32b432e26 docs: add hooncp as a contributor (#207)
* 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>
2020-08-02 16:34:20 -07:00
hooncp
5383865d88 Update documentation by adding new FAQ page (#202)
* Update documentation by adding new FAQ page

* Update FAQ page according to review
2020-08-02 16:33:22 -07:00
allcontributors[bot]
c2c639b402 docs: add Klaudioz as a contributor (#205)
* 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>
2020-08-02 11:51:20 -07:00
Claudio Canales
f4a864ee7d Add how to update pushed Azure Wiki (#203)
* Add how to update pushed Azure Wiki

Add the way to push to GitHub and Azure with one command.

* Update docs/azure-devops-wiki.md

Co-authored-by: Joe Previte <jjprevite@gmail.com>

* Update azure-devops-wiki.md

Add suggested changes

* Update docs/azure-devops-wiki.md

Co-authored-by: Joe Previte <jjprevite@gmail.com>

Co-authored-by: Joe Previte <jjprevite@gmail.com>
2020-08-02 10:45:03 -07:00
hooncp
bf2aa599ef Remove brackets (#201) 2020-08-02 18:33:03 +01:00
Claudio Canales
24738e4390 Update azure-devops-wiki.md (#189)
I followed the entire guide so I think this addition can do it a little bit more friendly.
2020-07-30 21:40:33 +01:00
Jani Eväkallio
0e0355ae9f Fix VSCode Launch Task and Monorepo builds (#193)
* revise vscode task and launch configs

- Removed commands from sub-package:
  same can be achieved from the root
- Added build and no-build variants.
  In case separate watcher is already running,
  no need to build.
- Changed watch to build (no hot-reload in vscode by default)
- Show the build in console

Didn't update yarn.lock, seems to want to add
foam-core@0.3.0-alpha.0, but not really
related to this change

Note: the extension tests are failing due to
`Cannot find module '...foam-vscode/out/test/suite/index'`.
However that shouldn't be related to this change either.

* Simplify VS Code launch tasks (shift work to typescript compiler)

* Fix circular dependency index->janitor/index->index

* Use --build flag to use TSC in Build Mode
https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript

This configures tsc to use multiple project roots (vscode, core) and run incremental builds on
changes to each projects. This will necessitate us running the core in commonjs build mode,
which is fine for testing and deployment to node environments, but won't work in the browser,
so foam-core will need a separate build config for UMD builds

* Build foam-core in commonjs mode by default (+cleanup unnecessary config)

* Create ES6 build config for distribution builds

* Cleanup yarn.lock

* Build package before running

* Fix missing lint paths and fix lint error

* Remove redundant .vscode settings files from foam-vscode project

* Add core test launch task

Co-authored-by: jojanaho <janne.ojanaho@iki.fi>
2020-07-30 21:39:16 +01:00
allcontributors[bot]
49ddc41d41 docs: add dshemetov as a contributor (#195)
* 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>
2020-07-30 10:13:10 +01:00
Dmitry Shemetov
5226917e35 Update creating-new-notes.md (#194)
* Update creating-new-notes.md

Added a non-intuitive hotkey for entering notes.

* Add instructions on how to remap the key binding

Co-authored-by: Jani Eväkallio <jani.evakallio@gmail.com>
2020-07-30 08:23:41 +01:00
allcontributors[bot]
ba6448ee89 docs: add vitaly-pevgonen as a contributor (#190)
* 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>
2020-07-29 08:38:17 -07:00
49 changed files with 1962 additions and 526 deletions

View File

@@ -315,6 +315,33 @@
"contributions": [
"doc"
]
},
{
"login": "vitaly-pevgonen",
"name": "vitaly-pevgonen",
"avatar_url": "https://avatars0.githubusercontent.com/u/6272738?v=4",
"profile": "https://github.com/vitaly-pevgonen",
"contributions": [
"doc"
]
},
{
"login": "dshemetov",
"name": "Dmitry Shemetov",
"avatar_url": "https://avatars0.githubusercontent.com/u/1810426?v=4",
"profile": "https://dshemetov.github.io",
"contributions": [
"doc"
]
},
{
"login": "hooncp",
"name": "hooncp",
"avatar_url": "https://avatars1.githubusercontent.com/u/48883554?v=4",
"profile": "https://github.com/hooncp",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7,

View File

@@ -20,8 +20,8 @@ jobs:
- name: Lint foam-vscode
run: yarn workspace foam-vscode lint
# - name: Test foam-vscode
# run: yarn workspace foam-vscode test
- name: Test foam-vscode
run: yarn workspace foam-vscode test
# - name: Publish foam-vscode
# if: github.ref == 'refs/heads/master'
# run: yarn workspace foam-vscode publish-extension

49
.vscode/launch.json vendored
View File

@@ -6,10 +6,34 @@
"version": "0.2.0",
"configurations": [
{
// This task is also defined in packages/foam-vscode/.vscode/launch.json
// for when running separately outside of the monorepo environment
"name": "Run Extension",
"type": "extensionHost",
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"runtimeArgs": ["workspace", "foam-core", "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": [
@@ -18,12 +42,14 @@
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
],
"preLaunchTask": "Build foam-vscode"
"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!
{
// This task is also defined in packages/foam-vscode/.vscode/launch.json
// for when running separately outside of the monorepo environment
"name": "Extension Tests",
"name": "Test VSCode Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
@@ -34,16 +60,17 @@
"outFiles": [
"${workspaceFolder}/packages/foam-vscode/out/test/**/*.js"
],
"preLaunchTask": "Build foam-vscode"
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Test Core",
"type": "node",
"request": "launch",
"name": "Workspace Manager tests",
"program": "${workspaceFolder}/node_modules/tsdx/dist/index.js",
"args": ["test"],
"cwd": "${workspaceFolder}/packages/foam-core",
"internalConsoleOptions": "openOnSessionStart"
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "${defaultBuildTask}"
}
]
}

29
.vscode/tasks.json vendored
View File

@@ -4,19 +4,28 @@
"version": "2.0.0",
"tasks": [
{
// This task is also defined in packages/foam-vscode/.vscode/tasks.json
// for when running separately outside of the monorepo environment
"type": "npm",
"script": "watch",
"label": "Build foam-vscode",
"path": "packages/foam-vscode",
"problemMatcher": "$tsc-watch",
"label": "watch: foam-vscode",
"type": "npm",
"script": "start:vscode",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem",
"focus": true
"reveal": "always"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "test: all packages",
"type": "npm",
"script": "test",
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
},
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

View File

@@ -9,7 +9,7 @@ The following recipe is written with the assumption that you already have an [Az
## Setup a Foam workspace
1. Generate a Foam workspace using the [foam-template project](https://github.com/foambubble/foam-template).
2. Change the remote to a git repository in Azure DevOps, or copy all the files into a new Azure DevOps git repository.
2. Change the remote to a git repository in Azure DevOps (Repos -> Import a Repository -> Add Clone URL with Authentication), or copy all the files into a new Azure DevOps git repository.
3. Define which document will be the wiki home page. To do that, create a file called `.order` in the Foam workspace root folder, with first line being the document filename without `.md` extension. For a project created from the Foam template, the file would look like this:
```
readme
@@ -31,4 +31,33 @@ There is default table of contents pane to the left of the wiki content. Here, y
_Note that first entry in `.order` file defines wiki's home page._
## Update wiki
While you are pushing changes to GitHub, you won't see the wiki updated if you don't add Azure as a remote. You can push to multiple repositories simultaneously.
1. First open a terminal and check if Azure is added running: `git remote show origin`. If you don't see Azure add it in the output then follow these steps.
2. Rename your current remote (most likely named origin) to a different name by running: `git remote rename origin main`
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
```bash
[core]
...
(ignore this part)
...
[branch "master"]
remote = github
merge = refs/heads/master
[remote "github"]
url = git@github.com:username/repo.git
fetch = +refs/heads/*:refs/remotes/github/*
[remote "azure"]
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
fetch = +refs/heads/*:refs/remotes/azure/*
[remote "origin"]
url = git@github.com:username/repo.git
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
```
6. You can then push to both repositories by: `git push origin master` or a single one using: `git push github master` or `git push azure master`
For more information, read the [Azure DevOps documentation](https://docs.microsoft.com/en-us/azure/devops/project/wiki/publish-repo-to-wiki).

View File

@@ -1,7 +1,8 @@
# Creating New Notes
- Write out a new `[[wiki-link]]` and `Cmd` + `Click` to create a new file.
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `New Note` from [VS Code Markdown Notes](<(https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes)>) and enter a **Title Case Name** to create `title-case-name.md`
- Write out a new `[[wiki-link]]` and `Cmd` + `Click` to create a new file and enter it.
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `New Note` from [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) and enter a **Title Case Name** to create `title-case-name.md`
- Add a keyboard binding to make creating new notes easier.
- You shouldn't worry too much about categorising your notes. You can always [[search-for-notes]], and explore them using the [[graph-visualisation]].

10
docs/custom-snippets.md Normal file
View File

@@ -0,0 +1,10 @@
# Adding Custom Snippets
You can add custom snippets whilst the default set of snippets are decided by following the below steps:
1. `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type `snippets` and select `Preferences: Configure User Snippets`.
2. The command palette will remain in focus. Search for `markdown` and select the entry entitled `markdown.json (Markdown)`.
3. A JSON file will open. You can author your own snippets using the [documentation](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets) to help you, or if you're using a snippet shared by another Foam user then you can copy and paste it in, as the below GIF demonstrates:
![Demonstrating adding a custom snippet](./assets/images/custom-snippet.gif)
To get started, you might consider replacing the entire contents of the `markdown.json` file opened by the steps above with the JSON in [this comment](https://github.com/foambubble/foam/pull/192#issuecomment-666736270).

View File

@@ -0,0 +1,19 @@
# Frequently Asked Questions
> ⚠️ Foam is still in preview. Expect the experience to be a little rough.
- [Frequently Asked Questions](#frequently-asked-questions)
- [Links/Graphs/BackLinks don't work. How do I enable them?](#linksgraphsbacklinks-dont-work-how-do-i-enable-them)
## Links/Graphs/BackLinks don't work. How do I enable them?
- Ensure that you have all the [[recommended-extensions]] installed in Visual Studio Code
- Reload Visual Studio Code by running `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "reload" and run the **Developer: Reload Window** command to for the updated extensions take effect
- Check the formatting rules for links on [[foam-file-format]], [[wiki-links]] and [[link-formatting-and-autocompletion]]
[//begin]: # "Autogenerated link references for markdown compatibility"
[recommended-extensions]: recommended-extensions "Recommended Extensions"
[foam-file-format]: foam-file-format "Foam File Format"
[wiki-links]: wiki-links "Wiki Links"
[link-formatting-and-autocompletion]: link-formatting-and-autocompletion "Link Formatting and Autocompletion"
[//end]: # "Autogenerated link references"

View File

@@ -74,6 +74,8 @@ After setting up the repository, open `.vscode/settings.json` and edit, add or r
To learn more about how to use **Foam**, read the [[recipes]].
Getting stuck in the setup? Read the [[frequently-asked-questions]].
There are [[known-issues]], and I'm sure, many unknown issues! Please [report them on GitHub](http://github.com/foambubble/foam/issues)!
## Features
@@ -149,6 +151,11 @@ If that sounds like something you're interested in, I'd love to have you along o
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
</tr>
</table>
@@ -169,6 +176,7 @@ Foam is licensed under the [MIT license](license).
[backlinking]: backlinking "Backlinking"
[recommended-extensions]: recommended-extensions "Recommended Extensions"
[recipes]: recipes "Recipes"
[frequently-asked-questions]: frequently-asked-questions "Frequently Asked Questions"
[known-issues]: known-issues "Known Issues"
[roadmap]: roadmap "Roadmap"
[principles]: principles "Principles"

View File

@@ -0,0 +1,36 @@
# Migrating from OneNote
This guide mostly duplicates the instructions at the repo for the PowerShell [script](https://github.com/nixsee/ConvertOneNote2MarkDown).
## Summary
The powershell script 'ConvertOneNote2MarkDown-v2.ps1' will utilize the OneNote Object Model on your workstation to convert all OneNote pages to Word documents and then utilizes PanDoc to convert the Word documents to Markdown (.md) format. It will also:
* Create a folder structure for your Notebooks and Sections.
* Process pages that are in sections at the Notebook, Section Group and 1st Nested Section Group levels.
* Allow you you choose between putting all Images in a central '/media' folder for each notebook, or in a separate '/media' folder in each folder of the hierarchy.
* Fix image references in the resulting .md files, generating relative references to the image files within the markdown document.
* A title, description, and date header will be added to each file as well.
* And more (see details at repo)!
## Usage
1. Start the OneNote application. All notebooks currently loaded in [OneNote](https://getonetastic.com/download) will be converted.
2. It is advised that you install [Onetastic](https://getonetastic.com/download) and the attached macro, which will automatically expand any collapsed paragraphs in the notebook. They won't be exported otherwise.
* To install the macro, click the New Macro Button within the Onetastic Toolbar and then select File -> Import and select the .xml macro included in the release.
* Run the macro for each Notebook that is open
3. For the next sections, it is highly recommended that you use VS Code, and its embedded PowerShell terminal, as this allows you to edit and run the script, as well as check the results of the .md output all in one window.
4. Whatever you choose, you will need to do the following:
1. Clone the script to your computer (see [here](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository), if you're unfamiliar with git).
2. Once cloned, navigate to the repo folder. In VS Code, use File -> Add Folder to Workspace, right click on the folder in the left side bar and click [Open In Integrated Terminal](assets/images/migrating-one-note.png).
3. Run the script by executing
```.\ConvertOnenote2Markdown-v2```
* if you receive an error, try running this line to bypass security:
```Set-ExecutionPolicy Bypass -Scope Process```
* if you still have trouble, try running both Onenote and Powershell as an administrator.
5. It will ask you for the path to store the markdown folder structure. Please use an empty folder. If using VS Code, you might not be able to paste the filepath - right click on the blinking cursor and it will paste from clipboard. **Attention:** use a full absolute path for the destination.
6. Read the prompts carefully to select your desired options. If you aren't actively editing your pages in Onenote, it is HIGHLY recommended that you don't delete the intermediate word docs, as they take 80+% of the time to generate. They are stored in their own folder, out of the way. You can then quickly re-run the script with different parameters until you find what you like.
7. Sit back and wait until the process completes.
8. To stop the process at any time, press Ctrl+C.
9. If you like, you can inspect some of the .md files prior to completion. If you're not happy with the results, stop the process, delete the .md and re-run with different parameters.
10. At this point, you should be ready to load the new directory into Foam!

View File

@@ -0,0 +1,59 @@
# Pre-defined User Snippets
Having pre-defined user snippets would enable us to introduce Roam style commands to Foam. Consider the below snippets:
```json
{
"Zettelkasten Id": {
"scope": "markdown",
"prefix": "/id",
"description": "Zettelkasten Id",
"body": [
"${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}"
]
},
"Current date": {
"scope": "markdown",
"prefix": "/date",
"description": "Current date",
"body": [
"${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}"
]
}
}
```
Which would look like:
![GIF demonstrating User Snippets](./assets/images/snippets.gif)
Using snippets enables Foam users to add [[custom-snippets]] themselves so they live alongside the first-class `/commands`.
## Notes & Considerations
- VS Code supplies "commands" already via the command palette
- Consider the UX around this. Users less familiar with VS Code are more likely to be familiar with `/` to trigger a command menu. Experienced VS Code users may be more likely to favour the command palette.
- We can use `TabCompletionProvider` and `snippets` and mix them (maybe) via the following VS Code setting: `"editor.snippetSuggestions": "inline" | "top" | "bottom" | "none"`
- For more discussion, consult the PR [here](https://github.com/foambubble/foam/pull/192).
## Simplifying Markdown Syntax
Some markdown syntax is difficult for users who have never authored markdown before. Take for example a checkbox/todo. The following syntax is required:
```
- [ ] Something todo...
```
We could provide snippets that expand out into the associated markdown syntax, like in the below GIF:
![GIF demonstrating markdown snippets](./assets/images/markdown-snippets.gif)
The JSON for these snippets can be found [here](https://github.com/foambubble/foam/pull/192#issuecomment-666736270).
[//begin]: # 'Autogenerated link references for markdown compatibility'
[custom-snippets]: custom-snippets 'Adding Custom Snippets'
[//end]: # 'Autogenerated link references'
[//begin]: # 'Autogenerated link references for markdown compatibility'
[custom-snippets]: custom-snippets 'Adding Custom Snippets'
[//end]: # 'Autogenerated link references'
[//begin]: # 'Autogenerated link references for markdown compatibility'
[custom-snippets]: custom-snippets 'Adding Custom Snippets'
[//end]: # 'Autogenerated link references'

View File

@@ -36,7 +36,7 @@ This principle may seem like it contradicts [Foam wants you to own your thoughts
## Foam allows people to collaborate in discovering better ways to work, together.
- **Foam is a collection of ideas.** Foam was released to the public not to share the few good ideas in it, but to learn many good ideas from others. As you improve your own workflow, share your work on your own Foam blog.
- **Foam is open for contributions.** If you use a tool or workflow that you like that fits these principles, please contribute them back to the Foam template as [[recipes]], [[recommended-extensions]] or documentation in [this workspace](httpsL//github,com/foambubble/foam). See also: [[roadmap]] and [[contribution-guide]].
- **Foam is open for contributions.** If you use a tool or workflow that you like that fits these principles, please contribute them back to the Foam template as [[recipes]], [[recommended-extensions]] or documentation in [this workspace](https://github.com/foambubble/foam). See also: [[roadmap]] and [[contribution-guide]].
- **Foam is open source.** Feel free to fork it, improve it and remix it. Just don't sell it, as per our [license](license).
- **Foam is not Roam.** This project was inspired by Roam Research, but we're not limited by what Roam does. No idea is too big (though if it doesn't fit with Foam's core workflow, we might make it a [[recipes]] page instead).

View File

@@ -43,6 +43,7 @@ If a roadmap item is a stub, **consider** opening a [GitHub issue](https://githu
- [[git-flows-for-teams]]
- [[user-settings]]
- [[link-reference-definitions]]
- [[predefined-user-snippets]]
### Publishing
@@ -65,14 +66,18 @@ If a roadmap item is a stub, **consider** opening a [GitHub issue](https://githu
### Migration
The community is working on a number of automated scripts to help you migrate to Foam. The main work of developing such a method involves exporting your notes, converting them to the Markdown format, and then making sure that the links between notes (if you had those) still work.
- [[migrating-from-roam]]
- Discussion: [foam#55](https://github.com/foambubble/foam/issues/55)
- [[migrating-from-obsidian]]
- Discussion: [foam#46](https://github.com/foambubble/foam/issues/46)
- [[Migrating from OneNote (stub)]]
- [[migrating-from-onenote]]
- Discussion: [foam#151](https://github.com/foambubble/foam/issues/151)
- _Migration from other tools..._
### integration
### Integration
- _Integrations to third party tools_...
### Wild ideas
@@ -100,6 +105,7 @@ If a roadmap item is a stub, **consider** opening a [GitHub issue](https://githu
[git-flows-for-teams]: git-flows-for-teams "Git Flows for Teams (stub)"
[user-settings]: user-settings "User Settings (stub)"
[link-reference-definitions]: link-reference-definitions "Link Reference Definitions"
[predefined-user-snippets]: predefined-user-snippets "Pre-defined User Snippets"
[officially-support-alternative-templates]: officially-support-alternative-templates "Officially Support Alternative Templates (stub)"
[improved-static-site-generation]: improved-static-site-generation "Improved Static Site Generation (stub)"
[mdx-by-default]: mdx-by-default "MDX by Default(stub)"
@@ -112,6 +118,7 @@ If a roadmap item is a stub, **consider** opening a [GitHub issue](https://githu
[web-editor]: web-editor "Web Editor (stub)"
[migrating-from-roam]: migrating-from-roam "Migrating from Roam (stub)"
[migrating-from-obsidian]: migrating-from-obsidian "Migrating from Obsidian (stub)"
[migrating-from-onenote]: migrating-from-onenote "Migrating from OneNote"
[foam-linter]: foam-linter "Foam Linter (stub)"
[refactoring-via-language-server-protocol]: refactoring-via-language-server-protocol "Refactoring via Language Server Protocol (stub)"
[//end]: # "Autogenerated link references"

View File

@@ -10,7 +10,7 @@
"packages/*"
],
"scripts": {
"watch:vscode": "yarn workspace foam-vscode watch",
"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",

View File

@@ -63,7 +63,7 @@
"build": "tsc -b",
"test": "jest",
"lint": "echo Missing lint task in CLI package",
"cli": "./bin/run",
"cli": "yarn build && ./bin/run",
"postpack": "rm -f oclif.manifest.json",
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
"version": "oclif-dev readme && git add README.md"

View File

@@ -57,12 +57,12 @@ export default class Janitor extends Command {
const definitions = generateLinkReferences(note, graph, !flags['without-extensions']);
// apply Edits
let file = note.source;
let file = note.source.text;
file = heading ? applyTextEdit(file, heading) : file;
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.path, file);
return writeFileToDisk(note.source.uri, file);
}
return Promise.resolve(null);

View File

@@ -58,7 +58,7 @@ Successfully generated link references and heading!
if (note.title != null) {
const kebabCasedFileName = getKebabCaseFileName(note.title);
if (kebabCasedFileName) {
return renameFile(note.path, kebabCasedFileName);
return renameFile(note.source.uri, kebabCasedFileName);
}
}
return Promise.resolve(null);
@@ -83,12 +83,12 @@ Successfully generated link references and heading!
const definitions = generateLinkReferences(note, graph, !flags['without-extensions']);
// apply Edits
let file = note.source;
let file = note.source.text;
file = heading ? applyTextEdit(file, heading) : file;
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.path, file);
return writeFileToDisk(note.source.uri, file);
}
return Promise.resolve(null);

View File

@@ -9,9 +9,9 @@
],
"scripts": {
"clean": "rimraf dist",
"build": "tsdx build",
"build": "tsdx build --tsconfig ./tsconfig.build.json",
"test": "tsdx test",
"lint": "tsdx lint",
"lint": "tsdx lint src test",
"watch": "tsdx watch",
"prepare": "tsdx build"
},
@@ -30,11 +30,13 @@
"glob": "^7.1.6",
"graphlib": "^2.1.8",
"lodash": "^4.17.19",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",
"remark-wiki-link": "^0.0.4",
"title-case": "^3.0.2",
"unified": "^9.0.0",
"unist-util-visit": "^2.0.2"
"unist-util-visit": "^2.0.2",
"yaml": "^1.10.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts"

View File

@@ -1,11 +1,10 @@
import { Position } from 'unist';
import GithubSlugger from 'github-slugger';
import { Note, GraphNote, NoteGraph } from '../note-graph';
import {
Note,
NoteGraph,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from '../index';
} from '../definitions';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
@@ -20,7 +19,7 @@ export interface TextEdit {
}
export const generateLinkReferences = (
note: Note,
note: GraphNote,
ng: NoteGraph,
includeExtensions: boolean
): TextEdit | null => {
@@ -41,19 +40,22 @@ export const generateLinkReferences = (
LINK_REFERENCE_DEFINITION_HEADER,
...markdownReferences.map(stringifyMarkdownLinkReferenceDefinition),
LINK_REFERENCE_DEFINITION_FOOTER,
].join(note.eol);
].join(note.source.eol);
if (note.definitions.length === 0) {
if (newReferences.length === 0) {
return null;
}
const padding = note.end.column === 1 ? note.eol : `${note.eol}${note.eol}`;
const padding =
note.source.end.column === 1
? note.source.eol
: `${note.source.eol}${note.source.eol}`;
return {
newText: `${padding}${newReferences}`,
range: {
start: note.end,
end: note.end,
start: note.source.end,
end: note.source.end,
},
};
} else {
@@ -62,7 +64,7 @@ export const generateLinkReferences = (
const oldReferences = note.definitions
.map(stringifyMarkdownLinkReferenceDefinition)
.join(note.eol);
.join(note.source.eol);
if (oldReferences === newReferences) {
return null;
@@ -88,11 +90,25 @@ export const generateHeading = (note: Note): TextEdit | null => {
return null;
}
const frontmatterExists = note.source.contentStart.line !== 1;
const newLineExistsAfterFrontmatter =
frontmatterExists &&
note.source.text.split(note.source.eol)[note.source.contentStart.line - 1]
.length === 0;
const paddingStart = frontmatterExists ? note.source.eol : '';
const paddingEnd = newLineExistsAfterFrontmatter
? note.source.eol
: `${note.source.eol}${note.source.eol}`;
return {
newText: `# ${getHeadingFromFileName(note.id)}${note.eol}${note.eol}`,
newText: `${paddingStart}# ${getHeadingFromFileName(
note.slug
)}${paddingEnd}`,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 0, offset: 0 },
start: note.source.contentStart,
end: note.source.contentStart,
},
};
};

View File

@@ -1,20 +1,26 @@
import unified from 'unified';
import markdownParse from 'remark-parse';
import wikiLinkPlugin from 'remark-wiki-link';
import frontmatterPlugin from 'remark-frontmatter';
import { parse as parseYAML } from 'yaml';
import visit, { CONTINUE, EXIT } from 'unist-util-visit';
import { Node, Parent, Point } from 'unist';
import * as path from 'path';
import { Note, NoteLink, NoteLinkDefinition, NoteGraph } from './note-graph';
import { dropExtension } from './utils';
import { NoteLink, NoteLinkDefinition, NoteGraph, Note } from './note-graph';
import { dropExtension, uriToSlug } from './utils';
import { ID } from './types';
let processor: unified.Processor | null = null;
const ALIAS_DIVIDER_CHAR = '|';
function parse(markdown: string): Node {
processor =
processor ||
unified()
.use(markdownParse, { gfm: true })
.use(wikiLinkPlugin);
.use(frontmatterPlugin, ['yaml'])
.use(wikiLinkPlugin, { aliasDivider: ALIAS_DIVIDER_CHAR });
return processor.parse(markdown);
}
@@ -23,23 +29,46 @@ export function createNoteFromMarkdown(
markdown: string,
eol: string
): Note {
const filename = path.basename(uri);
const id = path.parse(filename).name;
const tree = parse(markdown);
let title: string | null = null;
visit(tree, node => {
if (node.type === 'heading' && node.depth === 1) {
title = ((node as Parent)!.children[0].value as string) || title;
}
return title === null ? CONTINUE : EXIT;
});
const links: NoteLink[] = [];
const linkDefinitions: NoteLinkDefinition[] = [];
let frontmatter: any = {};
let start: Point = { line: 1, column: 1, offset: 0 }; // start position of the note
visit(tree, node => {
if (node.type === 'yaml') {
frontmatter = parseYAML(node.value as string) ?? {}; // parseYAML returns null if the frontmatter is empty
// Update the start position of the note by exluding the metadata
start = {
line: node.position!.end.line! + 1,
column: 1,
offset: node.position!.end.offset! + 1,
};
}
if (node.type === 'wikiLink') {
// links can be either in format [[text]] or [[text|alias]].
const text = node.value as string;
const alias = node.data?.alias as string;
const literalContent = markdown.substring(
node.position!.start.offset! + 2,
node.position!.end.offset! - 2
);
const hasAlias =
literalContent !== text && literalContent.includes(ALIAS_DIVIDER_CHAR);
links.push({
to: node.value as string,
text: node.value as string,
type: 'wikilink',
slug: text.trim(),
alias: hasAlias ? alias.trim() : null,
literalContent,
position: node.position!,
});
}
@@ -54,10 +83,26 @@ export function createNoteFromMarkdown(
}
});
// Give precendence to the title from the frontmatter if it exists
title = frontmatter.title ?? title;
const end = tree.position!.end;
const definitions = getFoamDefinitions(linkDefinitions, end);
return new Note(id, title, links, definitions, end, uri, markdown, eol);
return {
properties: frontmatter,
slug: uriToSlug(uri),
title: title,
links: links,
definitions: definitions,
source: {
uri: uri,
text: markdown,
contentStart: start,
end: end,
eol: eol,
},
};
}
function getFoamDefinitions(
@@ -95,9 +140,10 @@ export function stringifyMarkdownLinkReferenceDefinition(
return text;
}
export function createMarkdownReferences(
graph: NoteGraph,
noteId: string,
noteId: ID,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = graph.getNote(noteId);
@@ -111,36 +157,78 @@ export function createMarkdownReferences(
return [];
}
return graph
.getForwardLinks(noteId)
.map(link => {
const target = graph.getNote(link.to);
// if note doesn't exist, we can't find its links
const note = graph.getNote(noteId);
if (!note) {
return [];
}
// We are dropping links to non-existent notes here,
// but int the future we may want to surface these too
if (!target) {
console.log(
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
const forwardLinks = graph.getForwardLinks(noteId);
// Try to generate a definition for each [[link]] in the note.
//
// A note may have multiple [[link]] expressions to the same target
// note with different aliases.s
//
// - note.links contains all the [[link]] expressions
// - forwardLinks contains all the edges (links) between documents
return (
note.links
.map(linkExpression => {
// find the link between this and other document
const link = forwardLinks.find(
note => note.link.slug === linkExpression.slug
);
return null;
}
const relativePath = path.relative(
path.dirname(source.path),
target.path
);
// if other document is not found, bail
if (!link) {
return null;
}
const pathToNote = includeExtension
? relativePath
: dropExtension(relativePath);
// find the target document
let target = graph.getNote(link.to);
// [wiki-link-text]: path/to/file.md "Page title"
return {
label: link.text,
url: pathToNote,
title: target.title || target.id,
};
})
.filter(Boolean)
.sort() as NoteLinkDefinition[];
// if we don't find the target by ID we search the graph by slug
if (!target) {
const candidates = graph.getNotes({ slug: linkExpression.slug });
if (candidates.length > 1) {
console.log(
`Warning: Slug ${linkExpression.slug} matches ${candidates.length} documents. Picking one.`
);
}
target = candidates.length > 0 ? candidates[0] : null;
}
// We are dropping links to non-existent notes here,
// but in the future we may want to surface these too
if (!target) {
console.log(
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
);
return null;
}
const relativePath = path.relative(
path.dirname(source.source.uri),
target.source.uri
);
const pathToNote = includeExtension
? relativePath
: dropExtension(relativePath);
// [wiki-link-text]: path/to/file.md "Page title"
return {
label: linkExpression.literalContent,
url: pathToNote,
title: linkExpression.alias || target.title || target.slug,
};
})
// remove empty items
.filter(Boolean)
// sort by label, ascending
.sort((a, b) => (a!.label < b!.label ? -1 : 1)) as NoteLinkDefinition[]
);
}

View File

@@ -1,22 +1,27 @@
import { Graph, Edge } from 'graphlib';
import { Position, Point } from 'unist';
import GithubSlugger from 'github-slugger';
import { Graph } from 'graphlib';
import { EventEmitter } from 'events';
import { Position, Point, URI, ID } from './types';
import { hashURI, computeRelativeURI } from './utils';
type ID = string;
export interface Link {
from: ID;
to: ID;
export interface NoteSource {
uri: URI;
text: string;
contentStart: Point;
end: Point;
eol: string;
}
export interface NoteLink {
to: ID;
text: string;
export interface WikiLink {
type: 'wikilink';
slug: string;
alias: string | null;
literalContent: string;
position: Position;
}
// at the moment we only model wikilink
export type NoteLink = WikiLink;
export interface NoteLinkDefinition {
label: string;
url: string;
@@ -24,91 +29,108 @@ export interface NoteLinkDefinition {
position?: Position;
}
export class Note {
public id: ID;
public title: string | null;
public source: string;
public path: string;
public end: Point;
public eol: string;
public links: NoteLink[];
public definitions: NoteLinkDefinition[];
constructor(
id: ID,
title: string | null,
links: NoteLink[],
definitions: NoteLinkDefinition[],
end: Point,
path: string,
source: string,
eol: string
) {
this.id = id;
this.title = title;
this.source = source;
this.path = path;
this.links = links;
this.definitions = definitions;
this.end = end;
this.eol = eol;
}
export interface Note {
title: string | null;
slug: string; // note: this slug is not necessarily unique
properties: object;
// sections: NoteSection[]
// tags: NoteTag[]
links: NoteLink[];
definitions: NoteLinkDefinition[];
source: NoteSource;
}
export type NoteGraphEventHandler = (e: { note: Note }) => void;
export type GraphNote = Note & {
id: ID;
};
export interface GraphConnection {
from: ID;
to: ID;
link: NoteLink;
}
export type NoteGraphEventHandler = (e: { note: GraphNote }) => void;
export type NotesQuery = { slug: string } | { title: string };
export class NoteGraph {
private graph: Graph;
private events: EventEmitter;
private createIdFromURI: (uri: URI) => ID;
constructor() {
this.graph = new Graph();
this.events = new EventEmitter();
this.createIdFromURI = hashURI;
}
public setNote(note: Note) {
const noteExists = this.graph.hasNode(note.id);
public setNote(note: Note): GraphNote {
const id = this.createIdFromURI(note.source.uri);
const noteExists = this.graph.hasNode(id);
if (noteExists) {
(this.graph.outEdges(note.id) || []).forEach(edge => {
(this.graph.outEdges(id) || []).forEach(edge => {
this.graph.removeEdge(edge);
});
}
this.graph.setNode(note.id, note);
const graphNote: GraphNote = {
...note,
id: id,
};
this.graph.setNode(id, graphNote);
note.links.forEach(link => {
const slugger = new GithubSlugger();
this.graph.setEdge(note.id, slugger.slug(link.to), link.text);
const relativePath =
note.definitions.find(def => def.label === link.slug)?.url ?? link.slug;
const targetPath = computeRelativeURI(note.source.uri, relativePath);
const targetId = this.createIdFromURI(targetPath);
const connection: GraphConnection = {
from: graphNote.id,
to: targetId,
link: link,
};
this.graph.setEdge(graphNote.id, targetId, connection);
});
this.events.emit(noteExists ? 'update' : 'add', { note });
this.events.emit(noteExists ? 'update' : 'add', { note: graphNote });
return graphNote;
}
public getNotes(): Note[] {
return this.graph.nodes().map(id => this.graph.node(id));
public getNotes(query?: NotesQuery): GraphNote[] {
// prettier-ignore
const filterFn =
query == null ? (note: Note | null) => note != null
: 'slug' in query ? (note: Note | null) => note?.slug === query.slug
: 'title' in query ? (note: Note | null) => note?.title === query.title
: (note: Note | null) => note != null;
return this.graph
.nodes()
.map(id => this.graph.node(id))
.filter(filterFn);
}
public getNote(noteId: ID): Note | void {
if (this.graph.hasNode(noteId)) {
return this.graph.node(noteId);
}
throw new Error(`Note with ID [${noteId}] not found`);
public getNote(noteId: ID): GraphNote | null {
return this.graph.node(noteId) ?? null;
}
public getAllLinks(noteId: ID): Link[] {
public getNoteByURI(uri: URI): GraphNote | null {
return this.getNote(this.createIdFromURI(uri));
}
public getAllLinks(noteId: ID): GraphConnection[] {
return (this.graph.nodeEdges(noteId) || []).map(edge =>
convertEdgeToLink(edge, this.graph)
this.graph.edge(edge.v, edge.w)
);
}
public getForwardLinks(noteId: ID): Link[] {
public getForwardLinks(noteId: ID): GraphConnection[] {
return (this.graph.outEdges(noteId) || []).map(edge =>
convertEdgeToLink(edge, this.graph)
this.graph.edge(edge.v, edge.w)
);
}
public getBacklinks(noteId: ID): Link[] {
public getBacklinks(noteId: ID): GraphConnection[] {
return (this.graph.inEdges(noteId) || []).map(edge =>
convertEdgeToLink(edge, this.graph)
this.graph.edge(edge.v, edge.w)
);
}
@@ -129,9 +151,3 @@ export class NoteGraph {
this.events.removeAllListeners();
}
}
const convertEdgeToLink = (edge: Edge, graph: Graph): Link => ({
from: edge.v,
to: edge.w,
text: graph.edge(edge.v, edge.w),
});

4
packages/foam-core/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export { Position, Point } from 'unist';
export type URI = string;
export type ID = string;

View File

@@ -1,4 +1,34 @@
import path from 'path';
import crypto from 'crypto';
import { titleCase } from 'title-case';
import GithubSlugger from 'github-slugger';
import { URI, ID } from 'types';
export const hash = (text: string) =>
crypto
.createHash('sha1')
.update(text)
.digest('hex');
export const uriToSlug = (noteUri: URI): string => {
return GithubSlugger.slug(path.parse(noteUri).name);
};
export const hashURI = (uri: URI): ID => {
return hash(path.normalize(uri));
};
export const computeRelativeURI = (
reference: URI,
relativeSlug: string
): URI => {
// if no extension is provided, use the same extension as the source file
const slug =
path.extname(relativeSlug) !== ''
? relativeSlug
: `${relativeSlug}${path.extname(reference)}`;
return path.normalize(path.join(path.dirname(reference), slug));
};
export function dropExtension(path: string): string {
const parts = path.split('.');

View File

@@ -1,189 +1,187 @@
import { NoteGraph, Note } from '../src/note-graph';
import { NoteGraph, NoteLinkDefinition, Note } from '../src/note-graph';
import { uriToSlug } from '../src/utils';
const position = {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
start: { line: 1, column: 1 },
end: { line: 1, column: 1 },
};
const documentStart = position.start;
const documentEnd = position.end;
const eol = '\n';
const createTestNote = (params: {
uri: string;
title?: string;
definitions?: NoteLinkDefinition[];
links?: { slug: string }[];
text?: string;
}): Note => {
return {
properties: {},
title: params.title ?? null,
slug: uriToSlug(params.uri),
definitions: params.definitions ?? [],
links: params.links
? params.links.map(link => ({
type: 'wikilink',
slug: link.slug,
position: position,
text: 'link text',
}))
: [],
source: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
uri: params.uri,
text: params.text ?? '',
},
};
};
describe('Note graph', () => {
it('Adds notes to graph', () => {
const graph = new NoteGraph();
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, eol, '/page-a.md', '')
);
graph.setNote(
new Note('page-b', 'page-b', [], [], documentEnd, eol, '/page-b.md', '')
);
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
);
graph.setNote(createTestNote({ uri: '/page-a.md' }));
graph.setNote(createTestNote({ uri: '/page-b.md' }));
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph
.getNotes()
.map(n => n.id)
.map(n => n.slug)
.sort()
).toEqual(['page-a', 'page-b', 'page-c']);
});
it('Detects forward links', () => {
const graph = new NoteGraph();
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, eol, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
eol,
'/page-b.md',
''
)
);
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
graph.setNote(createTestNote({ uri: '/page-a.md' }));
const noteB = graph.setNote(
createTestNote({
uri: '/page-b.md',
links: [{ slug: 'page-a' }],
})
);
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph
.getForwardLinks('page-b')
.map(link => link.to)
.sort()
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)!.slug)
).toEqual(['page-a']);
});
it('Detects backlinks', () => {
const graph = new NoteGraph();
const noteA = graph.setNote(createTestNote({ uri: '/page-a.md' }));
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, eol, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
eol,
'/page-b.md',
''
)
);
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
createTestNote({
uri: '/page-b.md',
links: [{ slug: 'page-a' }],
})
);
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph
.getBacklinks('page-a')
.map(link => link.from)
.sort()
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)!.slug)
).toEqual(['page-b']);
});
it('Fails when accessing non-existing node', () => {
expect(() => {
const graph = new NoteGraph();
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, eol, '/path-b.md', '')
);
graph.getNote('non-existing');
}).toThrow();
it('Returns null when accessing non-existing node', () => {
const graph = new NoteGraph();
graph.setNote(createTestNote({ uri: 'page-a' }));
expect(graph.getNote('non-existing')).toBeNull();
});
it('Allows adding edges to non-existing documents', () => {
const graph = new NoteGraph();
graph.setNote(
new Note(
'page-a',
'page-a',
[{ to: 'non-existing', text: 'does not exist', position }],
[],
documentEnd,
eol,
'/path-b.md',
''
)
createTestNote({
uri: '/page-a.md',
links: [{ slug: 'non-existing' }],
})
);
expect(graph.getNote('non-existing')).toBeUndefined();
expect(graph.getNote('non-existing')).toBeNull();
});
it('Updates links when modifying note', () => {
const graph = new NoteGraph();
graph.setNote(
new Note('page-a', 'page-a', [], [], documentEnd, eol, '/page-a.md', '')
);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-a', text: 'go', position }],
[],
documentEnd,
eol,
'/page-b.md',
''
)
);
graph.setNote(
new Note('page-c', 'page-c', [], [], documentEnd, eol, '/page-c.md', '')
const noteA = graph.setNote(createTestNote({ uri: '/page-a.md' }));
const noteB = graph.setNote(
createTestNote({
uri: '/page-b.md',
links: [{ slug: 'page-a' }],
})
);
const noteC = graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph
.getForwardLinks('page-b')
.map(link => link.to)
.sort()
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
).toEqual(['page-a']);
expect(
graph
.getBacklinks('page-a')
.map(link => link.from)
.sort()
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
).toEqual(['page-b']);
expect(
graph
.getBacklinks('page-c')
.map(link => link.from)
.sort()
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
).toEqual([]);
graph.setNote(
new Note(
'page-b',
'page-b',
[{ to: 'page-c', text: 'go', position }],
[],
documentEnd,
eol,
'/path-2b.md',
''
)
createTestNote({
uri: '/page-b.md',
links: [{ slug: 'page-c' }],
})
);
expect(
graph
.getForwardLinks('page-b')
.map(link => link.to)
.sort()
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
).toEqual(['page-c']);
expect(
graph
.getBacklinks('page-a')
.map(link => link.from)
.sort()
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
).toEqual([]);
expect(
graph
.getBacklinks('page-c')
.map(link => link.from)
.sort()
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
).toEqual(['page-b']);
});
});
describe('Graph querying', () => {
it('returns empty set if no note is found', () => {
const graph = new NoteGraph();
graph.setNote(createTestNote({ uri: '/page-a.md' }));
expect(graph.getNotes({ slug: 'non-existing' })).toEqual([]);
expect(graph.getNotes({ title: 'non-existing' })).toEqual([]);
});
it('finds the note by slug', () => {
const graph = new NoteGraph();
const note = graph.setNote(createTestNote({ uri: '/page-a.md' }));
expect(graph.getNotes({ slug: note.slug }).length).toEqual(1);
});
it('finds a note by slug when there is more than one', () => {
const graph = new NoteGraph();
graph.setNote(createTestNote({ uri: '/dir1/page-a.md' }));
graph.setNote(createTestNote({ uri: '/dir2/page-a.md' }));
expect(graph.getNotes({ slug: 'page-a' }).length).toEqual(2);
});
it('finds a note by title', () => {
const graph = new NoteGraph();
const note = graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
expect(graph.getNotes({ title: 'My Title' }).length).toEqual(1);
});
it('finds a note by title when there are several', () => {
const graph = new NoteGraph();
graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
graph.setNote(
createTestNote({ uri: '/dir3/page-b.md', title: 'My Title' })
);
expect(graph.getNotes({ title: 'My Title' }).length).toEqual(2);
});
});

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { NoteGraph, Note } from '../../src/note-graph';
import { NoteGraph } from '../../src/note-graph';
import { generateHeading } from '../../src/janitor';
import { initializeNoteGraph } from '../../src/initialize-note-graph';
@@ -11,26 +11,26 @@ describe('generateHeadings', () => {
});
it('should add heading to a file that does not have them', () => {
const note = _graph.getNote('file-without-title') as Note;
const note = _graph.getNotes({ slug: 'file-without-title' })[0];
const expected = {
newText: `# File without Title
`,
range: {
start: {
line: 0,
column: 0,
line: 1,
column: 1,
offset: 0,
},
end: {
line: 0,
column: 0,
line: 1,
column: 1,
offset: 0,
},
},
};
const actual = generateHeading(note!);
const actual = generateHeading(note);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
@@ -38,12 +38,7 @@ describe('generateHeadings', () => {
});
it('should not cause any changes to a file that has a heading', () => {
const note = _graph.getNote('index') as Note;
const expected = null;
const actual = generateHeading(note!);
expect(actual).toEqual(expected);
const note = _graph.getNotes({ slug: 'index' })[0];
expect(generateHeading(note)).toBeNull();
});
});

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { NoteGraph, Note } from '../../src/note-graph';
import { NoteGraph } from '../../src/note-graph';
import { generateLinkReferences } from '../../src/janitor';
import { initializeNoteGraph } from '../../src/initialize-note-graph';
@@ -15,7 +15,7 @@ describe('generateLinkReferences', () => {
});
it('should add link references to a file that does not have them', () => {
const note = _graph.getNote('index') as Note;
const note = _graph.getNotes({ slug: 'index' })[0];
const expected = {
newText: `
[//begin]: # "Autogenerated link references for markdown compatibility"
@@ -37,7 +37,7 @@ describe('generateLinkReferences', () => {
},
};
const actual = generateLinkReferences(note!, _graph, false);
const actual = generateLinkReferences(note, _graph, false);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
@@ -45,7 +45,7 @@ describe('generateLinkReferences', () => {
});
it('should remove link definitions from a file that has them, if no links are present', () => {
const note = _graph.getNote('second-document') as Note;
const note = _graph.getNotes({ slug: 'second-document' })[0];
const expected = {
newText: '',
@@ -63,7 +63,7 @@ describe('generateLinkReferences', () => {
},
};
const actual = generateLinkReferences(note!, _graph, false);
const actual = generateLinkReferences(note, _graph, false);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
@@ -71,7 +71,7 @@ describe('generateLinkReferences', () => {
});
it('should update link definitions if they are present but changed', () => {
const note = _graph.getNote('first-document') as Note;
const note = _graph.getNotes({ slug: 'first-document' })[0];
const expected = {
newText: `[//begin]: # "Autogenerated link references for markdown compatibility"
@@ -91,7 +91,7 @@ describe('generateLinkReferences', () => {
},
};
const actual = generateLinkReferences(note!, _graph, false);
const actual = generateLinkReferences(note, _graph, false);
expect(actual!.range.start).toEqual(expected.range.start);
expect(actual!.range.end).toEqual(expected.range.end);
@@ -99,11 +99,11 @@ describe('generateLinkReferences', () => {
});
it('should not cause any changes if link reference definitions were up to date', () => {
const note = _graph.getNote('third-document') as Note;
const note = _graph.getNotes({ slug: 'third-document' })[0];
const expected = null;
const actual = generateLinkReferences(note!, _graph, false);
const actual = generateLinkReferences(note, _graph, false);
expect(actual).toEqual(expected);
});

View File

@@ -2,7 +2,7 @@ import {
createNoteFromMarkdown,
createMarkdownReferences,
} from '../src/markdown-provider';
import { NoteGraph, Note } from '../src/note-graph';
import { NoteGraph } from '../src/note-graph';
const pageA = `
# Page A
@@ -25,64 +25,128 @@ const pageD = `
This file has no heading.
`;
const pageE = `
---
title: Note Title
date: 20-12-12
---
# Other Note Title
`;
const pageF = `
---
---
# Empty Frontmatter
`;
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
const graph = new NoteGraph();
graph.setNote(createNoteFromMarkdown('page-a', pageA, '\n'));
graph.setNote(createNoteFromMarkdown('page-b', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('page-c', pageC, '\n'));
graph.setNote(createNoteFromMarkdown('/page-a.md', pageA, '\n'));
graph.setNote(createNoteFromMarkdown('/page-b.md', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC, '\n'));
expect(
graph
.getNotes()
.map(n => n.id)
.map(n => n.slug)
.sort()
).toEqual(['page-a', 'page-b', 'page-c']);
});
it('Parses wikilinks correctly', () => {
const graph = new NoteGraph();
graph.setNote(createNoteFromMarkdown('page-a', pageA, '\n'));
graph.setNote(createNoteFromMarkdown('page-b', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('page-c', pageC, '\n'));
const noteA = graph.setNote(
createNoteFromMarkdown('/page-a.md', pageA, '\n')
);
const noteB = graph.setNote(
createNoteFromMarkdown('/page-b.md', pageB, '\n')
);
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC, '\n'));
expect(graph.getBacklinks('page-b').map(link => link.from)).toEqual([
'page-a',
]);
expect(graph.getForwardLinks('page-a').map(link => link.to)).toEqual([
'page-b',
'page-c',
]);
expect(
graph.getBacklinks(noteB.id).map(link => graph.getNote(link.from)!.slug)
).toEqual(['page-a']);
expect(
graph.getForwardLinks(noteA.id).map(link => graph.getNote(link.to)!.slug)
).toEqual(['page-b', 'page-c']);
});
});
describe('Note Title', () => {
it('should initialize note title if heading exists', () => {
const graph = new NoteGraph();
graph.setNote(createNoteFromMarkdown('page-a', pageA, '\n'));
const note = graph.setNote(
createNoteFromMarkdown('/page-a.md', pageA, '\n')
);
const pageANoteTitle = (graph.getNote('page-a') as Note).title;
const pageANoteTitle = graph.getNote(note.id)!.title;
expect(pageANoteTitle).toBe('Page A');
});
it('should not initialize note title if heading does not exist', () => {
const graph = new NoteGraph();
graph.setNote(createNoteFromMarkdown('page-d', pageD, '\n'));
const note = graph.setNote(
createNoteFromMarkdown('/page-d.md', pageD, '\n')
);
const pageANoteTitle = (graph.getNote('page-d') as Note).title;
const pageANoteTitle = graph.getNote(note.id)!.title;
expect(pageANoteTitle).toBe(null);
});
it('should give precedence to frontmatter title over other headings', () => {
const graph = new NoteGraph();
const note = graph.setNote(
createNoteFromMarkdown('/page-e.md', pageE, '\n')
);
const pageENoteTitle = graph.getNote(note.id)!.title;
expect(pageENoteTitle).toBe('Note Title');
});
});
describe('frontmatter', () => {
it('should parse yaml frontmatter', () => {
const graph = new NoteGraph();
const note = graph.setNote(
createNoteFromMarkdown('/page-e.md', pageE, '\n')
);
const expected = {
title: 'Note Title',
date: '20-12-12',
};
const actual: any = graph.getNote(note.id)!.properties;
expect(actual.title).toBe(expected.title);
expect(actual.date).toBe(expected.date);
});
it('should parse empty frontmatter', () => {
const graph = new NoteGraph();
const note = graph.setNote(
createNoteFromMarkdown('/page-f.md', pageF, '\n')
);
const expected = {};
const actual = graph.getNote(note.id)!.properties;
expect(actual).toEqual(expected);
});
});
describe('wikilinks definitions', () => {
it('can generate links without file extension when includeExtension = false', () => {
const graph = new NoteGraph();
const noteA = createNoteFromMarkdown('dir1/page-a.md', pageA, '\n');
const noteB = createNoteFromMarkdown('dir1/page-b.md', pageB, '\n');
const noteC = createNoteFromMarkdown('dir1/page-c.md', pageC, '\n');
graph.setNote(noteA);
graph.setNote(noteB);
graph.setNote(noteC);
const noteA = graph.setNote(
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
);
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC, '\n'));
const noExtRefs = createMarkdownReferences(graph, noteA.id, false);
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
@@ -90,12 +154,11 @@ describe('wikilinks definitions', () => {
it('can generate links with file extension when includeExtension = true', () => {
const graph = new NoteGraph();
const noteA = createNoteFromMarkdown('dir1/page-a.md', pageA, '\n');
const noteB = createNoteFromMarkdown('dir1/page-b.md', pageB, '\n');
const noteC = createNoteFromMarkdown('dir1/page-c.md', pageC, '\n');
graph.setNote(noteA);
graph.setNote(noteB);
graph.setNote(noteC);
const noteA = graph.setNote(
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
);
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC, '\n'));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
@@ -103,12 +166,11 @@ describe('wikilinks definitions', () => {
it('use relative paths', () => {
const graph = new NoteGraph();
const noteA = createNoteFromMarkdown('dir1/page-a.md', pageA, '\n');
const noteB = createNoteFromMarkdown('dir2/page-b.md', pageB, '\n');
const noteC = createNoteFromMarkdown('dir3/page-c.md', pageC, '\n');
graph.setNote(noteA);
graph.setNote(noteB);
graph.setNote(noteC);
const noteA = graph.setNote(
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
);
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB, '\n'));
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC, '\n'));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
expect(extRefs.map(r => r.url)).toEqual([

View File

@@ -0,0 +1,30 @@
import { uriToSlug, hashURI, computeRelativeURI } from '../src/utils';
describe('URI utils', () => {
it('supports various cases', () => {
expect(uriToSlug('/this/is/a/path.md')).toEqual('path');
expect(uriToSlug('../a/relative/path.md')).toEqual('path');
expect(uriToSlug('another/relative/path.md')).toEqual('path');
expect(uriToSlug('no-directory.markdown')).toEqual('no-directory');
expect(uriToSlug('many.dots.name.markdown')).toEqual('manydotsname');
});
it('normalizes URI before hashing', () => {
expect(hashURI('/this/is/a/path.md')).toEqual(
hashURI('/this/has/../is/a/path.md')
);
expect(hashURI('this/is/a/path.md')).toEqual(
hashURI('this/has/../is/a/path.md')
);
});
it('computes a relative uri using a slug', () => {
expect(computeRelativeURI('/my/file.md', '../hello.md')).toEqual(
'/hello.md'
);
expect(computeRelativeURI('/my/file.md', '../hello')).toEqual('/hello.md');
expect(computeRelativeURI('/my/file.markdown', '../hello')).toEqual(
'/hello.markdown'
);
});
});

View File

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

View File

@@ -1,34 +1,29 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "src",
"composite": true,
"esModuleInterop": true,
"importHelpers": 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": [
"esnext"
]
},
"include": [
"src",
"types"
],
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
// to override config from tsconfig.base.json
"outDir": "dist",
"rootDir": "./src",
// for references
"baseUrl": "src",
"lib": [
"esnext"
],
"module": "esnext",
"importHelpers": true,
"sourceMap": true,
"strict": true,
// "noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"esModuleInterop": true
// "paths": {
// "*": ["src/*", "node_modules/*"]
// },
// "jsx": "react",
},
]
}

View File

@@ -1,7 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View File

@@ -1,40 +0,0 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
// This task is also defined in ${workspaceFolder}/.vscode/launch.json
// for when running in a monorepo environment
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
// This task is also defined in ${workspaceFolder}/.vscode/launch.json
// for when running in a monorepo environment
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

View File

@@ -1,11 +0,0 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

View File

@@ -1,22 +0,0 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
// This task is also defined in ${workspaceFolder}/.vscode/tasks.json
// for when running in a monorepo environment
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -0,0 +1,16 @@
/*
Note: this is needed in order to test certain parts
of functionality of `foam-vscode`
Following the advice from this article:
https://www.richardkotze.com/coding/unit-test-mock-vs-code-extension-api-jest
combined with advice from this GitHub issue comment:
https://github.com/microsoft/vscode-test/issues/37#issuecomment-584744386
*/
const vscode = {
// Add values and methods as needed for tests
};
module.exports = vscode;

View File

@@ -0,0 +1,6 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript"
]
};

View File

@@ -0,0 +1,188 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/p6/5y5l8tbs1d32pq9b596lk48h0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// 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"
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@@ -88,11 +88,12 @@
]
},
"scripts": {
"clean": "rimraf out",
"build": "tsc -p ./",
"test": "echo No tests in VSCode extensions yet",
"test": "jest",
"lint": "eslint src --ext ts",
"watch": "tsc -watch -p ./",
"clean": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --clean",
"watch": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
"vscode:prepublish": "yarn npm-install && yarn run build",
"npm-install": "rimraf node_modules && npm i",
"npm-cleanup": "rimraf package-lock.json node_modules && yarn",
@@ -100,14 +101,19 @@
"publish-extension": "npx vsce publish && yarn npm-cleanup"
},
"devDependencies": {
"@babel/core": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-typescript": "^7.10.4",
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/node": "^13.11.0",
"@types/vscode": "^1.45.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",
"jest": "^26.2.2",
"rimraf": "^3.0.2",
"typescript": "^3.8.3",
"vscode-test": "^1.3.0"

View File

@@ -3,8 +3,8 @@ import {
workspace,
ExtensionContext,
commands,
Position,
Range
Range,
ProgressLocation
} from "vscode";
import fs = require("fs");
import { FoamFeature } from "../types";
@@ -12,11 +12,11 @@ import {
applyTextEdit,
generateLinkReferences,
generateHeading,
Foam,
Note
Foam
} from "foam-core";
import { includeExtensions } from "../settings";
import { astPositionToVsCodePosition } from "../utils";
const feature: FoamFeature = {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
@@ -30,18 +30,29 @@ const feature: FoamFeature = {
async function janitor(foam: Foam) {
try {
const outcome = await runJanitor(foam);
if (outcome.processedFileCount === 0) {
window.showInformationMessage(
"Foam Janitor didn't file any notes to clean up"
const noOfFiles = foam.notes.getNotes().filter(Boolean).length;
if (noOfFiles === 0) {
return window.showInformationMessage(
"Foam Janitor didn't find any notes to clean up."
);
} else if (!outcome.changedAnyFiles) {
}
const outcome = await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Running Foam Janitor across ${noOfFiles} files!`
},
() => runJanitor(foam)
);
if (!outcome.changedAnyFiles) {
window.showInformationMessage(
`Foam Janitor checked ${outcome.processedFileCount} files, and found nothing to clean up!`
`Foam Janitor checked ${noOfFiles} files, and found nothing to clean up!`
);
} else {
window.showInformationMessage(
`Foam Janitor checked ${outcome.processedFileCount} files and updated ${outcome.updatedDefinitionListCount} out-of-date definition lists and added ${outcome.updatedHeadingCount} missing headings. Please check the changes before committing them into version control!`
`Foam Janitor checked ${noOfFiles} files and updated ${outcome.updatedDefinitionListCount} out-of-date definition lists and added ${outcome.updatedHeadingCount} missing headings. Please check the changes before committing them into version control!`
);
}
} catch (e) {
@@ -54,7 +65,6 @@ async function janitor(foam: Foam) {
async function runJanitor(foam: Foam) {
const notes = foam.notes.getNotes().filter(Boolean);
let processedFileCount = 0;
let updatedHeadingCount = 0;
let updatedDefinitionListCount = 0;
@@ -69,22 +79,19 @@ async function runJanitor(foam: Foam) {
dirtyTextDocument => dirtyTextDocument.fileName
);
const dirtyNotes: Note[] = notes.filter(note =>
dirtyEditorsFileName.includes(note.path)
const dirtyNotes = notes.filter(note =>
dirtyEditorsFileName.includes(note.source.uri)
);
const nonDirtyNotes: Note[] = notes.filter(
note => !dirtyEditorsFileName.includes(note.path)
const nonDirtyNotes = notes.filter(
note => !dirtyEditorsFileName.includes(note.source.uri)
);
// Apply Text Edits to Non Dirty Notes using fs module just like CLI
const fileWritePromises = nonDirtyNotes.map(note => {
processedFileCount += 1;
let heading = generateHeading(note);
if (heading) {
console.log("fs.write heading " + note.path + " " + note.title);
updatedHeadingCount += 1;
}
@@ -104,11 +111,11 @@ async function runJanitor(foam: Foam) {
// Apply Edits
// Note: The ordering matters. Definitions need to be inserted
// before heading, since inserting a heading changes line numbers below
let text = note.source;
let text = note.source.text;
text = definitions ? applyTextEdit(text, definitions) : text;
text = heading ? applyTextEdit(text, heading) : text;
return fs.promises.writeFile(note.path, text);
return fs.promises.writeFile(note.source.uri, text);
});
await Promise.all(fileWritePromises);
@@ -116,10 +123,10 @@ async function runJanitor(foam: Foam) {
// Handle dirty editors in serial, as VSCode only allows
// edits to be applied to active text editors
for (const doc of dirtyTextDocuments) {
processedFileCount += 1;
const editor = await window.showTextDocument(doc);
const note = dirtyNotes.find(n => n.path === editor.document.fileName);
const note = dirtyNotes.find(
n => n.source.uri === editor.document.fileName
);
// Get edits
const heading = generateHeading(note);
@@ -136,27 +143,17 @@ async function runJanitor(foam: Foam) {
// before heading, since inserting a heading changes line numbers below
if (definitions) {
updatedDefinitionListCount += 1;
const start = new Position(
definitions.range.start.line - 1,
definitions.range.start.column
);
const end = new Position(
definitions.range.end.line - 1,
definitions.range.end.column
);
const start = astPositionToVsCodePosition(definitions.range.start);
const end = astPositionToVsCodePosition(definitions.range.end);
const range = new Range(start, end);
editBuilder.replace(range, definitions!.newText);
}
if (heading) {
console.log("editor.write heading " + note.title);
updatedHeadingCount += 1;
const start = new Position(
heading.range.start.line,
heading.range.start.column
);
editBuilder.insert(start, heading.newText);
const start = astPositionToVsCodePosition(heading.range.start);
editBuilder.replace(start, heading.newText);
}
});
}
@@ -165,7 +162,6 @@ async function runJanitor(foam: Foam) {
return {
updatedHeadingCount,
updatedDefinitionListCount,
processedFileCount,
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount
};
}

View File

@@ -85,7 +85,7 @@ async function createReferenceList(foam: NoteGraph) {
let refs = await generateReferenceList(foam, editor.document);
if (refs && refs.length) {
await editor.edit(function (editBuilder) {
await editor.edit(function(editBuilder) {
if (editor) {
const spacing = hasEmptyTrailing
? docConfig.eol
@@ -128,17 +128,25 @@ async function updateReferenceList(foam: NoteGraph) {
}
}
async function generateReferenceList(
foam: NoteGraph,
doc: TextDocument
): Promise<string[]> {
const filePath = doc.fileName;
const id = dropExtension(basename(filePath));
const note = foam.getNoteByURI(filePath);
// Should never happen as `doc` is usually given by `editor.document`, which
// binds to an opened note.
if (!note) {
console.warn(
`Can't find note for URI ${filePath} before attempting to generate its markdown reference list`
);
return [];
}
const references = uniq(
createMarkdownReferences(foam, id, includeExtensions()).map(
createMarkdownReferences(foam, note.id, includeExtensions()).map(
stringifyMarkdownLinkReferenceDefinition
)
);

View File

@@ -1,4 +1,17 @@
import { EndOfLine, Range, TextDocument, window, Position, TextEditor } from "vscode";
import {
EndOfLine,
Range,
TextDocument,
window,
Position,
TextEditor
} from "vscode";
interface Point {
line: number;
column: number;
offset?: number;
}
export const docConfig = { tab: " ", eol: "\r\n" };
@@ -69,3 +82,12 @@ export function dropExtension(path: string): string {
parts.pop();
return parts.join(".");
}
/**
*
* @param point ast position (1-indexed)
* @returns VSCode position (0-indexed)
*/
export const astPositionToVsCodePosition = (point: Point): Position => {
return new Position(point.line - 1, point.column - 1);
};

View File

@@ -0,0 +1,10 @@
// @note: This will fail due to utils importing 'vscode'
// which needs to be mocked in the jest test environment.
// See: https://github.com/microsoft/vscode-test/issues/37
import { dropExtension } from '../src/utils';
describe("dropExtension", () => {
test("returns file name without extension", () => {
expect(dropExtension('file.md')).toEqual('file');
});
});

View File

@@ -1,22 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"esModuleInterop": true,
"outDir": "out",
"lib": ["es6"],
"sourceMap": true,
"rootDir": "src",
"strict": false /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"strict": false
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"],
"references": [{
"path": "../foam-core/tsconfig.json"
"path": "../foam-core"
}]
}

View File

@@ -7,7 +7,7 @@
# Foam
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
@@ -99,6 +99,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
</tr>
</table>

831
yarn.lock

File diff suppressed because it is too large Load Diff