Compare commits

...

65 Commits

Author SHA1 Message Date
Riccardo Ferretti
4e624d5cf9 v0.7.2 2020-11-28 18:11:01 +01:00
Riccardo Ferretti
42ec29d3e9 Updated changelog for 0.7.2 2020-11-28 18:10:08 +01:00
Riccardo
26ab27e06f Note deletion, events testing, and improved CI
Added API and events around note deletion

Improved github workflows, added logic to avoid duplicate runs in CI and merged build + test jobs

Added support for running workflows in multiple environments, commented window-2019 as test don't pass, but they will be fixed in another PR to avoid scope creep here.
2020-11-28 17:52:02 +01:00
Sanket Dasgupta
6073bf143e WIP: Fix wikilinks slug querying (#386)
* WIP: Fix wikilinks slug querying

* Fix tests
2020-11-28 15:04:36 +01:00
Jonathan Carter
865bb95745 Updating the GistPad documentation (#379) 2020-11-27 14:54:30 -07:00
Riccardo
fa908bb4c6 tweaked issue templates (#385) 2020-11-27 18:18:25 +01:00
Riccardo Ferretti
83afa873dc v0.7.1 2020-11-27 16:12:04 +01:00
Riccardo Ferretti
69c3f5fb25 Preparation for 0.7.1 2020-11-27 15:50:15 +01:00
Riccardo
6152e89590 Adding service that can direct logging to vscode console (#377)
* Improved logging
- using classes instead of functions (feels like it fits better the use case)
- using singleton global to not pass logging service around

* Added vscode logger, command to change level, and settings

* improved bootstrap logging

* build foam-core before running tests in github workflows
2020-11-27 15:38:19 +01:00
Riccardo
9f17b1f7b9 Fixed parsing of tags (#382)
* fixed parsing of tags
* improved regex, courtesy of @jmg-duarte!
2020-11-27 13:35:39 +01:00
Riccardo
8f1327337c adjusting canvas size on window resize (and removing scrollbars) (#383) 2020-11-27 10:07:42 +01:00
allcontributors[bot]
b15f27aea6 docs: add jmg-duarte as a contributor (#384)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-27 00:13:15 +01:00
José Duarte
b8a16cc5ed Fix title cutting on slugs (#381) 2020-11-26 23:46:44 +01:00
José Duarte
168ef5edb4 Simplify the contribution guide (#372)
* Simplify the contribution guide

* Remove outdated files

* Update the contribution guide

* Add call to fixes on the contrib-guide bottom

* Apply suggestions from code review

Co-authored-by: Riccardo <code@riccardoferretti.com>

Co-authored-by: Riccardo <code@riccardoferretti.com>
2020-11-26 21:25:03 +00:00
Riccardo Ferretti
788ccbd905 v0.7.0 2020-11-25 16:44:47 +01:00
Riccardo Ferretti
cdaeefb252 more babel tweaks 2020-11-25 14:58:10 +01:00
Riccardo Ferretti
ddbf365313 preparation for 0.7.0 2020-11-25 14:22:29 +01:00
Riccardo Ferretti
1e327a4cc6 tweaking babel setting
because of a runtime error in vscode, presented only with the packaged extension https://github.com/formium/tsdx/issues/547
2020-11-25 14:20:04 +01:00
Riccardo Ferretti
391f4d6d07 updated CHANGELOG 2020-11-25 13:20:10 +01:00
Riccardo Ferretti
befdeb70e0 using native matchAll string function 2020-11-25 13:15:22 +01:00
José Duarte
a086a75e37 Add node_modules to the default Foam ignore list (#371)
* Add explicit foam ignores

* Add node_modules to the default ignores by Foam
2020-11-24 22:24:02 +01:00
Riccardo
9b886d3b27 Dataviz: graph smooth update and theme (#360)
* graph uses vscode colors

* added fallback value to style function

* graph now updates smoothly

* added support for multiple selections in graph
2020-11-24 18:29:16 +01:00
allcontributors[bot]
ecfa04cc4b docs: add ShaunaGordon as a contributor (#368)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-24 18:28:53 +01:00
Shauna Gordon
08e51fcbc8 Add additional feature extensions (#361) 2020-11-24 18:28:31 +01:00
Riccardo
15f412cac4 Show message to user when foam fails to bootstrap (#364) 2020-11-23 13:08:14 +01:00
Riccardo
d054e19eae Keep foam in sync with file system (#349)
* added common code from vscode repo

lots of good utility functions and objects, especially around lifecycle and event management

* added datastore and logger services

* refactored bootstrap to consolidate behavior in foam-core

* tags treeview now updates when files are saved

* updated node engine version to match vscode's

* using new event model for foam graph events
2020-11-20 12:04:07 +01:00
Riccardo Ferretti
846908e9d2 v0.6.0 2020-11-19 17:59:04 +01:00
Riccardo Ferretti
da69cc0f5d improved publishing scripts 2020-11-19 17:57:56 +01:00
Riccardo Ferretti
76a9a4ac93 Prepare for 0.6.0 release 2020-11-19 17:03:06 +01:00
allcontributors[bot]
138217e39d docs: add SanketDG as a contributor (#354)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-18 22:20:10 +01:00
Sanket Dasgupta
b473749260 Fix: wikilinks are slugified when looking for linked note (#353) 2020-11-18 22:18:08 +01:00
ingalless
fa01cce934 Feature/templates (#346)
* Initial work to create new note from template

* Treat template as snippet string

* Small refactor

* Improve semantics of focusNote

* Ask for filename, not title

Authored-by: Jonathan Ingall <jonny@mondago.com>
2020-11-18 22:12:37 +01:00
Riccardo
44e498dddb Center graph on active note, plus other tweaks (#352)
* tweaked label display

* #319 highlight and center active document in graph

* minor style tweaks to graph
2020-11-18 18:32:16 +00:00
allcontributors[bot]
679a2947d2 docs: add litanlitudan as a contributor (#351) 2020-11-18 12:25:05 +01:00
Tan Li
8710438a46 Minor fix on the icon location (#350)
Co-authored-by: Tan Li <tan.li@sambanovasystems.com>
2020-11-18 12:24:36 +01:00
José Duarte
90f869e8d0 Refactor the bootstrap function to be simpler (#344)
* Add documentation to the settings getters

* Reduce code noise in the bootstrap method

Files are now all processed in one method,
without `filter`s or `map`s to avoid reallocating arrays.

`addFile` is now inlined, as its function was mostly noise and became
unused.
`isLocalMarkdown` is now done right after listing all workspace files,
before applying the ignore globs.
`registerFiles` merely deals with registering several files,
instead of requiring a loop each time several files are to be
registered.

* Add documentation and type annotations

* Rephrase filterAndRegister docstring
2020-11-11 20:16:23 +01:00
hikerpig
8a7e9bcdd4 docs: update gitlab-pages.md for demo site of foam-jekyll-template (#345) 2020-11-11 20:00:05 +01:00
allcontributors[bot]
e68b6e3023 docs: add asifm as a contributor (#341)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:55:56 +01:00
allcontributors[bot]
ba84b9b496 docs: add jbn as a contributor (#340)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:55:12 +01:00
allcontributors[bot]
af5a4a20e6 docs: add Jackiexiao as a contributor (#339)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:54:01 +01:00
allcontributors[bot]
38181acd53 docs: add scott-joe as a contributor (#338)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:52:13 +01:00
allcontributors[bot]
683c28e393 docs: add umbrellait-danil-rodin as a contributor (#337)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:51:07 +01:00
allcontributors[bot]
f9444636e2 docs: add tristansokol as a contributor (#336)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:48:58 +01:00
allcontributors[bot]
fd0b2ef912 docs: add Sigfried as a contributor (#335)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:44:31 +01:00
allcontributors[bot]
b911d5b7b1 docs: add hikerpig as a contributor (#334)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:43:07 +01:00
allcontributors[bot]
7287aa62b5 docs: add yenly as a contributor (#333)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:42:15 +01:00
allcontributors[bot]
0475d26f2c docs: add jmg-duarte as a contributor (#332)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Riccardo <code@riccardoferretti.com>
2020-11-10 12:40:08 +01:00
allcontributors[bot]
bf43113fac docs: add ingalless as a contributor (#331)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:38:15 +01:00
allcontributors[bot]
81639fd650 docs: add ingalless as a contributor (#330)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-11-10 12:35:26 +01:00
José Duarte
8d110eb04b Add Foam icon to the extension (#328) 2020-11-09 22:18:40 +01:00
José Duarte
d89b6f0285 Add title cutting after given length (#327) 2020-11-09 21:08:12 +01:00
José Duarte
dbdb4c30b8 Improve ignore setting description & defaults (#326)
Add a simple reminder on folder ignore and add defaults aligned with it
2020-11-09 21:03:20 +01:00
Riccardo Ferretti
fdc2c7cf4c v0.5.0 2020-11-09 17:41:16 +01:00
Riccardo Ferretti
b0536ce9f7 v0.5.0-alpha.2 2020-11-09 17:26:58 +01:00
Riccardo Ferretti
a60dcaa52a updated readme 2020-11-09 17:23:01 +01:00
Riccardo Ferretti
799caa96a6 v0.5.0-alpha.1 2020-11-09 16:45:25 +01:00
Riccardo Ferretti
fb2ff3ac95 updated changelog 2020-11-09 16:41:47 +01:00
Riccardo Ferretti
39a96a2d02 v0.5.0-alpha.0 2020-11-09 16:24:44 +01:00
Riccardo
6e5c138f31 improved error handling when parsing markdown files (#320) 2020-11-09 13:23:51 +01:00
Riccardo
b2b1b58262 Adding support for tags (#311) 2020-11-06 19:22:14 +01:00
José Duarte
c10b73c59c Add file ignore setting (issue #300) (#304) 2020-11-03 11:36:45 +01:00
Yenly
74591eb192 docs: Update Publish recipes to link to community templates (#289)
* docs: Update Publish recipes to link to community templates

* Add instructions on how to deploy foam-gatsby-template to vercel
2020-11-02 21:26:16 +01:00
hikerpig
b2ebd82f25 Fix error in cli command janitor & migrate (#312)
* Fix error caused by 'createConfigFromFolders' in command janitor & migrate

* style: fix lint error
2020-11-02 15:35:35 +01:00
Riccardo
b2c4e9f78b Graph v0.2 - fixing bugs and adding labels (#310)
* added labels to graph
* fixed counter of in/out links in graph generation
* fixed linting errors
* removed unnecessary code
2020-10-30 19:09:12 +01:00
ingalless
b22fd50394 Provide more options for date snippets completion actions (#307)
Co-authored-by: Jonathan Ingall <jonny@mondago.com>
2020-10-30 07:12:26 +00:00
77 changed files with 3985 additions and 719 deletions

View File

@@ -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,

View File

@@ -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!

View File

@@ -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*

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View 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"

View File

@@ -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"

View File

@@ -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"

View 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!

View File

@@ -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`)

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View File

@@ -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)

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.4.0"
"version": "0.7.2"
}

View File

@@ -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": {

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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",

View File

@@ -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;
};

View 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();
}
}
}

View 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;
}
}
}

View 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();
}
}

View 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;
}

View 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;
},
},
];
}
}

View 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 */
}
}

View 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;
}
}
}

View File

@@ -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) => {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,
};
};

View File

@@ -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;
}
})

View 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}`;
};

View File

@@ -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;
}

View File

@@ -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, ' '));
};

View 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');

View 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();
};

View 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, ' '));
};

View 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;
}
}

View 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));
};

View File

@@ -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([

View File

@@ -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;
});

View File

@@ -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', () => {

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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());
});
});

View File

@@ -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"
]
}
}

View File

@@ -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:

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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"
}
}

View File

@@ -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
};

View File

@@ -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);
};

View 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;

View File

@@ -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;
}

View File

@@ -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
];

View File

@@ -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, "/");

View 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";
}

View File

@@ -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;

View 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
});
};

View 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);
})
);
};

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
};
}

View File

@@ -6,7 +6,8 @@
"outDir": "out",
"lib": ["es6"],
"sourceMap": true,
"strict": false
"strict": false,
"downlevelIteration": true
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"],

View File

@@ -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 -->
[![All Contributors](https://img.shields.io/badge/all_contributors-43-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-56-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
@@ -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>

View File

@@ -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"