mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e624d5cf9 | ||
|
|
42ec29d3e9 | ||
|
|
26ab27e06f | ||
|
|
6073bf143e | ||
|
|
865bb95745 | ||
|
|
fa908bb4c6 | ||
|
|
83afa873dc | ||
|
|
69c3f5fb25 | ||
|
|
6152e89590 | ||
|
|
9f17b1f7b9 | ||
|
|
8f1327337c | ||
|
|
b15f27aea6 | ||
|
|
b8a16cc5ed | ||
|
|
168ef5edb4 | ||
|
|
788ccbd905 | ||
|
|
cdaeefb252 | ||
|
|
ddbf365313 | ||
|
|
1e327a4cc6 | ||
|
|
391f4d6d07 | ||
|
|
befdeb70e0 | ||
|
|
a086a75e37 | ||
|
|
9b886d3b27 | ||
|
|
ecfa04cc4b | ||
|
|
08e51fcbc8 | ||
|
|
15f412cac4 | ||
|
|
d054e19eae | ||
|
|
846908e9d2 | ||
|
|
da69cc0f5d | ||
|
|
76a9a4ac93 | ||
|
|
138217e39d | ||
|
|
b473749260 | ||
|
|
fa01cce934 | ||
|
|
44e498dddb | ||
|
|
679a2947d2 | ||
|
|
8710438a46 | ||
|
|
90f869e8d0 | ||
|
|
8a7e9bcdd4 | ||
|
|
e68b6e3023 | ||
|
|
ba84b9b496 | ||
|
|
af5a4a20e6 | ||
|
|
38181acd53 | ||
|
|
683c28e393 | ||
|
|
f9444636e2 | ||
|
|
fd0b2ef912 | ||
|
|
b911d5b7b1 | ||
|
|
7287aa62b5 | ||
|
|
0475d26f2c | ||
|
|
bf43113fac | ||
|
|
81639fd650 | ||
|
|
8d110eb04b | ||
|
|
d89b6f0285 | ||
|
|
dbdb4c30b8 | ||
|
|
fdc2c7cf4c | ||
|
|
b0536ce9f7 | ||
|
|
a60dcaa52a | ||
|
|
799caa96a6 | ||
|
|
fb2ff3ac95 | ||
|
|
39a96a2d02 | ||
|
|
6e5c138f31 | ||
|
|
b2b1b58262 | ||
|
|
c10b73c59c | ||
|
|
74591eb192 | ||
|
|
b2ebd82f25 | ||
|
|
b2c4e9f78b | ||
|
|
b22fd50394 |
@@ -212,7 +212,8 @@
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/8980971?v=4",
|
||||
"profile": "https://sanketdg.github.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -406,6 +407,125 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ingalless",
|
||||
"name": "ingalless",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/22981941?v=4",
|
||||
"profile": "https://ingalless.com",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jmg-duarte",
|
||||
"name": "José Duarte",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/15343819?v=4",
|
||||
"profile": "http://jmg-duarte.github.io",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yenly",
|
||||
"name": "Yenly",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6759658?v=4",
|
||||
"profile": "https://www.yenly.wtf",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hikerpig",
|
||||
"name": "hikerpig",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2259688?v=4",
|
||||
"profile": "https://www.hikerpig.cn",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Sigfried",
|
||||
"name": "Sigfried Gold",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1586931?v=4",
|
||||
"profile": "http://sigfried.org",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tristansokol",
|
||||
"name": "Tristan Sokol",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/867661?v=4",
|
||||
"profile": "http://www.tristansokol.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "umbrellait-danil-rodin",
|
||||
"name": "Danil Rodin",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/49779373?v=4",
|
||||
"profile": "https://umbrellait.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "scott-joe",
|
||||
"name": "Scott Williams",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2026866?v=4",
|
||||
"profile": "https://www.linkedin.com/in/scottjoewilliams/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Jackiexiao",
|
||||
"name": "jackiexiao",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/18050469?v=4",
|
||||
"profile": "https://jackiexiao.github.io/blog",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jbn",
|
||||
"name": "John B Nelson",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/78835?v=4",
|
||||
"profile": "https://generativist.substack.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "asifm",
|
||||
"name": "Asif Mehedi",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/3958387?v=4",
|
||||
"profile": "https://github.com/asifm",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "litanlitudan",
|
||||
"name": "Tan Li",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/4970420?v=4",
|
||||
"profile": "https://github.com/litanlitudan",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ShaunaGordon",
|
||||
"name": "Shauna Gordon",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/579361?v=4",
|
||||
"profile": "http://shaunagordon.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
14
.github/CONTRIBUTING.md
vendored
14
.github/CONTRIBUTING.md
vendored
@@ -1,14 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Hello, friend.
|
||||
|
||||
This repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) managed by [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/).
|
||||
|
||||
- The [packages](packages/) directory contains all Foam core code packages
|
||||
- The [docs](docs/) directory contains a Foam workspace that hosts the official [documentation site](https://foambubble.github.io/foam)
|
||||
|
||||
The foam starter template lives outside of this repository at [foambubble/foam-template](https://github.com/foambubble/foam-template).
|
||||
|
||||
See [Foam Contribution Guide](https://foambubble.github.io/foam/contribution-guide) on the rendered Foam workspace for more information on how to contribute to Foam.
|
||||
|
||||
Thank you for your interest!
|
||||
33
.github/ISSUE_TEMPLATE/bug.md
vendored
33
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -1,32 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us be foamier
|
||||
labels: bug, awaiting triage
|
||||
title: [BUG]
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- Check in the VSCode extension tab. -->
|
||||
- Foam version:
|
||||
|
||||
**Summary**
|
||||
<!-- A clear and concise description of what the bug is.-->
|
||||
|
||||
**Affected package**
|
||||
<!-- Its ok if you don't know! -->
|
||||
- [ ] `foam-cli`
|
||||
- [ ] `foam-core`
|
||||
- [ ] `foam-vscode`
|
||||
- [ ] `other/meta/???`
|
||||
**Steps to reproduce**
|
||||
1.
|
||||
2.
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
Does this issue occur on the [foam template](https://github.com/foambubble/foam-template) repo? Yes/No
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
**Additional information**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
*Feel free to attach a screenshot and/or include a zip with a minimal repo to reproduce the issue*
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://discord.gg/8c4BChMfSu
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
6
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea to help us be foamier
|
||||
---
|
||||
|
||||
<!-- Describe the feature you'd like. -->
|
||||
48
.github/workflows/ci.yml
vendored
Normal file
48
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
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
|
||||
pull_request:
|
||||
|
||||
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
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Check Lint Rules
|
||||
run: yarn lint
|
||||
|
||||
test:
|
||||
name: Build and Test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-10.15, ubuntu-18.04] # add windows-2019 after fixing tests for it
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Build Packages
|
||||
run: yarn build
|
||||
- name: Run Tests
|
||||
run: yarn test
|
||||
25
.github/workflows/foam-cli.yml
vendored
25
.github/workflows/foam-cli.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Test foam-cli
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/foam-cli/**'
|
||||
push:
|
||||
paths:
|
||||
- 'packages/foam-cli/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
# - name: Lint foam-lint
|
||||
# run: yarn workspace foam-cli lint
|
||||
|
||||
- name: Test foam-cli
|
||||
run: yarn workspace foam-cli test
|
||||
25
.github/workflows/foam-core.yml
vendored
25
.github/workflows/foam-core.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Test foam-core
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/foam-core/**'
|
||||
push:
|
||||
paths:
|
||||
- 'packages/foam-core/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Lint foam-core
|
||||
run: yarn workspace foam-core lint
|
||||
|
||||
- name: Test foam-core
|
||||
run: yarn workspace foam-core test
|
||||
29
.github/workflows/foam-vscode.yml
vendored
29
.github/workflows/foam-vscode.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Test foam-vscode
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/foam-vscode/**'
|
||||
push:
|
||||
paths:
|
||||
- 'packages/foam-vscode/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Lint foam-vscode
|
||||
run: yarn workspace foam-vscode lint
|
||||
- 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
|
||||
# with:
|
||||
# vsce_token: ${{ secrets.VSCE_TOKEN }}
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -7,9 +7,15 @@
|
||||
},
|
||||
"search.exclude": {
|
||||
// set this to false to include compiled JS folders in search results
|
||||
"packages/**/out": true,
|
||||
"packages/**/out": true,
|
||||
"packages/**/dist": true
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"foam.files.ignore": [
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
]
|
||||
}
|
||||
21
docs/architecture.md
Normal file
21
docs/architecture.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
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/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.
|
||||
- [/packages/foam-cli](https://github.com/foambubble/foam/tree/master/packages/foam-cli) - The Foam CLI tool.
|
||||
|
||||
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.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: recipes.md "Recipes"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -1,7 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Head over to the [[contribution-guide]]. `CONTRIBUTING.md` file name is blocklisted on GitHub pages, and doesn't appear in the [rendered output](https://foambubble.github.io/foam).
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[contribution-guide]: contribution-guide.md "Contribution Guide"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -1,25 +1,28 @@
|
||||
---
|
||||
tags: todo, good-first-task
|
||||
---
|
||||
# Contribution Guide
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
|
||||
|
||||
> [[todo]] [[good-first-task]] This contribution guide itself could be improved 😅
|
||||
## Getting Up To Speed
|
||||
Before you start contributing we recommend that you read the following links:
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback. Here are some general tips on how to get started on contributing to Foam:
|
||||
- [[principles]] - This document describes the guiding principles behind Foam.
|
||||
- [[code-of-conduct]] - Rules we hope every contributor aims to follow, allowing everyone to participate in our community!
|
||||
|
||||
- Use Foam for yourself, figure out what could be improved.
|
||||
- Check out [[roadmap]] to see what's already in the plans. I have thoughts about how to implement some of these, but open to ideas and code contributions!
|
||||
- Read about our [[principles]] to understand Foam's philosophy and direction
|
||||
- Read and act in accordance with our [[code-of-conduct]].
|
||||
- Feel free to open [GitHub issues](https://github.com/foambubble/foam/issues) to give me feedback and ideas for new features.
|
||||
- Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/)
|
||||
- [/docs](https://github.com/foambubble/foam/docs): documentation and [[recipes]]
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode): the core VSCode plugin
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core): powers the core functionality in Foam across all platforms
|
||||
- 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.
|
||||
## Diving In
|
||||
We understand that diving in an unfamiliar codebase may seem scary,
|
||||
to make it easier for new contributors we provide some resources:
|
||||
- [[roadmap]] - You can read our roadmap to see what is coming to Foam, many of these are open to suggestions!
|
||||
- [[architecture]] - This document describes the architecture of Foam and how the repository is structured.
|
||||
|
||||
## Contributing to the VS Code Extension
|
||||
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://discord.gg/rtdZKgj)!
|
||||
|
||||
If you're interested in contributing to the VS Code extension (aka `foam-vscode`), this guide will help you get things set up locally.
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing, this short guide will help you get things set up locally.
|
||||
|
||||
1. Clone the repo locally:
|
||||
|
||||
@@ -29,25 +32,36 @@ If you're interested in contributing to the VS Code extension (aka `foam-vscode`
|
||||
|
||||
`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. 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:
|
||||
|
||||
`yarn workspace foam-core build`
|
||||
`yarn build`
|
||||
|
||||
4. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/master/.vscode/launch.json) to start a new extension host of VS Code. From the root, or the `foam-vscode` workspace, press f5.
|
||||
5. In the new extension host of VS Code that launched, open a Foam workspace (e.g. your personal one, or a test-specific one created from foam-template). This is strictly not necessary, but the extension won't auto-run unless it's in a workspace with a `.vscode/foam.json` file.
|
||||
6. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
|
||||
You should now be ready to start working!
|
||||
|
||||
### The VS Code Extension
|
||||
|
||||
This guide assumes you read the previous instructions and you're set up to work on Foam.
|
||||
|
||||
1. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/master/.vscode/launch.json) to start a new extension host of VS Code. From the root, or the `foam-vscode` workspace, press f5.
|
||||
|
||||
2. In the new extension host of VS Code that launched, open a Foam workspace (e.g. your personal one, or a test-specific one created from [foam-template](https://github.com/foambubble/foam-template)). This is strictly not necessary, but the extension won't auto-run unless it's in a workspace with a `.vscode/foam.json` file.
|
||||
|
||||
3. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
|
||||
|
||||
For more resources related to the VS Code Extension, check out the links below:
|
||||
|
||||
- [[tutorial-adding-a-new-command-to-the-vs-code-extension]]
|
||||
|
||||
---
|
||||
|
||||
Feel free to modify and submit a PR if this guide is out-of-date or contains errors!
|
||||
|
||||
---
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[good-first-task]: good-first-task.md "Good First Task"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[principles]: principles.md "Principles"
|
||||
[code-of-conduct]: code-of-conduct.md "Code of Conduct"
|
||||
[recipes]: recipes.md "Recipes"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[architecture]: architecture.md "Architecture"
|
||||
[tutorial-adding-a-new-command-to-the-vs-code-extension]: tutorial-adding-a-new-command-to-the-vs-code-extension.md "Tutorial: Adding a New Command to the VS Code Extension"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
25
docs/foam-gatsby-template.md
Normal file
25
docs/foam-gatsby-template.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Foam Gatsby Template
|
||||
|
||||
You can use [foam-gatsby-template](https://github.com/mathieudutour/foam-gatsby-template) to generate a static site to host it online on Github or [Vercel](https://vercel.com).
|
||||
|
||||
## Publishing your foam to Github pages
|
||||
It comes configured with Github actions to auto deploy to Github pages when changes are pushed to your main branch.
|
||||
|
||||
## Publishing your foam to Vercel
|
||||
|
||||
When you're ready to publish, run a local build.
|
||||
```bash
|
||||
cd _layouts
|
||||
npm run build
|
||||
```
|
||||
|
||||
Remove `public` from your .gitignore file then commit and push your public folder in `_layouts` to Github.
|
||||
|
||||
Log into your Vercel account. (Create one if you don't have it already.)
|
||||
|
||||
Import your project. Select `_layouts/public` as your root directory and click **Continue**. Then name your project and click **Deploy**.
|
||||
|
||||
That's it!
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ To start using GistPad for your Foam-based knowledge base, simply perform the fo
|
||||
|
||||
1. Download the [GistPad extension](https://aka.ms/gistpad) and then re-start Visual Studio Code
|
||||
|
||||
1. Run the `GistPad: Sign In` command, and provide a [GitHub token](https://github.com/settings/tokens/new) that includes the `repo` scope (and optionally `gist` and `delete_repo` scope, if you'd like to use GistPad for managing your GitHub content more holistically)
|
||||
1. Run the `GistPad: Sign In` command and then complete the authentication flow using your GitHub account
|
||||
|
||||
1. Run the `GistPad: Manage Repository` command and select the `Create repo from template...` or `Create private repo from template...` depending on your preference
|
||||
1. Run the `GistPad: Open Repository` command and select the `Create repo from template...` or `Create private repo from template...` depending on your preference
|
||||
|
||||
1. Select the `Foam-style wiki` template, and then specify a name for your Foam workspace (e.g. `my-foam-notes`, `johns-knowledge-base`)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ There are many other templates which also support publish your foam workspace to
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://blog.hikerpig.cn/wiki/)
|
||||
* [demo-website](https://wiki.hikerpig.cn/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ 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/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
@@ -165,6 +165,21 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -59,11 +59,15 @@ Guides, tips and strategies for getting the most out of your Foam workspace!
|
||||
|
||||
## Publish
|
||||
|
||||
- Publish to [[github-pages]]
|
||||
- Publish to [[gitlab-pages]]
|
||||
- Publish your site with [[eleventy-and-netlify]]
|
||||
- Publish to [[azure-devops-wiki]]
|
||||
- Publish to [[vercel]]
|
||||
- Publish using official Foam template
|
||||
- Publish to [[github-pages]]
|
||||
- Publish to [[gitlab-pages]]
|
||||
- Publish to [[azure-devops-wiki]]
|
||||
- Publish to [[vercel]]
|
||||
- Publish using community templates
|
||||
- [[eleventy-and-netlify]] by [@juanfrank77](https://github.com/juanfrank77)
|
||||
- [[foam-gatsby-template]] by [@mathieudutour](https://github.com/mathieudutour)
|
||||
- [foamy-nextjs](https://github.com/yenly/foamy-nextjs) by [@yenly](https://github.com/yenly)
|
||||
- Make the site your own by [[customising-styles]].
|
||||
- Render math symbols, by either
|
||||
- adding client-side [[math-support]] to the default [[github-pages]] site
|
||||
@@ -124,4 +128,5 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[math-support]: math-support.md "Math Support"
|
||||
[katex-math-rendering]: katex-math-rendering.md "Katex Math Rendering"
|
||||
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
|
||||
[foam-gatsby-template]: foam-gatsby-template.md "Foam Gatsby Template"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -10,3 +10,16 @@ This list is subject to change. Especially the Git ones.
|
||||
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
|
||||
- [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
|
||||
|
||||
## Extensions For Additional Features
|
||||
|
||||
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
|
||||
|
||||
- [Emojisense](https://marketplace.visualstudio.com/items?itemName=bierner.emojisense)
|
||||
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)
|
||||
- [Mermaid Support for Preview](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
|
||||
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
|
||||
- [VSCode PDF Viewing](https://marketplace.visualstudio.com/items?itemName=tomoki1207.pdf)
|
||||
- [Markdown Extended](https://marketplace.visualstudio.com/items?itemName=jebbs.markdown-extended) (with `kbd` option disabled, `kbd` turns wiki-links into non-clickable buttons)
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc) (easy version management via git auto commits)
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.4.0"
|
||||
"version": "0.7.2"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"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",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "lerna run test",
|
||||
@@ -25,7 +28,7 @@
|
||||
"lerna": "^3.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -19,7 +19,7 @@ $ npm install -g foam-cli
|
||||
$ foam COMMAND
|
||||
running command...
|
||||
$ foam (-v|--version|version)
|
||||
foam-cli/0.4.0 darwin-x64 node-v10.19.0
|
||||
foam-cli/0.7.2 darwin-x64 node-v12.18.2
|
||||
$ foam --help [COMMAND]
|
||||
USAGE
|
||||
$ foam COMMAND
|
||||
@@ -65,7 +65,7 @@ EXAMPLE
|
||||
$ foam-cli janitor path-to-foam-workspace
|
||||
```
|
||||
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.4.0/src/commands/janitor.ts)_
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.7.2/src/commands/janitor.ts)_
|
||||
|
||||
## `foam migrate [WORKSPACEPATH]`
|
||||
|
||||
@@ -84,7 +84,7 @@ EXAMPLE
|
||||
Successfully generated link references and heading!
|
||||
```
|
||||
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.4.0/src/commands/migrate.ts)_
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.7.2/src/commands/migrate.ts)_
|
||||
<!-- commandsstop -->
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-cli",
|
||||
"description": "Foam CLI",
|
||||
"version": "0.4.0",
|
||||
"version": "0.7.2",
|
||||
"author": "Jani Eväkallio @jevakallio",
|
||||
"bin": {
|
||||
"foam": "./bin/run"
|
||||
@@ -11,7 +11,7 @@
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"foam-core": "^0.4.0",
|
||||
"foam-core": "^0.7.2",
|
||||
"ora": "^4.0.4",
|
||||
"tslib": "^1"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"foam-core": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { isValidDirectory } from '../utils';
|
||||
@@ -38,8 +40,11 @@ export default class Janitor extends Command {
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const graph = (await bootstrap(createConfigFromFolders(workspacePath)))
|
||||
.notes;
|
||||
const config = createConfigFromFolders([workspacePath]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
const notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
generateHeading,
|
||||
getKebabCaseFileName,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { renameFile } from '../utils/rename-file';
|
||||
@@ -40,10 +42,13 @@ Successfully generated link references and heading!
|
||||
const { args, flags } = this.parse(Migrate);
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
const config = createConfigFromFolders(workspacePath);
|
||||
const config = createConfigFromFolders([workspacePath]);
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
let graph = (await bootstrap(config)).notes;
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
let graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
let notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
@@ -72,7 +77,7 @@ Successfully generated link references and heading!
|
||||
spinner.text = 'Renaming files';
|
||||
|
||||
// Reinitialize the graph after renaming files
|
||||
graph = (await bootstrap(config)).notes;
|
||||
graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
notes = graph.getNotes().filter(Boolean); // remove undefined notes
|
||||
|
||||
|
||||
3
packages/foam-core/.babelrc.js
Normal file
3
packages/foam-core/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [['@babel/plugin-transform-runtime', { helpers: false }]],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "foam-core",
|
||||
"author": "Jani Eväkallio",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.4.0",
|
||||
"version": "0.7.2",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -19,6 +19,8 @@
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/graphlib": "^2.1.6",
|
||||
"@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",
|
||||
@@ -30,6 +32,7 @@
|
||||
"glob": "^7.1.6",
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.19",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-wiki-link": "^0.0.4",
|
||||
|
||||
@@ -1,59 +1,48 @@
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { createGraph, NoteGraphAPI } from './note-graph';
|
||||
import { createGraph } from './note-graph';
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam } from './index';
|
||||
import { FoamConfig, Foam, Services } from './index';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { isNotNull } from './utils';
|
||||
import { NoteParser } from './types';
|
||||
import { isSome } from './utils';
|
||||
import { isDisposable } from './common/lifecycle';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
const loadNoteGraph = (
|
||||
graph: NoteGraphAPI,
|
||||
parser: NoteParser,
|
||||
files: string[]
|
||||
) => {
|
||||
return Promise.all(
|
||||
files.map(f => {
|
||||
return fs.promises.readFile(f).then(data => {
|
||||
const markdown = (data || '').toString();
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
graph.setNote(parser.parse(f, markdown, eol));
|
||||
});
|
||||
})
|
||||
).then(() => graph);
|
||||
};
|
||||
|
||||
export const bootstrap = async (config: FoamConfig) => {
|
||||
export const bootstrap = async (config: FoamConfig, services: Services) => {
|
||||
const plugins = await loadPlugins(config);
|
||||
const middlewares = plugins
|
||||
.map(p => p.graphMiddleware || null)
|
||||
.filter(isNotNull);
|
||||
const parserPlugins = plugins.map(p => p.parser || null).filter(isNotNull);
|
||||
|
||||
const parserPlugins = plugins.map(p => p.parser).filter(isSome);
|
||||
const parser = createMarkdownParser(parserPlugins);
|
||||
const files = await Promise.all(
|
||||
config.workspaceFolders.map(folder => {
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
|
||||
const graphMiddlewares = plugins.map(p => p.graphMiddleware).filter(isSome);
|
||||
const graph = createGraph(graphMiddlewares);
|
||||
|
||||
const files = await services.dataStore.listFiles();
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
graph.setNote(parser.parse(uri, content));
|
||||
}
|
||||
return findAllFiles(`${folder}/**/*.md`, {});
|
||||
})
|
||||
);
|
||||
|
||||
const graph = await loadNoteGraph(
|
||||
createGraph(middlewares),
|
||||
parser,
|
||||
([] as string[]).concat(...files)
|
||||
);
|
||||
services.dataStore.onDidChange(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
graph.setNote(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidCreate(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
graph.setNote(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidDelete(async uri => {
|
||||
const note = graph.getNoteByURI(uri);
|
||||
note && graph.deleteNote(note.id);
|
||||
});
|
||||
|
||||
return {
|
||||
notes: graph,
|
||||
config: config,
|
||||
parse: parser.parse,
|
||||
dispose: () => {
|
||||
isDisposable(services.dataStore) && services.dataStore.dispose();
|
||||
},
|
||||
} as Foam;
|
||||
};
|
||||
|
||||
159
packages/foam-core/src/common/cancellation.ts
Normal file
159
packages/foam-core/src/common/cancellation.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
import { Emitter, Event } from './event';
|
||||
import { IDisposable } from './lifecycle';
|
||||
|
||||
export interface CancellationToken {
|
||||
/**
|
||||
* A flag signalling is cancellation has been requested.
|
||||
*/
|
||||
readonly isCancellationRequested: boolean;
|
||||
|
||||
/**
|
||||
* An event which fires when cancellation is requested. This event
|
||||
* only ever fires `once` as cancellation can only happen once. Listeners
|
||||
* that are registered after cancellation will be called (next event loop run),
|
||||
* but also only once.
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
readonly onCancellationRequested: (
|
||||
listener: (e: any) => any,
|
||||
thisArgs?: any,
|
||||
disposables?: IDisposable[]
|
||||
) => IDisposable;
|
||||
}
|
||||
|
||||
const shortcutEvent: Event<any> = Object.freeze(function(
|
||||
callback,
|
||||
context?
|
||||
): IDisposable {
|
||||
const handle = setTimeout(callback.bind(context), 0);
|
||||
return {
|
||||
dispose() {
|
||||
clearTimeout(handle);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export namespace CancellationToken {
|
||||
export function isCancellationToken(
|
||||
thing: unknown
|
||||
): thing is CancellationToken {
|
||||
if (
|
||||
thing === CancellationToken.None ||
|
||||
thing === CancellationToken.Cancelled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (thing instanceof MutableToken) {
|
||||
return true;
|
||||
}
|
||||
if (!thing || typeof thing !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof (thing as CancellationToken).isCancellationRequested ===
|
||||
'boolean' &&
|
||||
typeof (thing as CancellationToken).onCancellationRequested === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export const None: CancellationToken = Object.freeze({
|
||||
isCancellationRequested: false,
|
||||
onCancellationRequested: Event.None,
|
||||
});
|
||||
|
||||
export const Cancelled: CancellationToken = Object.freeze({
|
||||
isCancellationRequested: true,
|
||||
onCancellationRequested: shortcutEvent,
|
||||
});
|
||||
}
|
||||
|
||||
class MutableToken implements CancellationToken {
|
||||
private _isCancelled: boolean = false;
|
||||
private _emitter: Emitter<any> | null = null;
|
||||
|
||||
public cancel() {
|
||||
if (!this._isCancelled) {
|
||||
this._isCancelled = true;
|
||||
if (this._emitter) {
|
||||
this._emitter.fire(undefined);
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isCancellationRequested(): boolean {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
get onCancellationRequested(): Event<any> {
|
||||
if (this._isCancelled) {
|
||||
return shortcutEvent;
|
||||
}
|
||||
if (!this._emitter) {
|
||||
this._emitter = new Emitter<any>();
|
||||
}
|
||||
return this._emitter.event;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._emitter) {
|
||||
this._emitter.dispose();
|
||||
this._emitter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CancellationTokenSource {
|
||||
private _token?: CancellationToken = undefined;
|
||||
private _parentListener?: IDisposable = undefined;
|
||||
|
||||
constructor(parent?: CancellationToken) {
|
||||
this._parentListener =
|
||||
parent && parent.onCancellationRequested(this.cancel, this);
|
||||
}
|
||||
|
||||
get token(): CancellationToken {
|
||||
if (!this._token) {
|
||||
// be lazy and create the token only when
|
||||
// actually needed
|
||||
this._token = new MutableToken();
|
||||
}
|
||||
return this._token;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (!this._token) {
|
||||
// save an object by returning the default
|
||||
// cancelled token when cancellation happens
|
||||
// before someone asks for the token
|
||||
this._token = CancellationToken.Cancelled;
|
||||
} else if (this._token instanceof MutableToken) {
|
||||
// actually cancel
|
||||
this._token.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(cancel: boolean = false): void {
|
||||
if (cancel) {
|
||||
this.cancel();
|
||||
}
|
||||
if (this._parentListener) {
|
||||
this._parentListener.dispose();
|
||||
}
|
||||
if (!this._token) {
|
||||
// ensure to initialize with an empty token if we had none
|
||||
this._token = CancellationToken.None;
|
||||
} else if (this._token instanceof MutableToken) {
|
||||
// actually dispose
|
||||
this._token.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
221
packages/foam-core/src/common/errors.ts
Normal file
221
packages/foam-core/src/common/errors.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
export interface ErrorListenerCallback {
|
||||
(error: any): void;
|
||||
}
|
||||
|
||||
export interface ErrorListenerUnbind {
|
||||
(): void;
|
||||
}
|
||||
|
||||
// Avoid circular dependency on EventEmitter by implementing a subset of the interface.
|
||||
export class ErrorHandler {
|
||||
private unexpectedErrorHandler: (e: any) => void;
|
||||
private listeners: ErrorListenerCallback[];
|
||||
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
|
||||
this.unexpectedErrorHandler = function(e: any) {
|
||||
setTimeout(() => {
|
||||
if (e.stack) {
|
||||
throw new Error(e.message + '\n\n' + e.stack);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}, 0);
|
||||
};
|
||||
}
|
||||
|
||||
addListener(listener: ErrorListenerCallback): ErrorListenerUnbind {
|
||||
this.listeners.push(listener);
|
||||
|
||||
return () => {
|
||||
this._removeListener(listener);
|
||||
};
|
||||
}
|
||||
|
||||
private emit(e: any): void {
|
||||
this.listeners.forEach(listener => {
|
||||
listener(e);
|
||||
});
|
||||
}
|
||||
|
||||
private _removeListener(listener: ErrorListenerCallback): void {
|
||||
this.listeners.splice(this.listeners.indexOf(listener), 1);
|
||||
}
|
||||
|
||||
setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
|
||||
this.unexpectedErrorHandler = newUnexpectedErrorHandler;
|
||||
}
|
||||
|
||||
getUnexpectedErrorHandler(): (e: any) => void {
|
||||
return this.unexpectedErrorHandler;
|
||||
}
|
||||
|
||||
onUnexpectedError(e: any): void {
|
||||
this.unexpectedErrorHandler(e);
|
||||
this.emit(e);
|
||||
}
|
||||
|
||||
// For external errors, we don't want the listeners to be called
|
||||
onUnexpectedExternalError(e: any): void {
|
||||
this.unexpectedErrorHandler(e);
|
||||
}
|
||||
}
|
||||
|
||||
export const errorHandler = new ErrorHandler();
|
||||
|
||||
export function setUnexpectedErrorHandler(
|
||||
newUnexpectedErrorHandler: (e: any) => void
|
||||
): void {
|
||||
errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler);
|
||||
}
|
||||
|
||||
export function onUnexpectedError(e: any): undefined {
|
||||
// ignore errors from cancelled promises
|
||||
if (!isPromiseCanceledError(e)) {
|
||||
errorHandler.onUnexpectedError(e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function onUnexpectedExternalError(e: any): undefined {
|
||||
// ignore errors from cancelled promises
|
||||
if (!isPromiseCanceledError(e)) {
|
||||
errorHandler.onUnexpectedExternalError(e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface SerializedError {
|
||||
readonly $isError: true;
|
||||
readonly name: string;
|
||||
readonly message: string;
|
||||
readonly stack: string;
|
||||
}
|
||||
|
||||
export function transformErrorForSerialization(error: Error): SerializedError;
|
||||
export function transformErrorForSerialization(error: any): any;
|
||||
export function transformErrorForSerialization(error: any): any {
|
||||
if (error instanceof Error) {
|
||||
let { name, message } = error;
|
||||
const stack: string = (error as any).stacktrace || (error as any).stack;
|
||||
return {
|
||||
$isError: true,
|
||||
name,
|
||||
message,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
// return as is
|
||||
return error;
|
||||
}
|
||||
|
||||
// see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces
|
||||
export interface V8CallSite {
|
||||
getThis(): any;
|
||||
getTypeName(): string;
|
||||
getFunction(): string;
|
||||
getFunctionName(): string;
|
||||
getMethodName(): string;
|
||||
getFileName(): string;
|
||||
getLineNumber(): number;
|
||||
getColumnNumber(): number;
|
||||
getEvalOrigin(): string;
|
||||
isToplevel(): boolean;
|
||||
isEval(): boolean;
|
||||
isNative(): boolean;
|
||||
isConstructor(): boolean;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
const canceledName = 'Canceled';
|
||||
|
||||
/**
|
||||
* Checks if the given error is a promise in canceled state
|
||||
*/
|
||||
export function isPromiseCanceledError(error: any): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.name === canceledName &&
|
||||
error.message === canceledName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error that signals cancellation.
|
||||
*/
|
||||
export function canceled(): Error {
|
||||
const error = new Error(canceledName);
|
||||
error.name = error.message;
|
||||
return error;
|
||||
}
|
||||
|
||||
export function illegalArgument(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal argument: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal argument');
|
||||
}
|
||||
}
|
||||
|
||||
export function illegalState(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal state: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal state');
|
||||
}
|
||||
}
|
||||
|
||||
export function readonly(name?: string): Error {
|
||||
return name
|
||||
? new Error(`readonly property '${name} cannot be changed'`)
|
||||
: new Error('readonly property cannot be changed');
|
||||
}
|
||||
|
||||
export function disposed(what: string): Error {
|
||||
const result = new Error(`${what} has been disposed`);
|
||||
result.name = 'DISPOSED';
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getErrorMessage(err: any): string {
|
||||
if (!err) {
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
if (err.message) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
if (err.stack) {
|
||||
return err.stack.split('\n')[0];
|
||||
}
|
||||
|
||||
return String(err);
|
||||
}
|
||||
|
||||
export class NotImplementedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super('NotImplemented');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotSupportedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super('NotSupported');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
954
packages/foam-core/src/common/event.ts
Normal file
954
packages/foam-core/src/common/event.ts
Normal file
@@ -0,0 +1,954 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
import { onUnexpectedError } from './errors';
|
||||
import { once as onceFn } from './functional';
|
||||
import {
|
||||
Disposable,
|
||||
IDisposable,
|
||||
toDisposable,
|
||||
combinedDisposable,
|
||||
DisposableStore,
|
||||
} from './lifecycle';
|
||||
import { LinkedList } from './linkedList';
|
||||
|
||||
/**
|
||||
* To an event a function with one or zero parameters
|
||||
* can be subscribed. The event is the subscriber function itself.
|
||||
*/
|
||||
export interface Event<T> {
|
||||
(
|
||||
listener: (e: T) => any,
|
||||
thisArgs?: any,
|
||||
disposables?: IDisposable[] | DisposableStore
|
||||
): IDisposable;
|
||||
}
|
||||
|
||||
export namespace Event {
|
||||
export const None: Event<any> = () => Disposable.None;
|
||||
|
||||
/**
|
||||
* Given an event, returns another event which only fires once.
|
||||
*/
|
||||
export function once<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
// we need this, in case the event fires during the listener call
|
||||
let didFire = false;
|
||||
let result: IDisposable;
|
||||
result = event(
|
||||
e => {
|
||||
if (didFire) {
|
||||
return;
|
||||
} else if (result) {
|
||||
result.dispose();
|
||||
} else {
|
||||
didFire = true;
|
||||
}
|
||||
|
||||
return listener.call(thisArgs, e);
|
||||
},
|
||||
null,
|
||||
disposables
|
||||
);
|
||||
|
||||
if (didFire) {
|
||||
result.dispose();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and a `map` function, returns another event which maps each element
|
||||
* through the mapping function.
|
||||
*/
|
||||
export function map<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
|
||||
return snapshot((listener, thisArgs = null, disposables?) =>
|
||||
event(i => listener.call(thisArgs, map(i)), null, disposables)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and an `each` function, returns another identical event and calls
|
||||
* the `each` function per each element.
|
||||
*/
|
||||
export function forEach<I>(event: Event<I>, each: (i: I) => void): Event<I> {
|
||||
return snapshot((listener, thisArgs = null, disposables?) =>
|
||||
event(
|
||||
i => {
|
||||
each(i);
|
||||
listener.call(thisArgs, i);
|
||||
},
|
||||
null,
|
||||
disposables
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and a `filter` function, returns another event which emits those
|
||||
* elements for which the `filter` function returns `true`.
|
||||
*/
|
||||
export function filter<T>(
|
||||
event: Event<T>,
|
||||
filter: (e: T) => boolean
|
||||
): Event<T>;
|
||||
export function filter<T, R>(
|
||||
event: Event<T | R>,
|
||||
filter: (e: T | R) => e is R
|
||||
): Event<R>;
|
||||
export function filter<T>(
|
||||
event: Event<T>,
|
||||
filter: (e: T) => boolean
|
||||
): Event<T> {
|
||||
return snapshot((listener, thisArgs = null, disposables?) =>
|
||||
event(e => filter(e) && listener.call(thisArgs, e), null, disposables)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, returns the same event but typed as `Event<void>`.
|
||||
*/
|
||||
export function signal<T>(event: Event<T>): Event<void> {
|
||||
return (event as Event<any>) as Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collection of events, returns a single event which emits
|
||||
* whenever any of the provided events emit.
|
||||
*/
|
||||
export function any<T>(...events: Event<T>[]): Event<T>;
|
||||
export function any(...events: Event<any>[]): Event<void>;
|
||||
export function any<T>(...events: Event<T>[]): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) =>
|
||||
combinedDisposable(
|
||||
...events.map(event =>
|
||||
event(e => listener.call(thisArgs, e), null, disposables)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and a `merge` function, returns another event which maps each element
|
||||
* and the cumulative result through the `merge` function. Similar to `map`, but with memory.
|
||||
*/
|
||||
export function reduce<I, O>(
|
||||
event: Event<I>,
|
||||
merge: (last: O | undefined, event: I) => O,
|
||||
initial?: O
|
||||
): Event<O> {
|
||||
let output: O | undefined = initial;
|
||||
|
||||
return map<I, O>(event, e => {
|
||||
output = merge(output, e);
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a chain of event processing functions (filter, map, etc), each
|
||||
* function will be invoked per event & per listener. Snapshotting an event
|
||||
* chain allows each function to be invoked just once per event.
|
||||
*/
|
||||
export function snapshot<T>(event: Event<T>): Event<T> {
|
||||
let listener: IDisposable;
|
||||
const emitter = new Emitter<T>({
|
||||
onFirstListenerAdd() {
|
||||
listener = event(emitter.fire, emitter);
|
||||
},
|
||||
onLastListenerRemove() {
|
||||
listener.dispose();
|
||||
},
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounces the provided event, given a `merge` function.
|
||||
*
|
||||
* @param event The input event.
|
||||
* @param merge The reducing function.
|
||||
* @param delay The debouncing delay in millis.
|
||||
* @param leading Whether the event should fire in the leading phase of the timeout.
|
||||
* @param leakWarningThreshold The leak warning threshold override.
|
||||
*/
|
||||
export function debounce<T>(
|
||||
event: Event<T>,
|
||||
merge: (last: T | undefined, event: T) => T,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): Event<T>;
|
||||
export function debounce<I, O>(
|
||||
event: Event<I>,
|
||||
merge: (last: O | undefined, event: I) => O,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): Event<O>;
|
||||
export function debounce<I, O>(
|
||||
event: Event<I>,
|
||||
merge: (last: O | undefined, event: I) => O,
|
||||
delay: number = 100,
|
||||
leading = false,
|
||||
leakWarningThreshold?: number
|
||||
): Event<O> {
|
||||
let subscription: IDisposable;
|
||||
let output: O | undefined = undefined;
|
||||
let handle: any = undefined;
|
||||
let numDebouncedCalls = 0;
|
||||
|
||||
const emitter = new Emitter<O>({
|
||||
leakWarningThreshold,
|
||||
onFirstListenerAdd() {
|
||||
subscription = event(cur => {
|
||||
numDebouncedCalls++;
|
||||
output = merge(output, cur);
|
||||
|
||||
if (leading && !handle) {
|
||||
emitter.fire(output);
|
||||
output = undefined;
|
||||
}
|
||||
|
||||
clearTimeout(handle);
|
||||
handle = setTimeout(() => {
|
||||
const _output = output;
|
||||
output = undefined;
|
||||
handle = undefined;
|
||||
if (!leading || numDebouncedCalls > 1) {
|
||||
emitter.fire(_output!);
|
||||
}
|
||||
|
||||
numDebouncedCalls = 0;
|
||||
}, delay);
|
||||
});
|
||||
},
|
||||
onLastListenerRemove() {
|
||||
subscription.dispose();
|
||||
},
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, it returns another event which fires only once and as soon as
|
||||
* the input event emits. The event data is the number of millis it took for the
|
||||
* event to fire.
|
||||
*/
|
||||
export function stopwatch<T>(event: Event<T>): Event<number> {
|
||||
const start = new Date().getTime();
|
||||
return map(once(event), _ => new Date().getTime() - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, it returns another event which fires only when the event
|
||||
* element changes.
|
||||
*/
|
||||
export function latch<T>(event: Event<T>): Event<T> {
|
||||
let firstCall = true;
|
||||
let cache: T;
|
||||
|
||||
return filter(event, value => {
|
||||
const shouldEmit = firstCall || value !== cache;
|
||||
firstCall = false;
|
||||
cache = value;
|
||||
return shouldEmit;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffers the provided event until a first listener comes
|
||||
* along, at which point fire all the events at once and
|
||||
* pipe the event from then on.
|
||||
*
|
||||
* ```typescript
|
||||
* const emitter = new Emitter<number>();
|
||||
* const event = emitter.event;
|
||||
* const bufferedEvent = buffer(event);
|
||||
*
|
||||
* emitter.fire(1);
|
||||
* emitter.fire(2);
|
||||
* emitter.fire(3);
|
||||
* // nothing...
|
||||
*
|
||||
* const listener = bufferedEvent(num => console.log(num));
|
||||
* // 1, 2, 3
|
||||
*
|
||||
* emitter.fire(4);
|
||||
* // 4
|
||||
* ```
|
||||
*/
|
||||
export function buffer<T>(
|
||||
event: Event<T>,
|
||||
nextTick = false,
|
||||
_buffer: T[] = []
|
||||
): Event<T> {
|
||||
let buffer: T[] | null = _buffer.slice();
|
||||
|
||||
let listener: IDisposable | null = event(e => {
|
||||
if (buffer) {
|
||||
buffer.push(e);
|
||||
} else {
|
||||
emitter.fire(e);
|
||||
}
|
||||
});
|
||||
|
||||
const flush = () => {
|
||||
if (buffer) {
|
||||
buffer.forEach(e => emitter.fire(e));
|
||||
}
|
||||
buffer = null;
|
||||
};
|
||||
|
||||
const emitter = new Emitter<T>({
|
||||
onFirstListenerAdd() {
|
||||
if (!listener) {
|
||||
listener = event(e => emitter.fire(e));
|
||||
}
|
||||
},
|
||||
|
||||
onFirstListenerDidAdd() {
|
||||
if (buffer) {
|
||||
if (nextTick) {
|
||||
setTimeout(flush, 0);
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLastListenerRemove() {
|
||||
if (listener) {
|
||||
listener.dispose();
|
||||
}
|
||||
listener = null;
|
||||
},
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
export interface IChainableEvent<T> {
|
||||
event: Event<T>;
|
||||
map<O>(fn: (i: T) => O): IChainableEvent<O>;
|
||||
forEach(fn: (i: T) => void): IChainableEvent<T>;
|
||||
filter(fn: (e: T) => boolean): IChainableEvent<T>;
|
||||
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
|
||||
reduce<R>(
|
||||
merge: (last: R | undefined, event: T) => R,
|
||||
initial?: R
|
||||
): IChainableEvent<R>;
|
||||
latch(): IChainableEvent<T>;
|
||||
debounce(
|
||||
merge: (last: T | undefined, event: T) => T,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): IChainableEvent<T>;
|
||||
debounce<R>(
|
||||
merge: (last: R | undefined, event: T) => R,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): IChainableEvent<R>;
|
||||
on(
|
||||
listener: (e: T) => any,
|
||||
thisArgs?: any,
|
||||
disposables?: IDisposable[] | DisposableStore
|
||||
): IDisposable;
|
||||
once(
|
||||
listener: (e: T) => any,
|
||||
thisArgs?: any,
|
||||
disposables?: IDisposable[]
|
||||
): IDisposable;
|
||||
}
|
||||
|
||||
class ChainableEvent<T> implements IChainableEvent<T> {
|
||||
constructor(readonly event: Event<T>) {}
|
||||
|
||||
map<O>(fn: (i: T) => O): IChainableEvent<O> {
|
||||
return new ChainableEvent(map(this.event, fn));
|
||||
}
|
||||
|
||||
forEach(fn: (i: T) => void): IChainableEvent<T> {
|
||||
return new ChainableEvent(forEach(this.event, fn));
|
||||
}
|
||||
|
||||
filter(fn: (e: T) => boolean): IChainableEvent<T>;
|
||||
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
|
||||
filter(fn: (e: T) => boolean): IChainableEvent<T> {
|
||||
return new ChainableEvent(filter(this.event, fn));
|
||||
}
|
||||
|
||||
reduce<R>(
|
||||
merge: (last: R | undefined, event: T) => R,
|
||||
initial?: R
|
||||
): IChainableEvent<R> {
|
||||
return new ChainableEvent(reduce(this.event, merge, initial));
|
||||
}
|
||||
|
||||
latch(): IChainableEvent<T> {
|
||||
return new ChainableEvent(latch(this.event));
|
||||
}
|
||||
|
||||
debounce(
|
||||
merge: (last: T | undefined, event: T) => T,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): IChainableEvent<T>;
|
||||
debounce<R>(
|
||||
merge: (last: R | undefined, event: T) => R,
|
||||
delay?: number,
|
||||
leading?: boolean,
|
||||
leakWarningThreshold?: number
|
||||
): IChainableEvent<R>;
|
||||
debounce<R>(
|
||||
merge: (last: R | undefined, event: T) => R,
|
||||
delay: number = 100,
|
||||
leading = false,
|
||||
leakWarningThreshold?: number
|
||||
): IChainableEvent<R> {
|
||||
return new ChainableEvent(
|
||||
debounce(this.event, merge, delay, leading, leakWarningThreshold)
|
||||
);
|
||||
}
|
||||
|
||||
on(
|
||||
listener: (e: T) => any,
|
||||
thisArgs: any,
|
||||
disposables: IDisposable[] | DisposableStore
|
||||
) {
|
||||
return this.event(listener, thisArgs, disposables);
|
||||
}
|
||||
|
||||
once(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) {
|
||||
return once(this.event)(listener, thisArgs, disposables);
|
||||
}
|
||||
}
|
||||
|
||||
export function chain<T>(event: Event<T>): IChainableEvent<T> {
|
||||
return new ChainableEvent(event);
|
||||
}
|
||||
|
||||
export interface NodeEventEmitter {
|
||||
on(event: string | symbol, listener: Function): unknown;
|
||||
removeListener(event: string | symbol, listener: Function): unknown;
|
||||
}
|
||||
|
||||
export function fromNodeEventEmitter<T>(
|
||||
emitter: NodeEventEmitter,
|
||||
eventName: string,
|
||||
map: (...args: any[]) => T = id => id
|
||||
): Event<T> {
|
||||
const fn = (...args: any[]) => result.fire(map(...args));
|
||||
const onFirstListenerAdd = () => emitter.on(eventName, fn);
|
||||
const onLastListenerRemove = () => emitter.removeListener(eventName, fn);
|
||||
const result = new Emitter<T>({ onFirstListenerAdd, onLastListenerRemove });
|
||||
|
||||
return result.event;
|
||||
}
|
||||
|
||||
export interface DOMEventEmitter {
|
||||
addEventListener(event: string | symbol, listener: Function): void;
|
||||
removeEventListener(event: string | symbol, listener: Function): void;
|
||||
}
|
||||
|
||||
export function fromDOMEventEmitter<T>(
|
||||
emitter: DOMEventEmitter,
|
||||
eventName: string,
|
||||
map: (...args: any[]) => T = id => id
|
||||
): Event<T> {
|
||||
const fn = (...args: any[]) => result.fire(map(...args));
|
||||
const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn);
|
||||
const onLastListenerRemove = () =>
|
||||
emitter.removeEventListener(eventName, fn);
|
||||
const result = new Emitter<T>({ onFirstListenerAdd, onLastListenerRemove });
|
||||
|
||||
return result.event;
|
||||
}
|
||||
|
||||
export function fromPromise<T = any>(promise: Promise<T>): Event<undefined> {
|
||||
const emitter = new Emitter<undefined>();
|
||||
let shouldEmit = false;
|
||||
|
||||
promise
|
||||
.then(undefined, () => null)
|
||||
.then(() => {
|
||||
if (!shouldEmit) {
|
||||
setTimeout(() => emitter.fire(undefined), 0);
|
||||
} else {
|
||||
emitter.fire(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
shouldEmit = true;
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
export function toPromise<T>(event: Event<T>): Promise<T> {
|
||||
return new Promise(c => once(event)(c));
|
||||
}
|
||||
}
|
||||
|
||||
type Listener<T> = [(e: T) => void, any] | ((e: T) => void);
|
||||
|
||||
export interface EmitterOptions {
|
||||
onFirstListenerAdd?: Function;
|
||||
onFirstListenerDidAdd?: Function;
|
||||
onListenerDidAdd?: Function;
|
||||
onLastListenerRemove?: Function;
|
||||
leakWarningThreshold?: number;
|
||||
}
|
||||
|
||||
let _globalLeakWarningThreshold = -1;
|
||||
export function setGlobalLeakWarningThreshold(n: number): IDisposable {
|
||||
const oldValue = _globalLeakWarningThreshold;
|
||||
_globalLeakWarningThreshold = n;
|
||||
return {
|
||||
dispose() {
|
||||
_globalLeakWarningThreshold = oldValue;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class LeakageMonitor {
|
||||
private _stacks: Map<string, number> | undefined;
|
||||
private _warnCountdown: number = 0;
|
||||
|
||||
constructor(
|
||||
readonly customThreshold?: number,
|
||||
readonly name: string = Math.random()
|
||||
.toString(18)
|
||||
.slice(2, 5)
|
||||
) {}
|
||||
|
||||
dispose(): void {
|
||||
if (this._stacks) {
|
||||
this._stacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
check(listenerCount: number): undefined | (() => void) {
|
||||
let threshold = _globalLeakWarningThreshold;
|
||||
if (typeof this.customThreshold === 'number') {
|
||||
threshold = this.customThreshold;
|
||||
}
|
||||
|
||||
if (threshold <= 0 || listenerCount < threshold) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this._stacks) {
|
||||
this._stacks = new Map();
|
||||
}
|
||||
const stack = new Error()
|
||||
.stack!.split('\n')
|
||||
.slice(3)
|
||||
.join('\n');
|
||||
const count = this._stacks.get(stack) || 0;
|
||||
this._stacks.set(stack, count + 1);
|
||||
this._warnCountdown -= 1;
|
||||
|
||||
if (this._warnCountdown <= 0) {
|
||||
// only warn on first exceed and then every time the limit
|
||||
// is exceeded by 50% again
|
||||
this._warnCountdown = threshold * 0.5;
|
||||
|
||||
// find most frequent listener and print warning
|
||||
let topStack: string | undefined;
|
||||
let topCount: number = 0;
|
||||
for (const [stack, count] of this._stacks) {
|
||||
if (!topStack || topCount < count) {
|
||||
topStack = stack;
|
||||
topCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`
|
||||
);
|
||||
console.warn(topStack!);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const count = this._stacks!.get(stack) || 0;
|
||||
this._stacks!.set(stack, count - 1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Emitter can be used to expose an Event to the public
|
||||
* to fire it from the insides.
|
||||
* Sample:
|
||||
class Document {
|
||||
|
||||
private readonly _onDidChange = new Emitter<(value:string)=>any>();
|
||||
|
||||
public onDidChange = this._onDidChange.event;
|
||||
|
||||
// getter-style
|
||||
// get onDidChange(): Event<(value:string)=>any> {
|
||||
// return this._onDidChange.event;
|
||||
// }
|
||||
|
||||
private _doIt() {
|
||||
//...
|
||||
this._onDidChange.fire(value);
|
||||
}
|
||||
}
|
||||
*/
|
||||
export class Emitter<T> {
|
||||
private static readonly _noop = function() {};
|
||||
|
||||
private readonly _options?: EmitterOptions;
|
||||
private readonly _leakageMon?: LeakageMonitor;
|
||||
private _disposed: boolean = false;
|
||||
private _event?: Event<T>;
|
||||
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
|
||||
protected _listeners?: LinkedList<Listener<T>>;
|
||||
|
||||
constructor(options?: EmitterOptions) {
|
||||
this._options = options;
|
||||
this._leakageMon =
|
||||
_globalLeakWarningThreshold > 0
|
||||
? new LeakageMonitor(
|
||||
this._options && this._options.leakWarningThreshold
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the public to allow to subscribe
|
||||
* to events from this Emitter
|
||||
*/
|
||||
get event(): Event<T> {
|
||||
if (!this._event) {
|
||||
this._event = (
|
||||
listener: (e: T) => any,
|
||||
thisArgs?: any,
|
||||
disposables?: IDisposable[] | DisposableStore
|
||||
) => {
|
||||
if (!this._listeners) {
|
||||
this._listeners = new LinkedList();
|
||||
}
|
||||
|
||||
const firstListener = this._listeners.isEmpty();
|
||||
|
||||
if (
|
||||
firstListener &&
|
||||
this._options &&
|
||||
this._options.onFirstListenerAdd
|
||||
) {
|
||||
this._options.onFirstListenerAdd(this);
|
||||
}
|
||||
|
||||
const remove = this._listeners.push(
|
||||
!thisArgs ? listener : [listener, thisArgs]
|
||||
);
|
||||
|
||||
if (
|
||||
firstListener &&
|
||||
this._options &&
|
||||
this._options.onFirstListenerDidAdd
|
||||
) {
|
||||
this._options.onFirstListenerDidAdd(this);
|
||||
}
|
||||
|
||||
if (this._options && this._options.onListenerDidAdd) {
|
||||
this._options.onListenerDidAdd(this, listener, thisArgs);
|
||||
}
|
||||
|
||||
// check and record this emitter for potential leakage
|
||||
let removeMonitor: (() => void) | undefined;
|
||||
if (this._leakageMon) {
|
||||
removeMonitor = this._leakageMon.check(this._listeners.size);
|
||||
}
|
||||
|
||||
let result: IDisposable;
|
||||
result = {
|
||||
dispose: () => {
|
||||
if (removeMonitor) {
|
||||
removeMonitor();
|
||||
}
|
||||
result.dispose = Emitter._noop;
|
||||
if (!this._disposed) {
|
||||
remove();
|
||||
if (this._options && this._options.onLastListenerRemove) {
|
||||
const hasListeners =
|
||||
this._listeners && !this._listeners.isEmpty();
|
||||
if (!hasListeners) {
|
||||
this._options.onLastListenerRemove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
if (disposables instanceof DisposableStore) {
|
||||
disposables.add(result);
|
||||
} else if (Array.isArray(disposables)) {
|
||||
disposables.push(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
return this._event;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be kept private to fire an event to
|
||||
* subscribers
|
||||
*/
|
||||
fire(event: T): void {
|
||||
if (this._listeners) {
|
||||
// put all [listener,event]-pairs into delivery queue
|
||||
// then emit all event. an inner/nested event might be
|
||||
// the driver of this
|
||||
|
||||
if (!this._deliveryQueue) {
|
||||
this._deliveryQueue = new LinkedList();
|
||||
}
|
||||
|
||||
for (let listener of this._listeners) {
|
||||
this._deliveryQueue.push([listener, event]);
|
||||
}
|
||||
|
||||
while (this._deliveryQueue.size > 0) {
|
||||
const [listener, event] = this._deliveryQueue.shift()!;
|
||||
try {
|
||||
if (typeof listener === 'function') {
|
||||
listener.call(undefined, event);
|
||||
} else {
|
||||
listener[0].call(listener[1], event);
|
||||
}
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._listeners) {
|
||||
this._listeners.clear();
|
||||
}
|
||||
if (this._deliveryQueue) {
|
||||
this._deliveryQueue.clear();
|
||||
}
|
||||
if (this._leakageMon) {
|
||||
this._leakageMon.dispose();
|
||||
}
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PauseableEmitter<T> extends Emitter<T> {
|
||||
private _isPaused = 0;
|
||||
private _eventQueue = new LinkedList<T>();
|
||||
private _mergeFn?: (input: T[]) => T;
|
||||
|
||||
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
|
||||
super(options);
|
||||
this._mergeFn = options && options.merge;
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this._isPaused++;
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (this._isPaused !== 0 && --this._isPaused === 0) {
|
||||
if (this._mergeFn) {
|
||||
// use the merge function to create a single composite
|
||||
// event. make a copy in case firing pauses this emitter
|
||||
const events = Array.from(this._eventQueue);
|
||||
this._eventQueue.clear();
|
||||
super.fire(this._mergeFn(events));
|
||||
} else {
|
||||
// no merging, fire each event individually and test
|
||||
// that this emitter isn't paused halfway through
|
||||
while (!this._isPaused && this._eventQueue.size !== 0) {
|
||||
super.fire(this._eventQueue.shift()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fire(event: T): void {
|
||||
if (this._listeners) {
|
||||
if (this._isPaused !== 0) {
|
||||
this._eventQueue.push(event);
|
||||
} else {
|
||||
super.fire(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWaitUntil {
|
||||
waitUntil(thenable: Promise<any>): void;
|
||||
}
|
||||
|
||||
export class EventMultiplexer<T> implements IDisposable {
|
||||
private readonly emitter: Emitter<T>;
|
||||
private hasListeners = false;
|
||||
private events: { event: Event<T>; listener: IDisposable | null }[] = [];
|
||||
|
||||
constructor() {
|
||||
this.emitter = new Emitter<T>({
|
||||
onFirstListenerAdd: () => this.onFirstListenerAdd(),
|
||||
onLastListenerRemove: () => this.onLastListenerRemove(),
|
||||
});
|
||||
}
|
||||
|
||||
get event(): Event<T> {
|
||||
return this.emitter.event;
|
||||
}
|
||||
|
||||
add(event: Event<T>): IDisposable {
|
||||
const e = { event: event, listener: null };
|
||||
this.events.push(e);
|
||||
|
||||
if (this.hasListeners) {
|
||||
this.hook(e);
|
||||
}
|
||||
|
||||
const dispose = () => {
|
||||
if (this.hasListeners) {
|
||||
this.unhook(e);
|
||||
}
|
||||
|
||||
const idx = this.events.indexOf(e);
|
||||
this.events.splice(idx, 1);
|
||||
};
|
||||
|
||||
return toDisposable(onceFn(dispose));
|
||||
}
|
||||
|
||||
private onFirstListenerAdd(): void {
|
||||
this.hasListeners = true;
|
||||
this.events.forEach(e => this.hook(e));
|
||||
}
|
||||
|
||||
private onLastListenerRemove(): void {
|
||||
this.hasListeners = false;
|
||||
this.events.forEach(e => this.unhook(e));
|
||||
}
|
||||
|
||||
private hook(e: { event: Event<T>; listener: IDisposable | null }): void {
|
||||
e.listener = e.event(r => this.emitter.fire(r));
|
||||
}
|
||||
|
||||
private unhook(e: { event: Event<T>; listener: IDisposable | null }): void {
|
||||
if (e.listener) {
|
||||
e.listener.dispose();
|
||||
}
|
||||
e.listener = null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The EventBufferer is useful in situations in which you want
|
||||
* to delay firing your events during some code.
|
||||
* You can wrap that code and be sure that the event will not
|
||||
* be fired during that wrap.
|
||||
*
|
||||
* ```
|
||||
* const emitter: Emitter;
|
||||
* const delayer = new EventDelayer();
|
||||
* const delayedEvent = delayer.wrapEvent(emitter.event);
|
||||
*
|
||||
* delayedEvent(console.log);
|
||||
*
|
||||
* delayer.bufferEvents(() => {
|
||||
* emitter.fire(); // event will not be fired yet
|
||||
* });
|
||||
*
|
||||
* // event will only be fired at this point
|
||||
* ```
|
||||
*/
|
||||
export class EventBufferer {
|
||||
private buffers: Function[][] = [];
|
||||
|
||||
wrapEvent<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs?, disposables?) => {
|
||||
return event(
|
||||
i => {
|
||||
const buffer = this.buffers[this.buffers.length - 1];
|
||||
|
||||
if (buffer) {
|
||||
buffer.push(() => listener.call(thisArgs, i));
|
||||
} else {
|
||||
listener.call(thisArgs, i);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
disposables
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
bufferEvents<R = void>(fn: () => R): R {
|
||||
const buffer: Array<() => R> = [];
|
||||
this.buffers.push(buffer);
|
||||
const r = fn();
|
||||
this.buffers.pop();
|
||||
buffer.forEach(flush => flush());
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Relay is an event forwarder which functions as a replugabble event pipe.
|
||||
* Once created, you can connect an input event to it and it will simply forward
|
||||
* events from that input event through its own `event` property. The `input`
|
||||
* can be changed at any point in time.
|
||||
*/
|
||||
export class Relay<T> implements IDisposable {
|
||||
private listening = false;
|
||||
private inputEvent: Event<T> = Event.None;
|
||||
private inputEventListener: IDisposable = Disposable.None;
|
||||
|
||||
private readonly emitter = new Emitter<T>({
|
||||
onFirstListenerDidAdd: () => {
|
||||
this.listening = true;
|
||||
this.inputEventListener = this.inputEvent(
|
||||
this.emitter.fire,
|
||||
this.emitter
|
||||
);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this.listening = false;
|
||||
this.inputEventListener.dispose();
|
||||
},
|
||||
});
|
||||
|
||||
readonly event: Event<T> = this.emitter.event;
|
||||
|
||||
set input(event: Event<T>) {
|
||||
this.inputEvent = event;
|
||||
|
||||
if (this.listening) {
|
||||
this.inputEventListener.dispose();
|
||||
this.inputEventListener = event(this.emitter.fire, this.emitter);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.inputEventListener.dispose();
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
23
packages/foam-core/src/common/functional.ts
Normal file
23
packages/foam-core/src/common/functional.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
export function once<T extends Function>(this: unknown, fn: T): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return (function() {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
|
||||
didCall = true;
|
||||
result = fn.apply(_this, arguments);
|
||||
|
||||
return result;
|
||||
} as unknown) as T;
|
||||
}
|
||||
111
packages/foam-core/src/common/iterator.ts
Normal file
111
packages/foam-core/src/common/iterator.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
export namespace Iterable {
|
||||
export function is<T = any>(thing: any): thing is IterableIterator<T> {
|
||||
return (
|
||||
thing &&
|
||||
typeof thing === 'object' &&
|
||||
typeof thing[Symbol.iterator] === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
const _empty: Iterable<any> = Object.freeze([]);
|
||||
export function empty<T = any>(): Iterable<T> {
|
||||
return _empty;
|
||||
}
|
||||
|
||||
export function* single<T>(element: T): Iterable<T> {
|
||||
yield element;
|
||||
}
|
||||
|
||||
export function from<T>(
|
||||
iterable: Iterable<T> | undefined | null
|
||||
): Iterable<T> {
|
||||
return iterable || _empty;
|
||||
}
|
||||
|
||||
export function first<T>(iterable: Iterable<T>): T | undefined {
|
||||
return iterable[Symbol.iterator]().next().value;
|
||||
}
|
||||
|
||||
export function some<T>(
|
||||
iterable: Iterable<T>,
|
||||
predicate: (t: T) => boolean
|
||||
): boolean {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function* filter<T>(
|
||||
iterable: Iterable<T>,
|
||||
predicate: (t: T) => boolean
|
||||
): Iterable<T> {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, R>(
|
||||
iterable: Iterable<T>,
|
||||
fn: (t: T) => R
|
||||
): Iterable<R> {
|
||||
for (const element of iterable) {
|
||||
yield fn(element);
|
||||
}
|
||||
}
|
||||
|
||||
export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
|
||||
for (const iterable of iterables) {
|
||||
for (const element of iterable) {
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes `atMost` elements from iterable and returns the consumed elements,
|
||||
* and an iterable for the rest of the elements.
|
||||
*/
|
||||
export function consume<T>(
|
||||
iterable: Iterable<T>,
|
||||
atMost: number = Number.POSITIVE_INFINITY
|
||||
): [T[], Iterable<T>] {
|
||||
const consumed: T[] = [];
|
||||
|
||||
if (atMost === 0) {
|
||||
return [consumed, iterable];
|
||||
}
|
||||
|
||||
const iterator = iterable[Symbol.iterator]();
|
||||
|
||||
for (let i = 0; i < atMost; i++) {
|
||||
const next = iterator.next();
|
||||
|
||||
if (next.done) {
|
||||
return [consumed, Iterable.empty()];
|
||||
}
|
||||
|
||||
consumed.push(next.value);
|
||||
}
|
||||
|
||||
return [
|
||||
consumed,
|
||||
{
|
||||
[Symbol.iterator]() {
|
||||
return iterator;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
300
packages/foam-core/src/common/lifecycle.ts
Normal file
300
packages/foam-core/src/common/lifecycle.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
import { once } from './functional';
|
||||
import { Iterable } from './iterator';
|
||||
|
||||
/**
|
||||
* Enables logging of potentially leaked disposables.
|
||||
*
|
||||
* A disposable is considered leaked if it is not disposed or not registered as the child of
|
||||
* another disposable. This tracking is very simple an only works for classes that either
|
||||
* extend Disposable or use a DisposableStore. This means there are a lot of false positives.
|
||||
*/
|
||||
const TRACK_DISPOSABLES = false;
|
||||
|
||||
const __is_disposable_tracked__ = '__is_disposable_tracked__';
|
||||
|
||||
function markTracked<T extends IDisposable>(x: T): void {
|
||||
if (!TRACK_DISPOSABLES) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (x && x !== Disposable.None) {
|
||||
try {
|
||||
(x as any)[__is_disposable_tracked__] = true;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trackDisposable<T extends IDisposable>(x: T): T {
|
||||
if (!TRACK_DISPOSABLES) {
|
||||
return x;
|
||||
}
|
||||
|
||||
const stack = new Error('Potentially leaked disposable').stack!;
|
||||
setTimeout(() => {
|
||||
if (!(x as any)[__is_disposable_tracked__]) {
|
||||
console.log(stack);
|
||||
}
|
||||
}, 3000);
|
||||
return x;
|
||||
}
|
||||
|
||||
export class MultiDisposeError extends Error {
|
||||
constructor(public readonly errors: any[]) {
|
||||
super(
|
||||
`Encounter errors while disposing of store. Errors: [${errors.join(
|
||||
', '
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function isDisposable<E extends object>(
|
||||
thing: E
|
||||
): thing is E & IDisposable {
|
||||
return (
|
||||
typeof (thing as IDisposable).dispose === 'function' &&
|
||||
(thing as IDisposable).dispose.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function dispose<T extends IDisposable>(disposable: T): T;
|
||||
export function dispose<T extends IDisposable>(
|
||||
disposable: T | undefined
|
||||
): T | undefined;
|
||||
export function dispose<
|
||||
T extends IDisposable,
|
||||
A extends IterableIterator<T> = IterableIterator<T>
|
||||
>(disposables: IterableIterator<T>): A;
|
||||
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
|
||||
export function dispose<T extends IDisposable>(
|
||||
disposables: ReadonlyArray<T>
|
||||
): ReadonlyArray<T>;
|
||||
export function dispose<T extends IDisposable>(
|
||||
arg: T | IterableIterator<T> | undefined
|
||||
): any {
|
||||
if (Iterable.is(arg)) {
|
||||
let errors: any[] = [];
|
||||
|
||||
for (const d of arg) {
|
||||
if (d) {
|
||||
markTracked(d);
|
||||
try {
|
||||
d.dispose();
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 1) {
|
||||
throw errors[0];
|
||||
} else if (errors.length > 1) {
|
||||
throw new MultiDisposeError(errors);
|
||||
}
|
||||
|
||||
return Array.isArray(arg) ? [] : arg;
|
||||
} else if (arg) {
|
||||
markTracked(arg);
|
||||
arg.dispose();
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
||||
disposables.forEach(markTracked);
|
||||
return trackDisposable({ dispose: () => dispose(disposables) });
|
||||
}
|
||||
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
const self = trackDisposable({
|
||||
dispose: () => {
|
||||
markTracked(self);
|
||||
fn();
|
||||
},
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
export class DisposableStore implements IDisposable {
|
||||
static DISABLE_DISPOSED_WARNING = false;
|
||||
|
||||
private _toDispose = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables and mark this object as disposed.
|
||||
*
|
||||
* Any future disposables added to this object will be disposed of on `add`.
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
markTracked(this);
|
||||
this._isDisposed = true;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables but do not mark this object as disposed.
|
||||
*/
|
||||
public clear(): void {
|
||||
try {
|
||||
dispose(this._toDispose.values());
|
||||
} finally {
|
||||
this._toDispose.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public add<T extends IDisposable>(t: T): T {
|
||||
if (!t) {
|
||||
return t;
|
||||
}
|
||||
if (((t as unknown) as DisposableStore) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
|
||||
markTracked(t);
|
||||
if (this._isDisposed) {
|
||||
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
|
||||
console.warn(
|
||||
new Error(
|
||||
'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!'
|
||||
).stack
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._toDispose.add(t);
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable implements IDisposable {
|
||||
static readonly None = Object.freeze<IDisposable>({ dispose() {} });
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
markTracked(this);
|
||||
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
protected _register<T extends IDisposable>(t: T): T {
|
||||
if (((t as unknown) as Disposable) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
return this._store.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of a disposable value that may be changed.
|
||||
*
|
||||
* This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can
|
||||
* also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up.
|
||||
*/
|
||||
export class MutableDisposable<T extends IDisposable> implements IDisposable {
|
||||
private _value?: T;
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
get value(): T | undefined {
|
||||
return this._isDisposed ? undefined : this._value;
|
||||
}
|
||||
|
||||
set value(value: T | undefined) {
|
||||
if (this._isDisposed || value === this._value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._value) {
|
||||
this._value.dispose();
|
||||
}
|
||||
if (value) {
|
||||
markTracked(value);
|
||||
}
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._isDisposed = true;
|
||||
markTracked(this);
|
||||
if (this._value) {
|
||||
this._value.dispose();
|
||||
}
|
||||
this._value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IReference<T> extends IDisposable {
|
||||
readonly object: T;
|
||||
}
|
||||
|
||||
export abstract class ReferenceCollection<T> {
|
||||
private readonly references: Map<
|
||||
string,
|
||||
{ readonly object: T; counter: number }
|
||||
> = new Map();
|
||||
|
||||
acquire(key: string, ...args: any[]): IReference<T> {
|
||||
let reference = this.references.get(key);
|
||||
|
||||
if (!reference) {
|
||||
reference = {
|
||||
counter: 0,
|
||||
object: this.createReferencedObject(key, ...args),
|
||||
};
|
||||
this.references.set(key, reference);
|
||||
}
|
||||
|
||||
const { object } = reference;
|
||||
const dispose = once(() => {
|
||||
if (--reference!.counter === 0) {
|
||||
this.destroyReferencedObject(key, reference!.object);
|
||||
this.references.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
reference.counter++;
|
||||
|
||||
return { object, dispose };
|
||||
}
|
||||
|
||||
protected abstract createReferencedObject(key: string, ...args: any[]): T;
|
||||
protected abstract destroyReferencedObject(key: string, object: T): void;
|
||||
}
|
||||
|
||||
export class ImmortalReference<T> implements IReference<T> {
|
||||
constructor(public object: T) {}
|
||||
dispose(): void {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
129
packages/foam-core/src/common/linkedList.ts
Normal file
129
packages/foam-core/src/common/linkedList.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
class Node<E> {
|
||||
static readonly Undefined = new Node<any>(undefined);
|
||||
|
||||
element: E;
|
||||
next: Node<E>;
|
||||
prev: Node<E>;
|
||||
|
||||
constructor(element: E) {
|
||||
this.element = element;
|
||||
this.next = Node.Undefined;
|
||||
this.prev = Node.Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkedList<E> {
|
||||
private _first: Node<E> = Node.Undefined;
|
||||
private _last: Node<E> = Node.Undefined;
|
||||
private _size: number = 0;
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._first === Node.Undefined;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
unshift(element: E): () => void {
|
||||
return this._insert(element, false);
|
||||
}
|
||||
|
||||
push(element: E): () => void {
|
||||
return this._insert(element, true);
|
||||
}
|
||||
|
||||
private _insert(element: E, atTheEnd: boolean): () => void {
|
||||
const newNode = new Node(element);
|
||||
if (this._first === Node.Undefined) {
|
||||
this._first = newNode;
|
||||
this._last = newNode;
|
||||
} else if (atTheEnd) {
|
||||
// push
|
||||
const oldLast = this._last!;
|
||||
this._last = newNode;
|
||||
newNode.prev = oldLast;
|
||||
oldLast.next = newNode;
|
||||
} else {
|
||||
// unshift
|
||||
const oldFirst = this._first;
|
||||
this._first = newNode;
|
||||
newNode.next = oldFirst;
|
||||
oldFirst.prev = newNode;
|
||||
}
|
||||
this._size += 1;
|
||||
|
||||
let didRemove = false;
|
||||
return () => {
|
||||
if (!didRemove) {
|
||||
didRemove = true;
|
||||
this._remove(newNode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
shift(): E | undefined {
|
||||
if (this._first === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._first.element;
|
||||
this._remove(this._first);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
pop(): E | undefined {
|
||||
if (this._last === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._last.element;
|
||||
this._remove(this._last);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
private _remove(node: Node<E>): void {
|
||||
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
|
||||
// middle
|
||||
const anchor = node.prev;
|
||||
anchor.next = node.next;
|
||||
node.next.prev = anchor;
|
||||
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
|
||||
// only node
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
} else if (node.next === Node.Undefined) {
|
||||
// last
|
||||
this._last = this._last!.prev!;
|
||||
this._last.next = Node.Undefined;
|
||||
} else if (node.prev === Node.Undefined) {
|
||||
// first
|
||||
this._first = this._first!.next!;
|
||||
this._first.prev = Node.Undefined;
|
||||
}
|
||||
|
||||
// done
|
||||
this._size -= 1;
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): Iterator<E> {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
yield node.element;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,26 @@ import { merge } from 'lodash';
|
||||
|
||||
export interface FoamConfig {
|
||||
workspaceFolders: string[];
|
||||
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: string[],
|
||||
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);
|
||||
@@ -23,8 +33,15 @@ export const createConfigFromObject = (
|
||||
};
|
||||
|
||||
export const createConfigFromFolders = (
|
||||
workspaceFolders: string[]
|
||||
workspaceFolders: string[] | string,
|
||||
options: {
|
||||
include?: string[];
|
||||
ignore?: string[];
|
||||
} = {}
|
||||
): FoamConfig => {
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [workspaceFolders];
|
||||
}
|
||||
const workspaceConfig: any = workspaceFolders.reduce(
|
||||
(acc, f) => merge(acc, parseConfig(`${f}/config.json`)),
|
||||
{}
|
||||
@@ -39,7 +56,12 @@ export const createConfigFromFolders = (
|
||||
|
||||
const settings = merge(workspaceConfig, userConfig);
|
||||
|
||||
return createConfigFromObject(workspaceFolders, settings);
|
||||
return createConfigFromObject(
|
||||
workspaceFolders,
|
||||
options.include ?? DEFAULT_INCLUDES,
|
||||
options.ignore ?? DEFAULT_IGNORES,
|
||||
settings
|
||||
);
|
||||
};
|
||||
|
||||
const parseConfig = (path: string) => {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Note, NoteLink } from './types';
|
||||
import { Note, NoteLink, URI } from './types';
|
||||
import { NoteGraph, NoteGraphAPI } from './note-graph';
|
||||
import { FoamConfig } from './config';
|
||||
import { IDataStore, FileDataStore } from './services/datastore';
|
||||
import { ILogger } from './utils/log';
|
||||
|
||||
export { IDataStore, FileDataStore };
|
||||
export { ILogger };
|
||||
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
|
||||
export { IDisposable, isDisposable } from './common/lifecycle';
|
||||
export { Event, Emitter } from './common/event';
|
||||
export { FoamConfig };
|
||||
|
||||
export {
|
||||
@@ -22,15 +29,19 @@ export { createConfigFromFolders } from './config';
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
|
||||
export { NoteGraph, NoteGraphAPI, Note, NoteLink };
|
||||
export { NoteGraph, NoteGraphAPI, Note, NoteLink, URI };
|
||||
|
||||
export {
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
} from './definitions';
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
}
|
||||
|
||||
export interface Foam {
|
||||
notes: NoteGraphAPI;
|
||||
config: FoamConfig;
|
||||
parse: (uri: string, text: string, eol: string) => Note;
|
||||
parse: (uri: URI, text: string, eol: string) => Note;
|
||||
}
|
||||
|
||||
@@ -5,39 +5,44 @@ import frontmatterPlugin from 'remark-frontmatter';
|
||||
import { parse as parseYAML } from 'yaml';
|
||||
import visit from 'unist-util-visit';
|
||||
import { Parent, Point } from 'unist';
|
||||
import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import * as path from 'path';
|
||||
import { NoteGraphAPI } from './note-graph';
|
||||
import { NoteLinkDefinition, Note, NoteParser } from './types';
|
||||
import { dropExtension, uriToSlug } from './utils';
|
||||
import {
|
||||
dropExtension,
|
||||
uriToSlug,
|
||||
extractHashtags,
|
||||
extractTagsFromProp,
|
||||
} from './utils';
|
||||
import { ID } from './types';
|
||||
import { ParserPlugin } from './plugins';
|
||||
import { Logger } from './utils/log';
|
||||
|
||||
const yamlPlugin: ParserPlugin = {
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'yaml') {
|
||||
note.properties = {
|
||||
...note.properties,
|
||||
...(parseYAML(node.value as string) ?? {}),
|
||||
};
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = note.properties.title ?? note.title;
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = {
|
||||
line: node.position!.end.line! + 1,
|
||||
column: 1,
|
||||
offset: node.position!.end.offset! + 1,
|
||||
};
|
||||
}
|
||||
const tagsPlugin: ParserPlugin = {
|
||||
name: 'tags',
|
||||
onWillVisitTree: (tree, note) => {
|
||||
note.tags = extractHashtags(note.source.text);
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
const yamlTags = extractTagsFromProp(props.tags);
|
||||
yamlTags.forEach(tag => note.tags.add(tag));
|
||||
},
|
||||
};
|
||||
|
||||
const titlePlugin: ParserPlugin = {
|
||||
name: 'title',
|
||||
visit: (node, note) => {
|
||||
if (note.title == null && node.type === 'heading' && node.depth === 1) {
|
||||
note.title =
|
||||
((node as Parent)!.children?.[0]?.value as string) || note.title;
|
||||
}
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = props.title ?? note.title;
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title == null) {
|
||||
note.title = path.parse(note.source.uri).name;
|
||||
@@ -46,6 +51,7 @@ const titlePlugin: ParserPlugin = {
|
||||
};
|
||||
|
||||
const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'wikiLink') {
|
||||
note.links.push({
|
||||
@@ -58,6 +64,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
};
|
||||
|
||||
const definitionsPlugin: ParserPlugin = {
|
||||
name: 'definitions',
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'definition') {
|
||||
note.definitions.push({
|
||||
@@ -73,6 +80,19 @@ const definitionsPlugin: ParserPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const handleError = (
|
||||
plugin: ParserPlugin,
|
||||
fnName: string,
|
||||
uri: string | undefined,
|
||||
e: Error
|
||||
): void => {
|
||||
const name = plugin.name || '';
|
||||
Logger.warn(
|
||||
`Error while executing [${fnName}] in plugin [${name}] for file [${uri}]`,
|
||||
e
|
||||
);
|
||||
};
|
||||
|
||||
export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
const parser = unified()
|
||||
.use(markdownParse, { gfm: true })
|
||||
@@ -80,26 +100,40 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
.use(wikiLinkPlugin);
|
||||
|
||||
const plugins = [
|
||||
yamlPlugin,
|
||||
titlePlugin,
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
|
||||
plugins.forEach(plugin => plugin.onDidInitializeParser?.(parser));
|
||||
plugins.forEach(plugin => {
|
||||
try {
|
||||
plugin.onDidInitializeParser?.(parser);
|
||||
} catch (e) {
|
||||
handleError(plugin, 'onDidInitializeParser', undefined, e);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
parse: (uri: string, markdown: string, eol: string): Note => {
|
||||
const foamParser: NoteParser = {
|
||||
parse: (uri: string, markdown: string): Note => {
|
||||
Logger.debug('Parsing:', uri);
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
return plugin.onWillParseMarkdown?.(acc) || acc;
|
||||
try {
|
||||
return plugin.onWillParseMarkdown?.(acc) || acc;
|
||||
} catch (e) {
|
||||
handleError(plugin, 'onWillParseMarkdown', uri, e);
|
||||
return acc;
|
||||
}
|
||||
}, markdown);
|
||||
const tree = parser.parse(markdown);
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
var note: Note = {
|
||||
slug: uriToSlug(uri),
|
||||
properties: {},
|
||||
title: null,
|
||||
tags: new Set(),
|
||||
links: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
@@ -111,17 +145,62 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
},
|
||||
};
|
||||
|
||||
plugins.forEach(plugin => plugin.onWillVisitTree?.(tree, note));
|
||||
visit(tree, node => {
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
plugins[i].visit?.(node, note);
|
||||
plugins.forEach(plugin => {
|
||||
try {
|
||||
plugin.onWillVisitTree?.(tree, note);
|
||||
} catch (e) {
|
||||
handleError(plugin, 'onWillVisitTree', uri, e);
|
||||
}
|
||||
});
|
||||
plugins.forEach(plugin => plugin.onDidVisitTree?.(tree, note));
|
||||
visit(tree, node => {
|
||||
if (node.type === 'yaml') {
|
||||
try {
|
||||
const yamlProperties = parseYAML(node.value as string) ?? {};
|
||||
note.properties = {
|
||||
...note.properties,
|
||||
...yamlProperties,
|
||||
};
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = note.properties.title ?? note.title;
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = {
|
||||
line: node.position!.end.line! + 1,
|
||||
column: 1,
|
||||
offset: node.position!.end.offset! + 1,
|
||||
};
|
||||
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
try {
|
||||
plugins[i].onDidFindProperties?.(yamlProperties, note);
|
||||
} catch (e) {
|
||||
handleError(plugins[i], 'onDidFindProperties', uri, e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn(`Error while parsing YAML for [${uri}]`, e);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
try {
|
||||
plugins[i].visit?.(node, note);
|
||||
} catch (e) {
|
||||
handleError(plugins[i], 'visit', uri, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
plugins.forEach(plugin => {
|
||||
try {
|
||||
plugin.onDidVisitTree?.(tree, note);
|
||||
} catch (e) {
|
||||
handleError(plugin, 'onDidVisitTree', uri, e);
|
||||
}
|
||||
});
|
||||
Logger.debug('Result:', note);
|
||||
return note;
|
||||
},
|
||||
};
|
||||
return foamParser;
|
||||
}
|
||||
|
||||
function getFoamDefinitions(
|
||||
@@ -183,7 +262,7 @@ export function createMarkdownReferences(
|
||||
if (!target) {
|
||||
const candidates = graph.getNotes({ slug: link.link.slug });
|
||||
if (candidates.length > 1) {
|
||||
console.log(
|
||||
Logger.info(
|
||||
`Warning: Slug ${link.link.slug} matches ${candidates.length} documents. Picking one.`
|
||||
);
|
||||
}
|
||||
@@ -192,7 +271,7 @@ export function createMarkdownReferences(
|
||||
// We are dropping links to non-existent notes here,
|
||||
// but int the future we may want to surface these too
|
||||
if (!target) {
|
||||
console.log(
|
||||
Logger.info(
|
||||
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
|
||||
);
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Graph } from 'graphlib';
|
||||
import { EventEmitter } from 'events';
|
||||
import { URI, ID, Note, NoteLink } from './types';
|
||||
import { computeRelativeURI } from './utils';
|
||||
import { computeRelativeURI, nameToSlug, isSome } from './utils';
|
||||
import { Event, Emitter } from './common/event';
|
||||
|
||||
export type GraphNote = Note & {
|
||||
id: ID;
|
||||
@@ -19,15 +19,16 @@ export type NotesQuery = { slug: string } | { title: string };
|
||||
|
||||
export interface NoteGraphAPI {
|
||||
setNote(note: Note): GraphNote;
|
||||
deleteNote(noteId: ID): GraphNote | null;
|
||||
getNotes(query?: NotesQuery): GraphNote[];
|
||||
getNote(noteId: ID): GraphNote | null;
|
||||
getNoteByURI(uri: URI): GraphNote | null;
|
||||
getAllLinks(noteId: ID): GraphConnection[];
|
||||
getForwardLinks(noteId: ID): GraphConnection[];
|
||||
getBacklinks(noteId: ID): GraphConnection[];
|
||||
unstable_onNoteAdded(callback: NoteGraphEventHandler): void;
|
||||
unstable_onNoteUpdated(callback: NoteGraphEventHandler): void;
|
||||
unstable_removeEventListener(callback: NoteGraphEventHandler): void;
|
||||
onDidAddNote: Event<GraphNote>;
|
||||
onDidUpdateNote: Event<GraphNote>;
|
||||
onDidDeleteNote: Event<GraphNote>;
|
||||
}
|
||||
|
||||
export type Middleware = (next: NoteGraphAPI) => Partial<NoteGraphAPI>;
|
||||
@@ -38,24 +39,27 @@ export const createGraph = (middlewares: Middleware[]): NoteGraphAPI => {
|
||||
};
|
||||
|
||||
export class NoteGraph implements NoteGraphAPI {
|
||||
onDidAddNote: Event<GraphNote>;
|
||||
onDidUpdateNote: Event<GraphNote>;
|
||||
onDidDeleteNote: Event<GraphNote>;
|
||||
|
||||
private graph: Graph;
|
||||
private events: EventEmitter;
|
||||
private createIdFromURI: (uri: URI) => ID;
|
||||
private onDidAddNoteEmitter = new Emitter<GraphNote>();
|
||||
private onDidUpdateNoteEmitter = new Emitter<GraphNote>();
|
||||
private onDidDeleteEmitter = new Emitter<GraphNote>();
|
||||
|
||||
constructor() {
|
||||
this.graph = new Graph();
|
||||
this.events = new EventEmitter();
|
||||
this.onDidAddNote = this.onDidAddNoteEmitter.event;
|
||||
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
|
||||
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
|
||||
this.createIdFromURI = uri => uri;
|
||||
}
|
||||
|
||||
public setNote(note: Note): GraphNote {
|
||||
const id = this.createIdFromURI(note.source.uri);
|
||||
const noteExists = this.graph.hasNode(id);
|
||||
if (noteExists) {
|
||||
(this.graph.outEdges(id) || []).forEach(edge => {
|
||||
this.graph.removeEdge(edge);
|
||||
});
|
||||
}
|
||||
const oldNote = this.doDelete(id, false);
|
||||
const graphNote: GraphNote = {
|
||||
...note,
|
||||
id: id,
|
||||
@@ -73,15 +77,33 @@ export class NoteGraph implements NoteGraphAPI {
|
||||
};
|
||||
this.graph.setEdge(graphNote.id, targetId, connection);
|
||||
});
|
||||
this.events.emit(noteExists ? 'update' : 'add', { note: graphNote });
|
||||
isSome(oldNote)
|
||||
? this.onDidUpdateNoteEmitter.fire(graphNote)
|
||||
: this.onDidAddNoteEmitter.fire(graphNote);
|
||||
return graphNote;
|
||||
}
|
||||
|
||||
public deleteNote(noteId: ID): GraphNote | null {
|
||||
return this.doDelete(noteId, true);
|
||||
}
|
||||
|
||||
private doDelete(noteId: ID, fireEvent: boolean): GraphNote | null {
|
||||
const note = this.getNote(noteId);
|
||||
if (isSome(note)) {
|
||||
this.graph.removeNode(noteId);
|
||||
(this.graph.outEdges(noteId) || []).forEach(edge => {
|
||||
this.graph.removeEdge(edge);
|
||||
});
|
||||
fireEvent && this.onDidDeleteEmitter.fire(note);
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
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
|
||||
: 'slug' in query ? (note: Note | null) => [nameToSlug(query.slug), query.slug].includes(note?.slug as string)
|
||||
: 'title' in query ? (note: Note | null) => note?.title === query.title
|
||||
: (note: Note | null) => note != null;
|
||||
|
||||
@@ -117,21 +139,10 @@ export class NoteGraph implements NoteGraphAPI {
|
||||
);
|
||||
}
|
||||
|
||||
public unstable_onNoteAdded(callback: NoteGraphEventHandler) {
|
||||
this.events.addListener('add', callback);
|
||||
}
|
||||
|
||||
public unstable_onNoteUpdated(callback: NoteGraphEventHandler) {
|
||||
this.events.addListener('update', callback);
|
||||
}
|
||||
|
||||
public unstable_removeEventListener(callback: NoteGraphEventHandler) {
|
||||
this.events.removeListener('add', callback);
|
||||
this.events.removeListener('update', callback);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.events.removeAllListeners();
|
||||
this.onDidAddNoteEmitter.dispose();
|
||||
this.onDidUpdateNoteEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,14 +150,15 @@ const backfill = (next: NoteGraphAPI, middleware: Middleware): NoteGraphAPI => {
|
||||
const m = middleware(next);
|
||||
return {
|
||||
setNote: m.setNote || next.setNote,
|
||||
deleteNote: m.deleteNote || next.deleteNote,
|
||||
getNotes: m.getNotes || next.getNotes,
|
||||
getNote: m.getNote || next.getNote,
|
||||
getNoteByURI: m.getNoteByURI || next.getNoteByURI,
|
||||
getAllLinks: m.getAllLinks || next.getAllLinks,
|
||||
getForwardLinks: m.getForwardLinks || next.getForwardLinks,
|
||||
getBacklinks: m.getBacklinks || next.getBacklinks,
|
||||
unstable_onNoteAdded: next.unstable_onNoteAdded.bind(next),
|
||||
unstable_onNoteUpdated: next.unstable_onNoteUpdated.bind(next),
|
||||
unstable_removeEventListener: next.unstable_removeEventListener.bind(next),
|
||||
onDidAddNote: next.onDidAddNote,
|
||||
onDidUpdateNote: next.onDidUpdateNote,
|
||||
onDidDeleteNote: next.onDidDeleteNote,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Middleware } from '../note-graph';
|
||||
import { Note } from '../types';
|
||||
import unified from 'unified';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
|
||||
export interface FoamPlugin {
|
||||
name: string;
|
||||
@@ -15,11 +16,13 @@ export interface FoamPlugin {
|
||||
}
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
visit?: (node: Node, note: Note) => void;
|
||||
onDidInitializeParser?: (parser: unified.Processor) => void;
|
||||
onWillParseMarkdown?: (markdown: string) => string;
|
||||
onWillVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidFindProperties?: (properties: any, note: Note) => void;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
@@ -45,10 +48,11 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
try {
|
||||
const pluginFile = path.join(dir, 'index.js');
|
||||
fs.accessSync(pluginFile);
|
||||
Logger.info(`Found plugin at [${pluginFile}]. Loading..`);
|
||||
const plugin = validate(await import(pluginFile));
|
||||
return plugin;
|
||||
} catch (e) {
|
||||
console.error(`Error while loading plugin at [${dir}] - skipping`, e);
|
||||
Logger.error(`Error while loading plugin at [${dir}] - skipping`, e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
124
packages/foam-core/src/services/datastore.ts
Normal file
124
packages/foam-core/src/services/datastore.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { Event, Emitter } from '../common/event';
|
||||
import { URI } from '../types';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files available in the store
|
||||
*/
|
||||
listFiles: () => Promise<URI[]>;
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*/
|
||||
read: (uri: URI) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns whether the given URI is a match in
|
||||
* this data store
|
||||
*/
|
||||
isMatch: (uri: URI) => boolean;
|
||||
|
||||
/**
|
||||
* Filters a list of URIs based on whether they are a match
|
||||
* in this data store
|
||||
*/
|
||||
match: (uris: URI[]) => string[];
|
||||
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
readonly onDidChangeEmitter = new Emitter<URI>();
|
||||
readonly onDidCreateEmitter = new Emitter<URI>();
|
||||
readonly onDidDeleteEmitter = new Emitter<URI>();
|
||||
readonly onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
|
||||
readonly onDidChange: Event<URI> = this.onDidChangeEmitter.event;
|
||||
readonly onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
|
||||
readonly isMatch: (uri: URI) => boolean;
|
||||
readonly match: (uris: URI[]) => string[];
|
||||
|
||||
private _folders: readonly string[];
|
||||
|
||||
constructor(config: FoamConfig) {
|
||||
this._folders = config.workspaceFolders;
|
||||
|
||||
let includeGlobs: string[] = [];
|
||||
let ignoreGlobs: string[] = [];
|
||||
config.workspaceFolders.forEach(folder => {
|
||||
const withFolder = folderPlusGlob(folder);
|
||||
includeGlobs.push(
|
||||
...config.includeGlobs.map(glob => {
|
||||
if (glob.endsWith('*')) {
|
||||
glob = `${glob}\\.(md|mdx|markdown)`;
|
||||
}
|
||||
return withFolder(glob);
|
||||
})
|
||||
);
|
||||
ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
|
||||
});
|
||||
|
||||
Logger.debug('Glob patterns', {
|
||||
includeGlobs,
|
||||
ignoreGlobs,
|
||||
});
|
||||
this.match = (files: URI[]) => {
|
||||
return micromatch(files, includeGlobs, {
|
||||
ignore: ignoreGlobs,
|
||||
nocase: true,
|
||||
});
|
||||
};
|
||||
this.isMatch = uri => this.match([uri]).length > 0;
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
const files = (
|
||||
await Promise.all(
|
||||
this._folders.map(folder => {
|
||||
return findAllFiles(folderPlusGlob(folder)('**/*'));
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
return this.match(files);
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
return (await fs.promises.readFile(uri)).toString();
|
||||
}
|
||||
}
|
||||
|
||||
const folderPlusGlob = (folder: string) => (glob: string): string => {
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
if (glob.startsWith('/')) {
|
||||
glob = glob.slice(1);
|
||||
}
|
||||
return `${folder}/${glob}`;
|
||||
};
|
||||
@@ -35,12 +35,12 @@ export interface Note {
|
||||
slug: string; // note: this slug is not necessarily unique
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
// tags: NoteTag[]
|
||||
tags: Set<string>;
|
||||
links: NoteLink[];
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export interface NoteParser {
|
||||
parse: (uri: string, text: string, eol: string) => Note;
|
||||
parse: (uri: string, text: string) => Note;
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { titleCase } from 'title-case';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI, ID } from './types';
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
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('.');
|
||||
parts.pop();
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filename
|
||||
* @returns title cased heading after removing special characters
|
||||
*/
|
||||
export const getHeadingFromFileName = (filename: string): string => {
|
||||
return titleCase(filename.replace(/[^\w\s]/gi, ' '));
|
||||
};
|
||||
25
packages/foam-core/src/utils/core.ts
Normal file
25
packages/foam-core/src/utils/core.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export function isNumeric(value: string): boolean {
|
||||
return /-?\d+$/.test(value);
|
||||
}
|
||||
|
||||
export const hash = (text: string) =>
|
||||
crypto
|
||||
.createHash('sha1')
|
||||
.update(text)
|
||||
.digest('hex');
|
||||
16
packages/foam-core/src/utils/hashtags.ts
Normal file
16
packages/foam-core/src/utils/hashtags.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(^|[ ])#([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
const WORD_REGEX = /(^|[ ])([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
|
||||
export const extractHashtags = (text: string): Set<string> => {
|
||||
return isSome(text)
|
||||
? new Set(Array.from(text.matchAll(HASHTAG_REGEX), m => m[2].trim()))
|
||||
: new Set();
|
||||
};
|
||||
|
||||
export const extractTagsFromProp = (prop: string | string[]): Set<string> => {
|
||||
const text = Array.isArray(prop) ? prop.join(' ') : prop;
|
||||
return isSome(text)
|
||||
? new Set(Array.from(text.matchAll(WORD_REGEX)).map(m => m[2].trim()))
|
||||
: new Set();
|
||||
};
|
||||
19
packages/foam-core/src/utils/index.ts
Normal file
19
packages/foam-core/src/utils/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { titleCase } from 'title-case';
|
||||
export { extractHashtags, extractTagsFromProp } from './hashtags';
|
||||
export * from './uri';
|
||||
export * from './core';
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
const parts = path.split('.');
|
||||
parts.pop();
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filename
|
||||
* @returns title cased heading after removing special characters
|
||||
*/
|
||||
export const getHeadingFromFileName = (filename: string): string => {
|
||||
return titleCase(filename.replace(/[^\w\s]/gi, ' '));
|
||||
};
|
||||
89
packages/foam-core/src/utils/log.ts
Normal file
89
packages/foam-core/src/utils/log.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export interface ILogger {
|
||||
debug(message?: any, ...params: any[]): void;
|
||||
info(message?: any, ...params: any[]): void;
|
||||
warn(message?: any, ...params: any[]): void;
|
||||
error(message?: any, ...params: any[]): void;
|
||||
getLevel(): LogLevelThreshold;
|
||||
setLevel(level: LogLevelThreshold): void;
|
||||
}
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type LogLevelThreshold = LogLevel | 'off';
|
||||
|
||||
export abstract class BaseLogger implements ILogger {
|
||||
private static severity = {
|
||||
debug: 1,
|
||||
info: 2,
|
||||
warn: 3,
|
||||
error: 4,
|
||||
};
|
||||
|
||||
constructor(private level: LogLevelThreshold = 'info') {}
|
||||
|
||||
abstract log(lvl: LogLevel, msg?: any, ...extra: any[]): void;
|
||||
|
||||
doLog(msgLevel: LogLevel, message?: any, ...params: any[]): void {
|
||||
if (this.level === 'off') {
|
||||
return;
|
||||
}
|
||||
if (BaseLogger.severity[msgLevel] >= BaseLogger.severity[this.level]) {
|
||||
this.log(msgLevel, message, ...params);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message?: any, ...params: any[]): void {
|
||||
this.doLog('debug', message, ...params);
|
||||
}
|
||||
info(message?: any, ...params: any[]): void {
|
||||
this.doLog('info', message, ...params);
|
||||
}
|
||||
warn(message?: any, ...params: any[]): void {
|
||||
this.doLog('warn', message, ...params);
|
||||
}
|
||||
error(message?: any, ...params: any[]): void {
|
||||
this.doLog('error', message, ...params);
|
||||
}
|
||||
getLevel(): LogLevelThreshold {
|
||||
return this.level;
|
||||
}
|
||||
setLevel(level: LogLevelThreshold): void {
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsoleLogger extends BaseLogger {
|
||||
log(level: LogLevel, msg?: string, ...params: any[]): void {
|
||||
console[level](`[${level}] ${msg}`, ...params);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpLogger extends BaseLogger {
|
||||
log(_l: LogLevel, _m?: string, ..._p: any[]): void {}
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static debug(message?: any, ...params: any[]): void {
|
||||
Logger.defaultLogger.debug(message, ...params);
|
||||
}
|
||||
static info(message?: any, ...params: any[]): void {
|
||||
Logger.defaultLogger.info(message, ...params);
|
||||
}
|
||||
static warn(message?: any, ...params: any[]): void {
|
||||
Logger.defaultLogger.warn(message, ...params);
|
||||
}
|
||||
static error(message?: any, ...params: any[]): void {
|
||||
Logger.defaultLogger.error(message, ...params);
|
||||
}
|
||||
static getLevel(): LogLevelThreshold {
|
||||
return Logger.defaultLogger.getLevel();
|
||||
}
|
||||
static setLevel(level: LogLevelThreshold): void {
|
||||
Logger.defaultLogger.setLevel(level);
|
||||
}
|
||||
|
||||
private static defaultLogger: ILogger = new ConsoleLogger();
|
||||
|
||||
static setDefaultLogger(logger: ILogger) {
|
||||
Logger.defaultLogger = logger;
|
||||
}
|
||||
}
|
||||
28
packages/foam-core/src/utils/uri.ts
Normal file
28
packages/foam-core/src/utils/uri.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import path from 'path';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI, ID } from '../types';
|
||||
import { hash } from './core';
|
||||
|
||||
export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(path.parse(noteUri).name);
|
||||
};
|
||||
|
||||
export const nameToSlug = (noteName: string): string => {
|
||||
return GithubSlugger.slug(noteName);
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
||||
@@ -23,6 +23,7 @@ export const createTestNote = (params: {
|
||||
title: params.title ?? null,
|
||||
slug: uriToSlug(params.uri),
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map(link => ({
|
||||
type: 'wikilink',
|
||||
@@ -187,6 +188,93 @@ describe('Graph querying', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('graph events', () => {
|
||||
it('fires "add" event when adding a new note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidAddNote(callback);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('fires "updated" event when changing an existing note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidUpdateNote(callback);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Another title' })
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('fires "delete" event when removing a note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidDeleteNote(callback);
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.deleteNote(note.id);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('does not fire "delete" event when removing a non-existing note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidDeleteNote(callback);
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.deleteNote('non-existing-note');
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
listener.dispose();
|
||||
});
|
||||
it('happy lifecycle', () => {
|
||||
const graph = new NoteGraph();
|
||||
const addCallback = jest.fn();
|
||||
const updateCallback = jest.fn();
|
||||
const deleteCallback = jest.fn();
|
||||
const listeners = [
|
||||
graph.onDidAddNote(addCallback),
|
||||
graph.onDidUpdateNote(updateCallback),
|
||||
graph.onDidDeleteNote(deleteCallback),
|
||||
];
|
||||
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(0);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Another Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(1);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Yet Another Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(2);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.deleteNote(note.id);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(2);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
listeners.forEach(l => l.dispose());
|
||||
});
|
||||
});
|
||||
|
||||
describe('graph middleware', () => {
|
||||
it('can intercept calls to the graph', async () => {
|
||||
const graph = createGraph([
|
||||
|
||||
@@ -3,14 +3,19 @@ import { NoteGraphAPI } from '../../src/note-graph';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
|
||||
describe('generateHeadings', () => {
|
||||
let _graph: NoteGraphAPI;
|
||||
|
||||
beforeAll(async () => {
|
||||
const foam = await bootstrap(
|
||||
createConfigFromFolders([path.join(__dirname, '../__scaffold__')])
|
||||
);
|
||||
const config = createConfigFromFolders([
|
||||
path.join(__dirname, '../__scaffold__'),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const foam = await bootstrap(config, services);
|
||||
_graph = foam.notes;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,14 +3,20 @@ import { NoteGraphAPI } from '../../src/note-graph';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _graph: NoteGraphAPI;
|
||||
|
||||
beforeAll(async () => {
|
||||
_graph = await bootstrap(
|
||||
createConfigFromFolders([path.join(__dirname, '../__scaffold__')])
|
||||
).then(foam => foam.notes);
|
||||
const config = createConfigFromFolders([
|
||||
path.join(__dirname, '../__scaffold__'),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
_graph = await bootstrap(config, services).then(foam => foam.notes);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
|
||||
@@ -11,6 +11,8 @@ const pageA = `
|
||||
## Section
|
||||
- [[page-b]]
|
||||
- [[page-c]]
|
||||
- [[Page D]]
|
||||
- [[page e]]
|
||||
`;
|
||||
|
||||
const pageB = `
|
||||
@@ -23,23 +25,11 @@ const pageC = `
|
||||
`;
|
||||
|
||||
const pageD = `
|
||||
This file has no heading.
|
||||
# Page D
|
||||
`;
|
||||
|
||||
const pageE = `
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title
|
||||
`;
|
||||
|
||||
const pageF = `
|
||||
---
|
||||
---
|
||||
|
||||
# Empty Frontmatter
|
||||
# Page E
|
||||
`;
|
||||
|
||||
const createNoteFromMarkdown = createMarkdownParser([]).parse;
|
||||
@@ -47,43 +37,41 @@ const createNoteFromMarkdown = createMarkdownParser([]).parse;
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createNoteFromMarkdown('/page-a.md', pageA, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-b.md', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
graph.setNote(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('/page-d.md', pageD));
|
||||
graph.setNote(createNoteFromMarkdown('/page-e.md', pageE));
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getNotes()
|
||||
.map(n => n.slug)
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c']);
|
||||
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const graph = new NoteGraph();
|
||||
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'));
|
||||
const noteA = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
const noteB = graph.setNote(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('/Page D.md', pageD));
|
||||
graph.setNote(createNoteFromMarkdown('/page e.md', pageE));
|
||||
|
||||
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']);
|
||||
).toEqual(['page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Note Title', () => {
|
||||
it('should initialize note title if heading exists', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown('/page-a.md', pageA, '\n')
|
||||
);
|
||||
const note = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
|
||||
const pageANoteTitle = graph.getNote(note.id)!.title;
|
||||
expect(pageANoteTitle).toBe('Page A');
|
||||
@@ -92,7 +80,12 @@ describe('Note Title', () => {
|
||||
it('should default to file name if heading does not exist', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown('/page-d.md', pageD, '\n')
|
||||
createNoteFromMarkdown(
|
||||
'/page-d.md',
|
||||
`
|
||||
This file has no heading.
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const pageANoteTitle = graph.getNote(note.id)!.title;
|
||||
@@ -102,7 +95,17 @@ describe('Note Title', () => {
|
||||
it('should give precedence to frontmatter title over other headings', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown('/page-e.md', pageE, '\n')
|
||||
createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const pageENoteTitle = graph.getNote(note.id)!.title;
|
||||
@@ -116,8 +119,7 @@ describe('Note Title', () => {
|
||||
#
|
||||
|
||||
this note has an empty title line
|
||||
`,
|
||||
'\n'
|
||||
`
|
||||
);
|
||||
expect(note.title).toEqual('Hello Page');
|
||||
});
|
||||
@@ -127,7 +129,16 @@ describe('frontmatter', () => {
|
||||
it('should parse yaml frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown('/page-e.md', pageE, '\n')
|
||||
createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {
|
||||
@@ -144,7 +155,38 @@ describe('frontmatter', () => {
|
||||
it('should parse empty frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown('/page-f.md', pageF, '\n')
|
||||
createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
---
|
||||
---
|
||||
|
||||
# Empty Frontmatter
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {};
|
||||
|
||||
const actual = graph.getNote(note.id)!.properties;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not fail when there are issues with parsing frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
---
|
||||
title: - one
|
||||
- two
|
||||
- #
|
||||
---
|
||||
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {};
|
||||
@@ -159,10 +201,10 @@ describe('wikilinks definitions', () => {
|
||||
it('can generate links without file extension when includeExtension = false', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
|
||||
const noExtRefs = createMarkdownReferences(graph, noteA.id, false);
|
||||
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
|
||||
@@ -171,10 +213,10 @@ describe('wikilinks definitions', () => {
|
||||
it('can generate links with file extension when includeExtension = true', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
|
||||
const extRefs = createMarkdownReferences(graph, noteA.id, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
|
||||
@@ -183,10 +225,10 @@ describe('wikilinks definitions', () => {
|
||||
it('use relative paths', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA, '\n')
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC));
|
||||
|
||||
const extRefs = createMarkdownReferences(graph, noteA.id, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual([
|
||||
@@ -196,6 +238,51 @@ describe('wikilinks definitions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags plugin', () => {
|
||||
it('can find tags in the text of the note', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
expect(noteA.tags).toEqual(new Set(['text', 'tags', 'care-about']));
|
||||
});
|
||||
|
||||
it('can find tags as text in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
---
|
||||
tags: hello, world this_is_good
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
expect(noteA.tags).toEqual(
|
||||
new Set(['text', 'tags', 'care-about', 'hello', 'world', 'this_is_good'])
|
||||
);
|
||||
});
|
||||
|
||||
it('can find tags as array in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
---
|
||||
tags: [hello, world, this_is_good]
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
expect(noteA.tags).toEqual(
|
||||
new Set(['text', 'tags', 'care-about', 'hello', 'world', 'this_is_good'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser plugins', () => {
|
||||
const testPlugin: ParserPlugin = {
|
||||
visit: (node, note) => {
|
||||
@@ -212,8 +299,7 @@ describe('parser plugins', () => {
|
||||
`
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
`,
|
||||
'\n'
|
||||
`
|
||||
);
|
||||
expect(note1.properties.hasHeading).toBeUndefined();
|
||||
|
||||
@@ -221,8 +307,7 @@ But with some content.
|
||||
'/path/to/a',
|
||||
`
|
||||
# This is a note with header
|
||||
and some content`,
|
||||
'\n'
|
||||
and some content`
|
||||
);
|
||||
expect(note2.properties.hasHeading).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createGraph } from '../src/note-graph';
|
||||
import { createTestNote } from './core.test';
|
||||
import { FoamConfig, createConfigFromObject } from '../src/config';
|
||||
|
||||
const config: FoamConfig = createConfigFromObject([], {
|
||||
const config: FoamConfig = createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {
|
||||
enabled: true,
|
||||
@@ -16,10 +16,10 @@ const config: FoamConfig = createConfigFromObject([], {
|
||||
|
||||
describe('Foam plugins', () => {
|
||||
it('will not load if feature is not explicitly enabled', async () => {
|
||||
let plugins = await loadPlugins(createConfigFromObject([], {}));
|
||||
let plugins = await loadPlugins(createConfigFromObject([], [], [], {}));
|
||||
expect(plugins.length).toEqual(0);
|
||||
plugins = await loadPlugins(
|
||||
createConfigFromObject([], {
|
||||
createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {},
|
||||
},
|
||||
@@ -27,7 +27,7 @@ describe('Foam plugins', () => {
|
||||
);
|
||||
expect(plugins.length).toEqual(0);
|
||||
plugins = await loadPlugins(
|
||||
createConfigFromObject([], {
|
||||
createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {
|
||||
enabled: false,
|
||||
@@ -63,8 +63,7 @@ describe('Foam plugins', () => {
|
||||
'/path/to/a',
|
||||
`
|
||||
# This is a note with header
|
||||
and some content`,
|
||||
'\n'
|
||||
and some content`
|
||||
);
|
||||
expect(note.properties.hasHeading).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { uriToSlug, hashURI, computeRelativeURI } from '../src/utils';
|
||||
import {
|
||||
uriToSlug,
|
||||
nameToSlug,
|
||||
hashURI,
|
||||
computeRelativeURI,
|
||||
extractHashtags,
|
||||
} from '../src/utils';
|
||||
|
||||
describe('URI utils', () => {
|
||||
it('supports various cases', () => {
|
||||
@@ -9,6 +15,13 @@ describe('URI utils', () => {
|
||||
expect(uriToSlug('many.dots.name.markdown')).toEqual('manydotsname');
|
||||
});
|
||||
|
||||
it('converts a name to a slug', () => {
|
||||
expect(nameToSlug('this.has.dots')).toEqual('thishasdots');
|
||||
expect(nameToSlug('title')).toEqual('title');
|
||||
expect(nameToSlug('this is a title')).toEqual('this-is-a-title');
|
||||
expect(nameToSlug('this is a title/slug')).toEqual('this-is-a-titleslug');
|
||||
});
|
||||
|
||||
it('normalizes URI before hashing', () => {
|
||||
expect(hashURI('/this/is/a/path.md')).toEqual(
|
||||
hashURI('/this/has/../is/a/path.md')
|
||||
@@ -28,3 +41,46 @@ describe('URI utils', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashtag extraction', () => {
|
||||
it('works with simple strings', () => {
|
||||
expect(extractHashtags('hello #world on #this planet')).toEqual(
|
||||
new Set(['world', 'this'])
|
||||
);
|
||||
});
|
||||
it('works with tags at beginning or end of text', () => {
|
||||
expect(extractHashtags('#hello world on this #planet')).toEqual(
|
||||
new Set(['hello', 'planet'])
|
||||
);
|
||||
});
|
||||
it('supports _ and -', () => {
|
||||
expect(extractHashtags('#hello-world on #this_planet')).toEqual(
|
||||
new Set(['hello-world', 'this_planet'])
|
||||
);
|
||||
});
|
||||
it('ignores tags that only have numbers in text', () => {
|
||||
expect(
|
||||
extractHashtags('this #123 tag should be ignore, but not #123four')
|
||||
).toEqual(new Set(['123four']));
|
||||
});
|
||||
|
||||
it('ignores hashes in plain text urls and links', () => {
|
||||
expect(
|
||||
extractHashtags(`
|
||||
test text with url https://site.com/#section1 https://site.com/home#section2 and
|
||||
https://site.com/home/#section3a
|
||||
[link](https://site.com/#section4) with [link2](https://site.com/home#section5) #control
|
||||
hello world
|
||||
`)
|
||||
).toEqual(new Set(['control']));
|
||||
});
|
||||
|
||||
it('ignores hashes in links to sections', () => {
|
||||
expect(
|
||||
extractHashtags(`
|
||||
this is a wikilink to [[#section1]] in the file and a [[link#section2]] in another
|
||||
this is a [link](#section3) to a section
|
||||
`)
|
||||
).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
|
||||
"downlevelIteration": true,
|
||||
"target": "es2019",
|
||||
// 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.
|
||||
@@ -19,11 +20,11 @@
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"lib": [
|
||||
"esnext"
|
||||
"ES2019", "es2020.string"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,57 @@ 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.7.2] - 2020-11-27
|
||||
|
||||
Fixes and Improvements:
|
||||
- Dataviz: Sync note deletion
|
||||
- Foam model: Fix to wikilink format (#386 - thanks @SanketDG)
|
||||
|
||||
## [0.7.1] - 2020-11-27
|
||||
|
||||
New Feature:
|
||||
- Foam logging can now be inspected in VsCode Output panel (#377)
|
||||
|
||||
Fixes and Improvements:
|
||||
- Foam model: Fixed bug in tags parsing (#382)
|
||||
- Dataviz: Graph canvas now resizes with window (#383, #375)
|
||||
- Dataviz: Limit label length for placeholder nodes (#381)
|
||||
|
||||
## [0.7.0] - 2020-11-25
|
||||
|
||||
New Features:
|
||||
- Foam stays in sync with changes in notes
|
||||
- Dataviz: Added multiple selection in graph (shift+click on node)
|
||||
|
||||
Fixes and Improvements:
|
||||
- Dataviz: Graph uses VSCode theme colors
|
||||
- Reporting: Errors occurring during foam bootstrap are now reported for easier debugging
|
||||
|
||||
## [0.6.0] - 2020-11-19
|
||||
|
||||
New features:
|
||||
- Added command to create notes from templates (#115 - Thanks @ingalless)
|
||||
|
||||
Fixes and Improvements:
|
||||
- Foam model: Fixed bug that prevented wikilinks from being slugified (#323 - thanks @SanketDG)
|
||||
- Editor: Improvements in defaults for ignored files setting (thanks @jmg-duarte)
|
||||
- Dataviz: Centering of the graph on note displayed in active editor (#319)
|
||||
- Dataviz: Improved graph styling
|
||||
- Dataviz: Added setting to cap the length of labels in the graph (thanks @jmg-duarte)
|
||||
- Misc: Fixed problem with packaging icon in extension (#350 - thanks @litanlitudan)
|
||||
|
||||
## [0.5.0] - 2020-11-09
|
||||
|
||||
New features:
|
||||
- Added tags panel (#311)
|
||||
|
||||
Fixes and Improvements:
|
||||
- Date snippets now support configurable completion actions (#307 - thanks @ingalless)
|
||||
- Graph now show note titles when zooming in (#310)
|
||||
- New `foam.files.ignore` setting to exclude globs from being processed by Foam (#304 - thanks @jmg-duarte)
|
||||
- Errors in YAML parsing no longer causes foam to crash (#320)
|
||||
- Fixed error in CLI command janitor & migrate (#312 - thanks @hikerpig)
|
||||
|
||||
## [0.4.0] - 2020-10-28
|
||||
|
||||
New features:
|
||||
|
||||
@@ -2,5 +2,6 @@ module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
],
|
||||
plugins: [["@babel/plugin-transform-runtime", { helpers: false }]]
|
||||
};
|
||||
|
||||
BIN
packages/foam-vscode/icon/FOAM_ICON.png
Normal file
BIN
packages/foam-vscode/icon/FOAM_ICON.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 604 KiB |
BIN
packages/foam-vscode/icon/FOAM_ICON_256.png
Normal file
BIN
packages/foam-vscode/icon/FOAM_ICON_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -3,18 +3,24 @@
|
||||
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
|
||||
"description": "Generate markdown reference lists from wikilinks in a workspace",
|
||||
"author": "Jani Eväkallio",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.4.0",
|
||||
"repository": {
|
||||
"url": "https://github.com/foambubble/foam",
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.7.2",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
"vscode": "^1.45.1"
|
||||
},
|
||||
"icon": "icon/FOAM_ICON_256.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:.vscode/foam.json",
|
||||
"onView:foam-vscode.tags-explorer",
|
||||
"onCommand:foam-vscode.update-wikilinks",
|
||||
"onCommand:foam-vscode.open-daily-note",
|
||||
"onCommand:foam-vscode.janitor",
|
||||
@@ -23,7 +29,21 @@
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
"id": "foam-vscode.tags-explorer",
|
||||
"name": "Tag Explorer",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Tags Explorer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "foam-vscode.set-log-level",
|
||||
"title": "Foam: Set log level"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.show-graph",
|
||||
"title": "Foam: Show graph"
|
||||
@@ -43,11 +63,38 @@
|
||||
{
|
||||
"command": "foam-vscode.copy-without-brackets",
|
||||
"title": "Foam: Copy To Clipboard Without Brackets"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-note-from-template",
|
||||
"title": "Foam: Create New Note From Template"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Foam",
|
||||
"properties": {
|
||||
"foam.files.ignore": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"default": [
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
],
|
||||
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`"
|
||||
},
|
||||
"foam.logging.level": {
|
||||
"type": "string",
|
||||
"default": "info",
|
||||
"enum": [
|
||||
"off",
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"foam.edit.linkReferenceDefinitions": {
|
||||
"type": "string",
|
||||
"default": "withoutExtensions",
|
||||
@@ -90,10 +137,25 @@
|
||||
"default": null,
|
||||
"description": "The directory into which daily notes should be created. Defaults to the workspace root."
|
||||
},
|
||||
"foam.snippets.navigateOnSelect": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"foam.dateSnippets.afterCompletion": {
|
||||
"type": "string",
|
||||
"default": "createNote",
|
||||
"enum": [
|
||||
"noop",
|
||||
"createNote",
|
||||
"navigateToNote"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Nothing happens after selecting the completion item",
|
||||
"The note is created following your daily note settings if it does not exist, but no navigation takes place",
|
||||
"Navigates to the note, creating it following your daily note settings if it does not exist"
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
"default": 24,
|
||||
"description": "The maximum title length before being abbreviated. Set to 0 or less to disable."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -115,7 +177,8 @@
|
||||
"npm-install": "rimraf node_modules && npm i",
|
||||
"npm-cleanup": "rimraf package-lock.json node_modules && yarn",
|
||||
"package-extension": "npx vsce package && yarn npm-cleanup",
|
||||
"publish-extension": "npx vsce publish && yarn npm-cleanup"
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix && yarn npm-cleanup"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
@@ -137,6 +200,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"foam-core": "^0.4.0"
|
||||
"foam-core": "^0.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { dirname, join } from "path";
|
||||
import dateFormat from "dateformat";
|
||||
import * as fs from "fs";
|
||||
import { docConfig, pathExists } from "./utils";
|
||||
import { docConfig, focusNote, pathExists } from "./utils";
|
||||
|
||||
async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration("foam");
|
||||
@@ -79,16 +79,9 @@ async function createDailyNoteDirectoryIfNotExists(dailyNotePath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function focusNote(notePath: string, isNewNote: boolean) {
|
||||
const document = await workspace.openTextDocument(Uri.file(notePath));
|
||||
const editor = await window.showTextDocument(document);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (isNewNote) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
}
|
||||
|
||||
export { openDailyNoteFor, getDailyNoteFileName };
|
||||
export {
|
||||
openDailyNoteFor,
|
||||
getDailyNoteFileName,
|
||||
createDailyNoteIfNotExists,
|
||||
getDailyNotePath
|
||||
};
|
||||
|
||||
@@ -1,99 +1,72 @@
|
||||
/**
|
||||
* Adapted from vscode-markdown/src/toc.ts
|
||||
* https://github.com/yzhang-gh/vscode-markdown/blob/master/src/toc.ts
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
import path from "path";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
workspace,
|
||||
ExtensionContext,
|
||||
window,
|
||||
EndOfLine,
|
||||
Uri,
|
||||
FileSystemWatcher
|
||||
} from "vscode";
|
||||
import { workspace, ExtensionContext, window } from "vscode";
|
||||
|
||||
import {
|
||||
bootstrap as foamBootstrap,
|
||||
bootstrap,
|
||||
FoamConfig,
|
||||
Foam,
|
||||
createConfigFromFolders
|
||||
FileDataStore,
|
||||
Services,
|
||||
isDisposable,
|
||||
Logger
|
||||
} from "foam-core";
|
||||
|
||||
import { features } from "./features";
|
||||
import { getConfigFromVscode } from "./services/config";
|
||||
import { VsCodeOutputLogger, exposeLogger } from "./services/logging";
|
||||
|
||||
let workspaceWatcher: FileSystemWatcher | null = null;
|
||||
let foam: Foam | null = null;
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
Logger.setDefaultLogger(logger);
|
||||
exposeLogger(context, logger);
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
try {
|
||||
const foamPromise = bootstrap();
|
||||
Logger.info("Starting Foam");
|
||||
|
||||
const config: FoamConfig = getConfigFromVscode();
|
||||
const dataStore = new FileDataStore(config);
|
||||
|
||||
const watcher = workspace.createFileSystemWatcher("**/*");
|
||||
watcher.onDidCreate(uri => {
|
||||
if (dataStore.isMatch(uri.fsPath)) {
|
||||
dataStore.onDidCreateEmitter.fire(uri.fsPath);
|
||||
}
|
||||
});
|
||||
watcher.onDidChange(uri => {
|
||||
if (dataStore.isMatch(uri.fsPath)) {
|
||||
dataStore.onDidChangeEmitter.fire(uri.fsPath);
|
||||
}
|
||||
});
|
||||
watcher.onDidDelete(uri => {
|
||||
if (dataStore.isMatch(uri.fsPath)) {
|
||||
dataStore.onDidDeleteEmitter.fire(uri.fsPath);
|
||||
}
|
||||
});
|
||||
|
||||
const services: Services = {
|
||||
dataStore: dataStore
|
||||
};
|
||||
const foamPromise: Promise<Foam> = bootstrap(config, services);
|
||||
|
||||
features.forEach(f => {
|
||||
f.activate(context, foamPromise);
|
||||
});
|
||||
|
||||
foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.notes.getNotes().length} notes`);
|
||||
} catch (e) {
|
||||
console.log("An error occurred while bootstrapping Foam", e);
|
||||
Logger.error("An error occurred while bootstrapping Foam", e);
|
||||
window.showErrorMessage(
|
||||
`An error occurred while bootstrapping Foam. ${e.stack}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
workspaceWatcher?.dispose();
|
||||
if (isDisposable(foam)) {
|
||||
foam?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalMarkdownFile(uri: Uri) {
|
||||
return uri.scheme === "file" && uri.path.match(/\.(md|mdx|markdown)/i);
|
||||
}
|
||||
|
||||
async function registerFile(foam: Foam, localUri: Uri) {
|
||||
// read file from disk (async)
|
||||
const path = localUri.fsPath;
|
||||
const data = await fs.promises.readFile(path);
|
||||
const markdown = (data || "").toString();
|
||||
|
||||
// create note
|
||||
const eol =
|
||||
window.activeTextEditor?.document?.eol === EndOfLine.CRLF ? "\r\n" : "\n";
|
||||
const note = foam.parse(path, markdown, eol);
|
||||
|
||||
// add to graph
|
||||
foam.notes.setNote(note);
|
||||
return note;
|
||||
}
|
||||
|
||||
const bootstrap = async () => {
|
||||
const files = await workspace.findFiles("**/*");
|
||||
const config: FoamConfig = getConfig();
|
||||
const foam = await foamBootstrap(config);
|
||||
const addFile = (uri: Uri) => registerFile(foam, uri);
|
||||
|
||||
await Promise.all(files.filter(isLocalMarkdownFile).map(addFile));
|
||||
|
||||
workspaceWatcher = workspace.createFileSystemWatcher(
|
||||
"**/*",
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
workspaceWatcher.onDidCreate(uri => {
|
||||
if (isLocalMarkdownFile(uri)) {
|
||||
addFile(uri).then(() => {
|
||||
console.log(`Added ${uri} to workspace`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return foam;
|
||||
};
|
||||
|
||||
export const getConfig = (): FoamConfig => {
|
||||
const workspaceFolders = workspace
|
||||
.workspaceFolders!.filter(dir => {
|
||||
const foamPath = path.join(dir.uri.fsPath, ".foam");
|
||||
return fs.existsSync(foamPath) && fs.statSync(foamPath).isDirectory();
|
||||
})
|
||||
.map(dir => dir.uri.fsPath);
|
||||
|
||||
return createConfigFromFolders(workspaceFolders);
|
||||
};
|
||||
|
||||
67
packages/foam-vscode/src/features/create-from-template.ts
Normal file
67
packages/foam-vscode/src/features/create-from-template.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
window,
|
||||
commands,
|
||||
ExtensionContext,
|
||||
workspace,
|
||||
Uri,
|
||||
SnippetString
|
||||
} from "vscode";
|
||||
import * as path from "path";
|
||||
import { FoamFeature } from "../types";
|
||||
import { TextEncoder } from "util";
|
||||
import { focusNote } from "../utils";
|
||||
|
||||
const templatesDir = `${workspace.workspaceFolders[0].uri.fsPath}/.foam/templates`;
|
||||
|
||||
async function getTemplates(): Promise<string[]> {
|
||||
const templates = await workspace.findFiles(".foam/templates/**.md");
|
||||
// parse title, not whole file!
|
||||
return templates.map(template => path.basename(template.fsPath));
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
"foam-vscode.create-note-from-template",
|
||||
async () => {
|
||||
const templates = await getTemplates();
|
||||
const activeFile = window.activeTextEditor?.document?.fileName;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? path.dirname(activeFile)
|
||||
: workspace.workspaceFolders[0].uri.fsPath;
|
||||
const selectedTemplate = await window.showQuickPick(templates);
|
||||
const folder = await window.showInputBox({
|
||||
prompt: `Where should the template be created?`,
|
||||
value: currentDir
|
||||
});
|
||||
|
||||
let filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: ``,
|
||||
validateInput: value =>
|
||||
value.length ? undefined : "Please enter a value!"
|
||||
});
|
||||
filename = path.extname(filename).length
|
||||
? filename
|
||||
: `${filename}.md`;
|
||||
const targetFile = path.join(folder, filename);
|
||||
|
||||
const templateText = await workspace.fs.readFile(
|
||||
Uri.file(`${templatesDir}/${selectedTemplate}`)
|
||||
);
|
||||
const snippet = new SnippetString(templateText.toString());
|
||||
await workspace.fs.writeFile(
|
||||
Uri.file(targetFile),
|
||||
new TextEncoder().encode("")
|
||||
);
|
||||
await focusNote(targetFile, true);
|
||||
await window.activeTextEditor.insertSnippet(snippet);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -1,40 +1,49 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import { FoamFeature } from "../types";
|
||||
import { isNone } from "../utils";
|
||||
import { Foam, Note } from "foam-core";
|
||||
import { Foam } from "foam-core";
|
||||
import { TextDecoder } from "util";
|
||||
import { getTitleMaxLength } from "../settings";
|
||||
import { isSome } from "../utils";
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
vscode.commands.registerCommand("foam-vscode.show-graph", async () => {
|
||||
const foam = await foamPromise;
|
||||
const panel = await createGraphPanel(foam, context)
|
||||
const panel = await createGraphPanel(foam, context);
|
||||
|
||||
const graph = generateGraphData(foam)
|
||||
panel.webview.postMessage({
|
||||
type: "refresh",
|
||||
payload: graph
|
||||
});
|
||||
const onFoamChanged = _ => {
|
||||
updateGraph(panel, foam);
|
||||
};
|
||||
|
||||
const onNoteAdded = _ => {
|
||||
updateGraph(panel, foam)
|
||||
}
|
||||
|
||||
foam.notes.unstable_onNoteAdded(onNoteAdded)
|
||||
const noteAddedListener = foam.notes.onDidAddNote(onFoamChanged);
|
||||
const noteUpdatedListener = foam.notes.onDidUpdateNote(onFoamChanged);
|
||||
const noteDeletedListener = foam.notes.onDidDeleteNote(onFoamChanged);
|
||||
panel.onDidDispose(() => {
|
||||
foam.notes.unstable_removeEventListener(onNoteAdded)
|
||||
noteAddedListener.dispose();
|
||||
noteUpdatedListener.dispose();
|
||||
noteDeletedListener.dispose();
|
||||
});
|
||||
|
||||
updateGraph(panel, foam)
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e.document.uri.scheme === "file") {
|
||||
const note = foam.notes.getNoteByURI(e.document.uri.fsPath);
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
type: "didSelectNote",
|
||||
payload: note.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function updateGraph(panel: vscode.WebviewPanel, foam: Foam) {
|
||||
const graph = generateGraphData(foam)
|
||||
const graph = generateGraphData(foam);
|
||||
panel.webview.postMessage({
|
||||
type: "refresh",
|
||||
type: "didUpdateGraphData",
|
||||
payload: graph
|
||||
});
|
||||
}
|
||||
@@ -42,28 +51,24 @@ function updateGraph(panel: vscode.WebviewPanel, foam: Foam) {
|
||||
function generateGraphData(foam: Foam) {
|
||||
const graph = {
|
||||
nodes: {},
|
||||
edges: new Set(),
|
||||
edges: new Set()
|
||||
};
|
||||
|
||||
foam.notes.getNotes().forEach(n => {
|
||||
const links = foam.notes.getForwardLinks(n.id)
|
||||
const links = foam.notes.getForwardLinks(n.id);
|
||||
graph.nodes[n.id] = {
|
||||
id: n.id,
|
||||
type: "note",
|
||||
uri: n.source.uri,
|
||||
title: n.title,
|
||||
nOutLinks: links.length,
|
||||
nInLinks: graph.nodes[n.id]?.nInLinks ?? 0,
|
||||
title: cutTitle(n.title)
|
||||
};
|
||||
links.forEach(link => {
|
||||
if (!(link.to in graph.nodes)) {
|
||||
graph.nodes[link.to] = {
|
||||
id: link.to,
|
||||
type: "nonExistingNote",
|
||||
uri: "orphan",
|
||||
title: link.link.slug,
|
||||
nOutLinks: graph.nodes[n.id]?.nOutLinks ?? 0,
|
||||
nInLinks: graph.nodes[n.id]?.nInLinks + 1 ?? 0
|
||||
uri: `virtual:${link.to}`,
|
||||
title: cutTitle(link.link.slug)
|
||||
};
|
||||
}
|
||||
graph.edges.add({
|
||||
@@ -73,9 +78,17 @@ function generateGraphData(foam: Foam) {
|
||||
});
|
||||
});
|
||||
return {
|
||||
nodes: Array.from(Object.values(graph.nodes)),
|
||||
edges: Array.from(graph.edges),
|
||||
nodes: graph.nodes,
|
||||
links: Array.from(graph.edges)
|
||||
};
|
||||
}
|
||||
|
||||
function cutTitle(title: string): string {
|
||||
const maxLen = getTitleMaxLength();
|
||||
if (maxLen > 0 && title.length > maxLen) {
|
||||
return title.substring(0, maxLen).concat("...");
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
@@ -89,26 +102,31 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
panel.webview.html = await getWebviewContent(context, panel);
|
||||
|
||||
panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
if (message.type === "selected") {
|
||||
const noteId = message.payload
|
||||
const noteUri = foam.notes.getNote(noteId).source.uri
|
||||
const openPath = vscode.Uri.file(noteUri);
|
||||
message => {
|
||||
switch (message.type) {
|
||||
case "webviewDidLoad":
|
||||
updateGraph(panel, foam);
|
||||
break;
|
||||
|
||||
vscode.workspace.openTextDocument(openPath).then((doc) => {
|
||||
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
});
|
||||
case "webviewDidSelectNode":
|
||||
const noteId = message.payload;
|
||||
const noteUri = foam.notes.getNote(noteId).source.uri;
|
||||
const openPath = vscode.Uri.file(noteUri);
|
||||
|
||||
vscode.workspace.openTextDocument(openPath).then(doc => {
|
||||
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
|
||||
return panel
|
||||
return panel;
|
||||
}
|
||||
|
||||
async function getWebviewContent(
|
||||
@@ -127,8 +145,6 @@ async function getWebviewContent(
|
||||
vscode.Uri.file(path.join(context.extensionPath, "static", fileName))
|
||||
)
|
||||
.toString();
|
||||
const codiconsUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css'));
|
||||
const codiconsFontUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'node_modules', 'vscode-codicons', 'dist', 'codicon.ttf'));
|
||||
|
||||
const graphDirectory = path.join("graphs", "default");
|
||||
const textWithVariables = text
|
||||
@@ -139,22 +155,19 @@ async function getWebviewContent(
|
||||
.replace(
|
||||
"${graphStylesPath}",
|
||||
"{{" + path.join(graphDirectory, "graph.css") + "}}"
|
||||
)
|
||||
.replace(
|
||||
"${styleUri}",
|
||||
codiconsUri.toString()
|
||||
)
|
||||
.replace(
|
||||
"${codiconsUri}",
|
||||
codiconsFontUri.toString()
|
||||
);
|
||||
|
||||
// Basic templating. Will replace the script paths with the
|
||||
// appropriate webview URI.
|
||||
const filled = textWithVariables.replace(/<script data-replace src="([^"]+")/g, (match) => {
|
||||
const fileName = match.slice("<script data-replace src=\"".length, -1).trim();
|
||||
return "<script src=\"" + webviewUri(fileName) + "\"";
|
||||
});
|
||||
const filled = textWithVariables.replace(
|
||||
/<script data-replace src="([^"]+")/g,
|
||||
match => {
|
||||
const fileName = match
|
||||
.slice('<script data-replace src="'.length, -1)
|
||||
.trim();
|
||||
return '<script src="' + webviewUri(fileName) + '"';
|
||||
}
|
||||
);
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import createReferences from "./wikilink-reference-generation";
|
||||
import openDailyNote from "./open-daily-note";
|
||||
import janitor from "./janitor";
|
||||
import dataviz from './dataviz'
|
||||
import dataviz from "./dataviz";
|
||||
import copyWithoutBrackets from "./copy-without-brackets";
|
||||
import openDatedNote from "./open-dated-note";
|
||||
import tagsExplorer from "./tags-tree-view";
|
||||
import createFromTemplate from "./create-from-template";
|
||||
import { FoamFeature } from "../types";
|
||||
|
||||
export const features: FoamFeature[] = [
|
||||
tagsExplorer,
|
||||
createReferences,
|
||||
openDailyNote,
|
||||
janitor,
|
||||
dataviz,
|
||||
copyWithoutBrackets,
|
||||
openDatedNote
|
||||
openDatedNote,
|
||||
createFromTemplate
|
||||
];
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
CompletionItemKind,
|
||||
CompletionList
|
||||
} from "vscode";
|
||||
import { getDailyNoteFileName, openDailyNoteFor } from "../dated-notes";
|
||||
import {
|
||||
createDailyNoteIfNotExists,
|
||||
getDailyNoteFileName,
|
||||
openDailyNoteFor,
|
||||
getDailyNotePath
|
||||
} from "../dated-notes";
|
||||
import { LinkReferenceDefinitionsSetting } from "../settings";
|
||||
import { FoamFeature } from "../types";
|
||||
|
||||
@@ -27,13 +32,15 @@ const daysOfWeek = [
|
||||
{ day: "friday", index: 5 },
|
||||
{ day: "saturday", index: 6 }
|
||||
];
|
||||
|
||||
type AfterCompletionOptions = "noop" | "createNote" | "navigateToNote";
|
||||
const foamConfig = workspace.getConfiguration("foam");
|
||||
const foamExtension = foamConfig.get("openDailyNote.fileExtension");
|
||||
const foamLinkReferenceDefinitions = foamConfig.get(
|
||||
"edit.linkReferenceDefinitions"
|
||||
);
|
||||
const foamNavigateOnSelect = foamConfig.get("snippets.navigateOnSelect");
|
||||
const foamNavigateOnSelect: AfterCompletionOptions = foamConfig.get(
|
||||
"dateSnippets.afterCompletion"
|
||||
);
|
||||
|
||||
const generateDayOfWeekSnippets = (): DateSnippet[] => {
|
||||
const getTarget = (day: number) => {
|
||||
@@ -61,7 +68,7 @@ const createCompletionItem = ({ snippet, date, detail }: DateSnippet) => {
|
||||
);
|
||||
completionItem.insertText = getDailyNoteLink(date);
|
||||
completionItem.detail = `${completionItem.insertText} - ${detail}`;
|
||||
if (foamNavigateOnSelect) {
|
||||
if (foamNavigateOnSelect !== "noop") {
|
||||
completionItem.command = {
|
||||
command: "foam-vscode.open-dated-note",
|
||||
title: "Open a note for the given date",
|
||||
@@ -188,11 +195,24 @@ const computedCompletions: CompletionItemProvider = {
|
||||
}
|
||||
};
|
||||
|
||||
const datedNoteCommand = (date: Date) => {
|
||||
if (foamNavigateOnSelect === "navigateToNote") {
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
if (foamNavigateOnSelect === "createNote") {
|
||||
return createDailyNoteIfNotExists(
|
||||
foamConfig,
|
||||
getDailyNotePath(foamConfig, date),
|
||||
date
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand("foam-vscode.open-dated-note", date =>
|
||||
openDailyNoteFor(date)
|
||||
datedNoteCommand(date)
|
||||
)
|
||||
);
|
||||
languages.registerCompletionItemProvider("markdown", completions, "/");
|
||||
|
||||
156
packages/foam-vscode/src/features/tags-tree-view/index.ts
Normal file
156
packages/foam-vscode/src/features/tags-tree-view/index.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as vscode from "vscode";
|
||||
import { FoamFeature } from "../../types";
|
||||
import { Foam, Note } from "foam-core";
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const provider = new TagsProvider(foam);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
"foam-vscode.tags-explorer",
|
||||
provider
|
||||
)
|
||||
);
|
||||
foam.notes.onDidUpdateNote(() => provider.refresh());
|
||||
}
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<TagTreeItem | undefined | void> = new vscode.EventEmitter<TagTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> = this._onDidChangeTreeData.event;
|
||||
|
||||
private tags: {
|
||||
tag: string;
|
||||
noteIds: string[];
|
||||
}[];
|
||||
|
||||
constructor(private foam: Foam) {
|
||||
this.computeTags();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.computeTags();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private computeTags() {
|
||||
const rawTags: {
|
||||
[key: string]: string[];
|
||||
} = this.foam.notes.getNotes().reduce((acc, note) => {
|
||||
note.tags.forEach(tag => {
|
||||
acc[tag] = acc[tag] ?? [];
|
||||
acc[tag].push(note.id);
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
this.tags = Object.entries(rawTags)
|
||||
.map(([tag, noteIds]) => ({ tag, noteIds }))
|
||||
.sort((a, b) => a.tag.localeCompare(b.tag));
|
||||
}
|
||||
|
||||
getTreeItem(element: TagTreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
|
||||
if (element) {
|
||||
const references: TagReference[] = element.noteIds.map(id => {
|
||||
const note = this.foam.notes.getNote(id);
|
||||
return new TagReference(element.tag, note);
|
||||
});
|
||||
return Promise.resolve([
|
||||
new TagSearch(element.tag),
|
||||
...references.sort((a, b) => a.title.localeCompare(b.title))
|
||||
]);
|
||||
}
|
||||
if (!element) {
|
||||
const tags: Tag[] = this.tags.map(
|
||||
({ tag, noteIds }) => new Tag(tag, noteIds)
|
||||
);
|
||||
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TagTreeItem = Tag | TagReference | TagSearch;
|
||||
|
||||
export class Tag extends vscode.TreeItem {
|
||||
constructor(public readonly tag: string, public readonly noteIds: string[]) {
|
||||
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.description = `${this.noteIds.length} reference${
|
||||
this.noteIds.length !== 1 ? "s" : ""
|
||||
}`;
|
||||
this.tooltip = this.description;
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon("symbol-number");
|
||||
contextValue = "tag";
|
||||
}
|
||||
|
||||
export class TagSearch extends vscode.TreeItem {
|
||||
constructor(public readonly tag: string) {
|
||||
super(`Search #${tag}`, vscode.TreeItemCollapsibleState.None);
|
||||
const searchString = `#${tag}`;
|
||||
this.tooltip = `Search ${searchString} in workspace`;
|
||||
this.command = {
|
||||
command: "workbench.action.findInFiles",
|
||||
arguments: [
|
||||
{
|
||||
query: searchString,
|
||||
triggerSearch: true,
|
||||
matchWholeWord: true,
|
||||
isCaseSensitive: true
|
||||
}
|
||||
],
|
||||
title: "Search"
|
||||
};
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon("search");
|
||||
contextValue = "tag-search";
|
||||
}
|
||||
|
||||
export class TagReference extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(tag: string, note: Note) {
|
||||
super(note.title, vscode.TreeItemCollapsibleState.None);
|
||||
this.title = note.title;
|
||||
this.description = note.source.uri;
|
||||
this.tooltip = this.description;
|
||||
const resourceUri = vscode.Uri.file(note.source.uri);
|
||||
let selection: vscode.Range | null = null;
|
||||
// TODO move search fn to core
|
||||
const lines = note.source.text.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const found = lines[i].indexOf(`#${tag}`);
|
||||
if (found >= 0) {
|
||||
selection = new vscode.Range(i, found, i, found + `#${tag}`.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO I like about this showing the git state of the note, but I don't like the md icon
|
||||
this.resourceUri = resourceUri;
|
||||
this.command = {
|
||||
command: "vscode.open",
|
||||
arguments: [
|
||||
resourceUri,
|
||||
{
|
||||
preview: true,
|
||||
selection: selection
|
||||
}
|
||||
],
|
||||
title: "Open File"
|
||||
};
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon("note");
|
||||
contextValue = "reference";
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const feature: FoamFeature = {
|
||||
|
||||
// when a file is created as a result of peekDefinition
|
||||
// action on a wikilink, add definition update references
|
||||
foam.notes.unstable_onNoteAdded(e => {
|
||||
foam.notes.onDidAddNote(_ => {
|
||||
let editor = window.activeTextEditor;
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
|
||||
17
packages/foam-vscode/src/services/config.ts
Normal file
17
packages/foam-vscode/src/services/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { workspace } from "vscode";
|
||||
import { FoamConfig, createConfigFromFolders } from "foam-core";
|
||||
import { getIgnoredFilesSetting } from "../settings";
|
||||
|
||||
// TODO this is still to be improved - foam config should
|
||||
// not be dependent on vscode but at the moment it's convenient
|
||||
// to leverage it
|
||||
export const getConfigFromVscode = (): FoamConfig => {
|
||||
const workspaceFolders = workspace.workspaceFolders.map(
|
||||
dir => dir.uri.fsPath
|
||||
);
|
||||
const excludeGlobs: string[] = getIgnoredFilesSetting();
|
||||
|
||||
return createConfigFromFolders(workspaceFolders, {
|
||||
ignore: excludeGlobs
|
||||
});
|
||||
};
|
||||
56
packages/foam-vscode/src/services/logging.ts
Normal file
56
packages/foam-vscode/src/services/logging.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { window, commands, ExtensionContext } from "vscode";
|
||||
import { ILogger, IDisposable, LogLevel, BaseLogger } from "foam-core";
|
||||
import { getFoamLoggerLevel } from "../settings";
|
||||
|
||||
export interface VsCodeLogger extends ILogger, IDisposable {
|
||||
show();
|
||||
}
|
||||
|
||||
export class VsCodeOutputLogger extends BaseLogger implements VsCodeLogger {
|
||||
private channel = window.createOutputChannel("Foam");
|
||||
|
||||
constructor() {
|
||||
super(getFoamLoggerLevel());
|
||||
this.channel.appendLine("Foam Logging: " + getFoamLoggerLevel());
|
||||
}
|
||||
|
||||
log(lvl: LogLevel, msg?: any, ...extra: any[]): void {
|
||||
if (msg) {
|
||||
this.channel.appendLine(
|
||||
`[${lvl} - ${new Date().toLocaleTimeString()}] ${msg}`
|
||||
);
|
||||
}
|
||||
extra?.forEach(param => {
|
||||
if (param?.stack) {
|
||||
this.channel.appendLine(JSON.stringify(param.stack, null, 2));
|
||||
} else {
|
||||
this.channel.appendLine(JSON.stringify(param, null, 2));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.channel.show();
|
||||
}
|
||||
dispose(): void {
|
||||
this.channel.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export const exposeLogger = (
|
||||
context: ExtensionContext,
|
||||
logger: VsCodeLogger
|
||||
): void => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand("foam-vscode.set-log-level", async () => {
|
||||
const items: LogLevel[] = ["debug", "info", "warn", "error"];
|
||||
const level = await window.showQuickPick(
|
||||
items.map(item => ({
|
||||
label: item,
|
||||
description: item === logger.getLevel() && "Current"
|
||||
}))
|
||||
);
|
||||
logger.setLevel(level.label);
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { workspace } from "vscode";
|
||||
import { LogLevel } from "foam-core";
|
||||
|
||||
export enum LinkReferenceDefinitionsSetting {
|
||||
withExtensions = "withExtensions",
|
||||
@@ -14,3 +15,17 @@ export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting
|
||||
LinkReferenceDefinitionsSetting.withoutExtensions
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieve the list of file ignoring globs. */
|
||||
export function getIgnoredFilesSetting(): string[] {
|
||||
return workspace.getConfiguration("foam.files").get("ignore");
|
||||
}
|
||||
|
||||
/** Retrieves the maximum length for a Graph node title. */
|
||||
export function getTitleMaxLength(): number {
|
||||
return workspace.getConfiguration("foam.graph").get("titleMaxLength");
|
||||
}
|
||||
|
||||
export function getFoamLoggerLevel(): LogLevel {
|
||||
return workspace.getConfiguration("foam.logging").get("level") ?? "info";
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import {
|
||||
TextDocument,
|
||||
window,
|
||||
Position,
|
||||
TextEditor
|
||||
TextEditor,
|
||||
workspace,
|
||||
Uri,
|
||||
Selection
|
||||
} from "vscode";
|
||||
import * as fs from "fs";
|
||||
import { Logger } from "foam-core";
|
||||
|
||||
interface Point {
|
||||
line: number;
|
||||
@@ -25,7 +29,7 @@ export function loadDocConfig() {
|
||||
// Load workspace config
|
||||
let activeEditor = window.activeTextEditor;
|
||||
if (!activeEditor) {
|
||||
console.log("Failed to load config, no active editor");
|
||||
Logger.debug("Failed to load config, no active editor");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +158,8 @@ export function pathExists(path: string) {
|
||||
* @param value The object to verify
|
||||
*/
|
||||
export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
return value != null;
|
||||
//
|
||||
return value != null; // eslint-disable-line
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +167,20 @@ export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
*
|
||||
* @param value The object to verify
|
||||
*/
|
||||
export function isNone<T>(value: T | null | undefined | void): value is null | undefined | void {
|
||||
return value == null;
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null; // eslint-disable-line
|
||||
}
|
||||
|
||||
export async function focusNote(notePath: string, moveCursorToEnd: boolean) {
|
||||
const document = await workspace.openTextDocument(Uri.file(notePath));
|
||||
const editor = await window.showTextDocument(document);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (moveCursorToEnd) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- <link rel="stylesheet" href="{{main.css}}" />
|
||||
<link rel="stylesheet" href="${graphStylesPath}" />
|
||||
<link rel="stylesheet" href="${styleUri}" />
|
||||
<link rel="stylesheet" href="${codiconsUri}" /> -->
|
||||
|
||||
<script data-replace src="./g6.min.js"></script>
|
||||
<script data-replace src="./d3.v6.min.js"></script>
|
||||
<script data-replace src="./force-graph.1.34.1.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graph" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
|
||||
|
||||
@@ -1,195 +1,302 @@
|
||||
try {
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case "refresh":
|
||||
const data = message.payload;
|
||||
createWebGLGraph(data, vscode);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.log("VSCode not detected")
|
||||
}
|
||||
|
||||
const CONTAINER_ID = "graph";
|
||||
|
||||
function getStyle(name, fallback) {
|
||||
return (
|
||||
getComputedStyle(document.documentElement).getPropertyValue(name) ||
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
const style = {
|
||||
backgroundColor: "#202020",
|
||||
background: getStyle(`--vscode-panel-background`, "#202020"),
|
||||
fontSize: parseInt(getStyle(`--vscode-font-size`, 12)),
|
||||
highlightedForeground: getStyle(
|
||||
"--vscode-list-highlightForeground",
|
||||
"#f9c74f"
|
||||
),
|
||||
node: {
|
||||
note: "#277da1",
|
||||
nonExistingNote: "#545454",
|
||||
attachment: "#43aa8b",
|
||||
externalResource: "#f8961e",
|
||||
tag: "#f3722c",
|
||||
unknown: "#f94144"
|
||||
},
|
||||
link: {
|
||||
highlighted: "#f9c74f",
|
||||
regular: "#277da1",
|
||||
},
|
||||
note: getStyle("--vscode-editor-foreground", "#277da1"),
|
||||
nonExistingNote: getStyle(
|
||||
"--vscode-list-deemphasizedForeground",
|
||||
"#545454"
|
||||
),
|
||||
unknown: getStyle("--vscode-editor-foreground", "#f94144")
|
||||
}
|
||||
};
|
||||
|
||||
const sizeScale = d3.scaleLinear()
|
||||
.domain([0, 30]).range([4, 10])
|
||||
const sizeScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, 30])
|
||||
.range([1, 3])
|
||||
.clamp(true);
|
||||
|
||||
function createWebGLGraph(data, channel) {
|
||||
data = convertData(data)
|
||||
let model = updateModel(null, null)
|
||||
const labelAlpha = d3
|
||||
.scaleLinear()
|
||||
.domain([1.2, 2])
|
||||
.range([0, 1])
|
||||
.clamp(true);
|
||||
|
||||
const elem = document.getElementById(CONTAINER_ID)
|
||||
const myGraph = ForceGraph();
|
||||
myGraph(elem)
|
||||
.graphData(data)
|
||||
.backgroundColor(style.backgroundColor)
|
||||
let model = {
|
||||
selectedNodes: new Set(),
|
||||
hoverNode: null,
|
||||
focusNodes: new Set(),
|
||||
focusLinks: new Set(),
|
||||
nodeInfo: {},
|
||||
data: {
|
||||
nodes: [],
|
||||
links: []
|
||||
}
|
||||
};
|
||||
const graph = ForceGraph();
|
||||
|
||||
function update(patch) {
|
||||
// Apply the patch function to the model..
|
||||
patch(model);
|
||||
// ..then compute the derived state
|
||||
|
||||
// compute highlighted elements
|
||||
const focusNodes = new Set();
|
||||
const focusLinks = new Set();
|
||||
if (model.hoverNode) {
|
||||
focusNodes.add(model.hoverNode);
|
||||
const info = model.nodeInfo[model.hoverNode];
|
||||
info.neighbors.forEach(neighborId => focusNodes.add(neighborId));
|
||||
info.links.forEach(link => focusLinks.add(link));
|
||||
}
|
||||
if (model.selectedNodes) {
|
||||
model.selectedNodes.forEach(nodeId => {
|
||||
focusNodes.add(nodeId);
|
||||
const info = model.nodeInfo[nodeId];
|
||||
info.neighbors.forEach(neighborId => focusNodes.add(neighborId));
|
||||
info.links.forEach(link => focusLinks.add(link));
|
||||
});
|
||||
}
|
||||
model.focusNodes = focusNodes;
|
||||
model.focusLinks = focusLinks;
|
||||
}
|
||||
|
||||
const Actions = {
|
||||
refresh: graphInfo =>
|
||||
update(m => {
|
||||
m.nodeInfo = graphInfo.nodes;
|
||||
const links = graphInfo.links;
|
||||
|
||||
// compute graph delta, for smooth transitions we need to mutate objects in-place
|
||||
const remaining = new Set(Object.keys(m.nodeInfo));
|
||||
m.data.nodes.forEach((node, index, object) => {
|
||||
if (remaining.has(node.id)) {
|
||||
remaining.delete(node.id);
|
||||
} else {
|
||||
object.splice(index, 1); // delete the element
|
||||
}
|
||||
});
|
||||
remaining.forEach(nodeId => {
|
||||
m.data.nodes.push({
|
||||
id: nodeId
|
||||
});
|
||||
});
|
||||
m.data.links = links; // links can be swapped out without problem
|
||||
|
||||
// annoying we need to call this function, but I haven't found a good workaround
|
||||
graph.graphData(m.data);
|
||||
}),
|
||||
selectNode: (nodeId, isAppend) =>
|
||||
update(m => {
|
||||
if (!isAppend) {
|
||||
m.selectedNodes.clear();
|
||||
}
|
||||
if (nodeId != null) {
|
||||
m.selectedNodes.add(nodeId);
|
||||
}
|
||||
}),
|
||||
highlightNode: nodeId =>
|
||||
update(m => {
|
||||
m.hoverNode = nodeId;
|
||||
})
|
||||
};
|
||||
|
||||
function initDataviz(channel) {
|
||||
const elem = document.getElementById(CONTAINER_ID);
|
||||
graph(elem)
|
||||
.graphData(model.data)
|
||||
.backgroundColor(style.background)
|
||||
.linkHoverPrecision(8)
|
||||
.d3Force("x", d3.forceX())
|
||||
.d3Force("y", d3.forceY())
|
||||
.d3Force("collide", d3.forceCollide(graph.nodeRelSize()))
|
||||
.linkWidth(0.5)
|
||||
.linkDirectionalParticles(1)
|
||||
.linkDirectionalParticleWidth(
|
||||
link => getLinkState(link, model) === "highlighted" ? 2 : 0
|
||||
.linkDirectionalParticleWidth(link =>
|
||||
getLinkState(link, model) === "highlighted" ? 1 : 0
|
||||
)
|
||||
.nodeVal(node => sizeScale(node.nInLinks + node.nOutLinks))
|
||||
.nodeCanvasObject((node, ctx) => {
|
||||
const size = sizeScale(node.nInLinks + node.nOutLinks);
|
||||
const { fill, border } = getNodeColor(node, model)
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size + 0.5, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = border;
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
.nodeCanvasObject((node, ctx, globalScale) => {
|
||||
const info = model.nodeInfo[node.id];
|
||||
const size = sizeScale(info.neighbors.length);
|
||||
const { fill, border } = getNodeColor(node.id, model);
|
||||
const fontSize = style.fontSize / globalScale;
|
||||
let textColor = d3.rgb(fill);
|
||||
textColor.opacity =
|
||||
getNodeState(node.id, model) === "highlighted"
|
||||
? 1
|
||||
: labelAlpha(globalScale);
|
||||
const label = info.title;
|
||||
|
||||
Draw(ctx)
|
||||
.circle(node.x, node.y, size + 0.5, border)
|
||||
.circle(node.x, node.y, size, fill)
|
||||
.text(label, node.x, node.y + size + 1, fontSize, textColor);
|
||||
})
|
||||
.linkColor(link => getLinkColor(link, model))
|
||||
.onNodeHover(node => {
|
||||
model = updateModel(model.selectedNode, node)
|
||||
Actions.highlightNode(node?.id);
|
||||
})
|
||||
.onNodeClick((node, event) => {
|
||||
if (event.getModifierState("Control") || event.getModifierState("Meta")) {
|
||||
channel.postMessage({
|
||||
type: "selected",
|
||||
payload: node.id,
|
||||
type: "webviewDidSelectNode",
|
||||
payload: node.id
|
||||
});
|
||||
}
|
||||
model = updateModel(node, model.hoverNode)
|
||||
Actions.selectNode(node.id, event.getModifierState("Shift"));
|
||||
})
|
||||
.onBackgroundClick(e => {
|
||||
model = updateModel(null, model.hoverNode)
|
||||
.onBackgroundClick(event => {
|
||||
Actions.selectNode(null, event.getModifierState("Shift"));
|
||||
});
|
||||
}
|
||||
|
||||
function convertData(raw) {
|
||||
data = {
|
||||
nodes: raw.nodes.map(n => ({
|
||||
...n,
|
||||
name: n.title,
|
||||
neighbors: [],
|
||||
links: [],
|
||||
})),
|
||||
links: raw.edges
|
||||
};
|
||||
|
||||
const nodeIdToIndex = data.nodes.reduce((acc, node, idx) => {
|
||||
acc[node.id] = idx;
|
||||
return acc;
|
||||
}, {});
|
||||
function augmentGraphInfo(data) {
|
||||
Object.values(data.nodes).forEach(node => {
|
||||
node.neighbors = [];
|
||||
node.links = [];
|
||||
});
|
||||
data.links.forEach(link => {
|
||||
const a = data.nodes[nodeIdToIndex[link.source]];
|
||||
const b = data.nodes[nodeIdToIndex[link.target]];
|
||||
!a.neighbors && (a.neighbors = []);
|
||||
!b.neighbors && (b.neighbors = []);
|
||||
a.neighbors.push(b);
|
||||
b.neighbors.push(a);
|
||||
|
||||
!a.links && (a.links = []);
|
||||
!b.links && (b.links = []);
|
||||
const a = data.nodes[link.source];
|
||||
const b = data.nodes[link.target];
|
||||
a.neighbors.push(b.id);
|
||||
b.neighbors.push(a.id);
|
||||
a.links.push(link);
|
||||
b.links.push(link);
|
||||
});
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
function getNodeColor(node, model) {
|
||||
const typeFill = style.node[node.type || "unknown"];
|
||||
switch (getNodeState(node, model)) {
|
||||
function getNodeColor(nodeId, model) {
|
||||
const info = model.nodeInfo[nodeId];
|
||||
const typeFill = style.node[info.type || "unknown"];
|
||||
switch (getNodeState(nodeId, model)) {
|
||||
case "regular":
|
||||
return { fill: typeFill, border: typeFill}
|
||||
return { fill: typeFill, border: typeFill };
|
||||
case "lessened":
|
||||
const darker = d3.hsl(typeFill).darker(3);
|
||||
return { fill: darker, border: darker}
|
||||
return { fill: darker, border: darker };
|
||||
case "highlighted":
|
||||
return { fill: typeFill, border: "#f9c74f"}
|
||||
return {
|
||||
fill: typeFill,
|
||||
border: style.highlightedForeground
|
||||
};
|
||||
default:
|
||||
throw new Error("Unknown type for node", node)
|
||||
throw new Error("Unknown type for node", nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
function getLinkColor(link, model) {
|
||||
switch(getLinkState(link, model)) {
|
||||
case "regular":
|
||||
return style.link.regular
|
||||
case "highlighted":
|
||||
return style.link.highlighted
|
||||
case "lessened":
|
||||
return d3.hsl(style.link.regular).darker(3);
|
||||
default:
|
||||
throw new Error("Unknown type for link", link)
|
||||
switch (getLinkState(link, model)) {
|
||||
case "regular":
|
||||
return d3.hsl(style.node.note).darker(2);
|
||||
case "highlighted":
|
||||
return style.highlightedForeground;
|
||||
case "lessened":
|
||||
return d3.hsl(style.node.note).darker(4);
|
||||
default:
|
||||
throw new Error("Unknown type for link", link);
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeState(node, model) {
|
||||
return model.selectedNode?.id === node.id || model.hoverNode?.id === node.id
|
||||
function getNodeState(nodeId, model) {
|
||||
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId
|
||||
? "highlighted"
|
||||
: model.focusNodes.size === 0
|
||||
? "regular"
|
||||
: model.focusNodes.has(node)
|
||||
? "regular"
|
||||
: "lessened";
|
||||
? "regular"
|
||||
: model.focusNodes.has(nodeId)
|
||||
? "regular"
|
||||
: "lessened";
|
||||
}
|
||||
|
||||
function getLinkState(link, model) {
|
||||
return model.focusNodes.size === 0
|
||||
? "regular"
|
||||
: model.focusLinks.has(link)
|
||||
? "highlighted"
|
||||
: "lessened";
|
||||
? "highlighted"
|
||||
: "lessened";
|
||||
}
|
||||
|
||||
function updateModel(selectedNode, hoverNode) {
|
||||
const focusNodes = new Set()
|
||||
const focusLinks = new Set()
|
||||
if (hoverNode) {
|
||||
focusNodes.add(hoverNode);
|
||||
hoverNode.neighbors.forEach(neighbor => focusNodes.add(neighbor));
|
||||
hoverNode.links.forEach(link => focusLinks.add(link));
|
||||
}
|
||||
if (selectedNode) {
|
||||
focusNodes.add(selectedNode);
|
||||
selectedNode.neighbors.forEach(neighbor => focusNodes.add(neighbor));
|
||||
selectedNode.links.forEach(link => focusLinks.add(link));
|
||||
}
|
||||
return {
|
||||
focusNodes: focusNodes,
|
||||
focusLinks: focusLinks,
|
||||
selectedNode: selectedNode,
|
||||
hoverNode: hoverNode,
|
||||
const Draw = ctx => ({
|
||||
circle: function(x, y, radius, color) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
return this;
|
||||
},
|
||||
text: function(text, x, y, size, color) {
|
||||
ctx.font = `${size}px Sans-Serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillText(text, x, y);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// init the app
|
||||
try {
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
window.onload = () => {
|
||||
initDataviz(vscode);
|
||||
console.log("ready");
|
||||
vscode.postMessage({
|
||||
type: "webviewDidLoad"
|
||||
});
|
||||
};
|
||||
window.addEventListener("message", event => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case "didUpdateGraphData":
|
||||
const graphData = augmentGraphInfo(message.payload);
|
||||
console.log("didUpdateGraphData", graphData);
|
||||
Actions.refresh(graphData);
|
||||
break;
|
||||
case "didSelectNote":
|
||||
const noteId = message.payload;
|
||||
const node = graph.graphData().nodes.find(node => node.id === noteId);
|
||||
if (node) {
|
||||
graph.centerAt(node.x, node.y, 300).zoom(3, 300);
|
||||
Actions.selectNode(noteId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.log("VsCode not detected");
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
graph.width(window.innerWidth).height(window.innerHeight);
|
||||
});
|
||||
|
||||
// For testing
|
||||
window.onload = () => {
|
||||
if (window.data) {
|
||||
createWebGLGraph(window.data, {
|
||||
if (window.data) {
|
||||
console.log("Test mode");
|
||||
window.model = model;
|
||||
window.graph = graph;
|
||||
window.onload = () => {
|
||||
initDataviz({
|
||||
postMessage: message => console.log("message", message)
|
||||
});
|
||||
}
|
||||
};
|
||||
const graphData = augmentGraphInfo(window.data);
|
||||
Actions.refresh(graphData);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "out",
|
||||
"lib": ["es6"],
|
||||
"sourceMap": true,
|
||||
"strict": false
|
||||
"strict": false,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"],
|
||||
|
||||
21
readme.md
21
readme.md
@@ -1,10 +1,10 @@
|
||||
👀*This is an early stage project under rapid development. For updates follow [@jevakallio](https://twitter.com/jevakallio) on Twitter, or join the [Foam community Discord](https://discord.gg/rtdZKgj)! 💬*
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://discord.gg/rtdZKgj)! 💬*
|
||||
|
||||
|
||||
# Foam
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#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/).
|
||||
@@ -85,7 +85,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
@@ -113,6 +113,21 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -2681,6 +2681,11 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/braces@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb"
|
||||
integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==
|
||||
|
||||
"@types/color-name@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
@@ -2773,6 +2778,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8"
|
||||
integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==
|
||||
|
||||
"@types/micromatch@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
|
||||
integrity sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==
|
||||
dependencies:
|
||||
"@types/braces" "*"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
@@ -2808,6 +2820,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/picomatch@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.2.1.tgz#f9e5a5e6ad03996832975ab7eadfa35791ca2a8f"
|
||||
integrity sha512-26/tQcDmJXYHiaWAAIjnTVL5nwrT+IVaqFZIbBImAuKk/r/j1r/1hmZ7uaOzG6IknqP3QHcNNQ6QO8Vp28lUoA==
|
||||
|
||||
"@types/prettier@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3"
|
||||
|
||||
Reference in New Issue
Block a user