mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947ddf0b77 | ||
|
|
f206e855a9 | ||
|
|
1b8f0cd2fd | ||
|
|
ca063d4eee | ||
|
|
734986211a | ||
|
|
54a4aec1a0 | ||
|
|
d1a28717fe | ||
|
|
30759bd1f3 | ||
|
|
852b19f177 | ||
|
|
16cad729fd | ||
|
|
ab6c046404 | ||
|
|
4b16b530b4 | ||
|
|
51ec6ddec4 | ||
|
|
ca39351407 | ||
|
|
8e48dd77a2 | ||
|
|
ade5b01316 | ||
|
|
4e661aa6b5 | ||
|
|
fa4b9d57aa | ||
|
|
a6db7815f0 | ||
|
|
e604f26544 | ||
|
|
9b12c79daf | ||
|
|
d924a8612e | ||
|
|
7aa2e0e411 | ||
|
|
a710358701 | ||
|
|
9e4124068a | ||
|
|
84e774144e | ||
|
|
ef9131ead7 | ||
|
|
18f0725779 | ||
|
|
eb2a2ed9e0 | ||
|
|
433c0c5b7e | ||
|
|
f48c74c607 |
@@ -598,6 +598,51 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joeltjames",
|
||||
"name": "Joel James",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3732400?v=4",
|
||||
"profile": "https://github.com/joeltjames",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ryo33",
|
||||
"name": "Hashiguchi Ryo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8780513?v=4",
|
||||
"profile": "https://www.ryo33.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "movermeyer",
|
||||
"name": "Michael Overmeyer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1459385?v=4",
|
||||
"profile": "https://movermeyer.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "derrickqin",
|
||||
"name": "Derrick Qin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3038111?v=4",
|
||||
"profile": "https://github.com/derrickqin",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zomars",
|
||||
"name": "Omar López",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3504472?v=4",
|
||||
"profile": "https://www.linkedin.com/in/zomars/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -20,13 +20,14 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Restore Dependencies
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
packages/foam-vscode/.vscode-test
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/foam-vscode/src/test/run-tests.ts') }}
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Check Lint Rules
|
||||
@@ -47,13 +48,14 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Restore Dependencies
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
packages/foam-vscode/.vscode-test
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/foam-vscode/src/test/run-tests.ts') }}
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Build Packages
|
||||
|
||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -4,12 +4,21 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"inputs": [
|
||||
{
|
||||
"id": "packageName",
|
||||
"type": "pickString",
|
||||
"description": "Select the package in which this test is located",
|
||||
"options": ["foam-core", "foam-vscode"],
|
||||
"default": "foam-core"
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["workspace", "foam-core", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"runtimeArgs": ["workspace", "${input:packageName}", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"args": [
|
||||
"--runInBand"
|
||||
],
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -22,5 +22,10 @@
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"]
|
||||
"jest.debugCodeLens.showWhenTestStateIn": [
|
||||
"fail",
|
||||
"unknown",
|
||||
"pass"
|
||||
],
|
||||
"gitdoc.enabled": false
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ We use the following convention in Foam:
|
||||
- *.test.ts are unit tests
|
||||
- *.spec.ts are integration tests
|
||||
|
||||
Also, note that tests in `foam-core` and `foam-cli` live in the `test` directory.
|
||||
Also, note that tests in `foam-core` live in the `test` directory.
|
||||
Tests in `foam-vscode` live alongside the code in `src`.
|
||||
|
||||
### The VS Code Extension
|
||||
|
||||
@@ -9,11 +9,11 @@ Foam code and documentation live in the monorepo at [foambubble/foam](https://gi
|
||||
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VSCode plugin.
|
||||
- [/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.
|
||||
- [foam-cli](https://github.com/foambubble/foam-cli) - The Foam CLI tool.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: ../recipes/recipes.md "Recipes"
|
||||
|
||||
@@ -11,7 +11,7 @@ One of Foam's big features is the ability to find all instances of a reference,
|
||||
Implementing this is on the [[roadmap]], but for the time being you can achieve similar things by:
|
||||
|
||||
- `Cmd` + `Shift` + `F` ( `Ctrl` + `Shift` + `F` on Windows ) to find all the references, e.g. "Cat food"
|
||||
- `Cmd` + `Shift` + `H` ( `Ctrl` + `Shift` + `F` on Windows ) to replace them with [[cat-food]].
|
||||
- `Cmd` + `Shift` + `H` ( `Ctrl` + `Shift` + `H` on Windows ) to replace them with [[cat-food]].
|
||||
- Click any of the references to create a new note.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -14,8 +14,8 @@ By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the
|
||||
|
||||
These settings can be overridden in your workspace or global `.vscode/settings.json` file, using the [**dateformat** date masking syntax](https://github.com/felixge/node-dateformat#mask-options):
|
||||
|
||||
```json
|
||||
"foam.openDailyNote.directory": "journal",
|
||||
```jsonc
|
||||
"foam.openDailyNote.directory": "journal", // a relative directory path will get appended to the workspace root. An absolute directory path will be used unmodified.
|
||||
"foam.openDailyNote.filenameFormat": "'daily-note'-yyyy-mm-dd",
|
||||
"foam.openDailyNote.fileExtension": "mdx",
|
||||
"foam.openDailyNote.titleFormat": "'Journal Entry, ' dddd, mmmm d",
|
||||
@@ -31,24 +31,19 @@ In the meantime, you can use [VS Code Snippets](https://code.visualstudio.com/do
|
||||
|
||||
## Roam-style Automatic Daily Notes
|
||||
|
||||
In the future, Foam may provide an option for automatically opening your Daily Note when you open your Foam workspace.
|
||||
|
||||
If you want this behavior now, you can use the excellent [Auto Run Command](https://marketplace.visualstudio.com/items?itemName=gabrielgrinberg.auto-run-command#review-details) extension to run the "Open Daily Note" command upon entering a Foam workspace by specifying the following configuration in your `.vscode/settings.json`:
|
||||
Foam provides an option for automatically opening your Daily Note when you open your Foam workspace. You can enable it by specifying the following configuration in your `.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
"auto-run-command.rules": [
|
||||
{
|
||||
"condition": "hasFile: .vscode/foam.json",
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"message": "Have a nice day!"
|
||||
}
|
||||
],
|
||||
{
|
||||
// ...Other configurations
|
||||
"foam.openDailyNote.onStartup": true
|
||||
}
|
||||
```
|
||||
|
||||
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
|
||||
|
||||
Please see [[note-macros]]
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[note-macros]: ../recipes/note-macros.md "Custom Note Macros"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[note-macros]: ../recipes/note-macros.md 'Custom Note Macros'
|
||||
[//end]: # 'Autogenerated link references'
|
||||
|
||||
@@ -189,6 +189,11 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -22,3 +22,4 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
|
||||
- [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
- [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)
|
||||
- [Markdown Footnotes](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-footnotes) (Adds [^footnote] syntax support to VS Code's built-in markdown preview)
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.10.3"
|
||||
"version": "0.12.0"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/lib
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"oclif",
|
||||
"oclif-typescript"
|
||||
]
|
||||
}
|
||||
8
packages/foam-cli/.gitignore
vendored
8
packages/foam-cli/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
*-debug.log
|
||||
*-error.log
|
||||
/.nyc_output
|
||||
/dist
|
||||
/lib
|
||||
/package-lock.json
|
||||
/tmp
|
||||
node_modules
|
||||
@@ -1,95 +0,0 @@
|
||||
foam-cli
|
||||
========
|
||||
|
||||
Foam CLI
|
||||
|
||||
[](https://oclif.io)
|
||||
[](https://npmjs.org/package/foam-cli)
|
||||
[](https://npmjs.org/package/foam-cli)
|
||||
[](https://github.com/foambubble/foam/blob/master/package.json)
|
||||
|
||||
<!-- toc -->
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
<!-- tocstop -->
|
||||
# Usage
|
||||
<!-- usage -->
|
||||
```sh-session
|
||||
$ npm install -g foam-cli
|
||||
$ foam COMMAND
|
||||
running command...
|
||||
$ foam (-v|--version|version)
|
||||
foam-cli/0.10.3 darwin-x64 node-v12.18.2
|
||||
$ foam --help [COMMAND]
|
||||
USAGE
|
||||
$ foam COMMAND
|
||||
...
|
||||
```
|
||||
<!-- usagestop -->
|
||||
# Commands
|
||||
<!-- commands -->
|
||||
* [`foam help [COMMAND]`](#foam-help-command)
|
||||
* [`foam janitor [WORKSPACEPATH]`](#foam-janitor-workspacepath)
|
||||
* [`foam migrate [WORKSPACEPATH]`](#foam-migrate-workspacepath)
|
||||
|
||||
## `foam help [COMMAND]`
|
||||
|
||||
display help for foam
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam help [COMMAND]
|
||||
|
||||
ARGUMENTS
|
||||
COMMAND command to show help for
|
||||
|
||||
OPTIONS
|
||||
--all see all commands in CLI
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.1.0/src/commands/help.ts)_
|
||||
|
||||
## `foam janitor [WORKSPACEPATH]`
|
||||
|
||||
Updates link references and heading across all the markdown files in the given workspaces
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam janitor [WORKSPACEPATH]
|
||||
|
||||
OPTIONS
|
||||
-h, --help show CLI help
|
||||
-w, --without-extensions generate link reference definitions without extensions (for legacy support)
|
||||
|
||||
EXAMPLE
|
||||
$ foam-cli janitor path-to-foam-workspace
|
||||
```
|
||||
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.10.3/src/commands/janitor.ts)_
|
||||
|
||||
## `foam migrate [WORKSPACEPATH]`
|
||||
|
||||
Updates file names, link references and heading across all the markdown files in the given workspaces
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam migrate [WORKSPACEPATH]
|
||||
|
||||
OPTIONS
|
||||
-h, --help show CLI help
|
||||
-w, --without-extensions generate link reference definitions without extensions (for legacy support)
|
||||
|
||||
EXAMPLE
|
||||
$ foam-cli migrate path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
```
|
||||
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.10.3/src/commands/migrate.ts)_
|
||||
<!-- commandsstop -->
|
||||
|
||||
## Development
|
||||
|
||||
- Run `yarn` somewhere in workspace (ideally root, see [yarn workspace docs](https://classic.yarnpkg.com/en/docs/workspaces/)
|
||||
- This will automatically symlink all package directories so you're using the local copy
|
||||
- In `packages/foam-core`, run `yarn start` to rebuild the library on every change
|
||||
- In `packages/foam-cli`, make changes and run with `yarn run cli`. This should use latest workspace manager changes.
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/command').run()
|
||||
.then(require('@oclif/command/flush'))
|
||||
.catch(require('@oclif/errors/handle'))
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
@@ -1,188 +0,0 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/p6/5y5l8tbs1d32pq9b596lk48h0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"name": "foam-cli",
|
||||
"description": "Foam CLI",
|
||||
"version": "0.10.3",
|
||||
"bin": {
|
||||
"foam": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/foambubble/foam/issues",
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"foam-core": "^0.10.3",
|
||||
"ora": "^4.0.4",
|
||||
"tslib": "^1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@oclif/dev-cli": "^1",
|
||||
"@types/node": "^10",
|
||||
"babel-jest": "^26.1.0",
|
||||
"chai": "^4",
|
||||
"eslint": "^5.13",
|
||||
"eslint-config-oclif": "^3.1",
|
||||
"eslint-config-oclif-typescript": "^0.1",
|
||||
"globby": "^10",
|
||||
"jest": "^26.1.0",
|
||||
"mock-fs": "^4.12.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"foam-core": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/lib",
|
||||
"/npm-shrinkwrap.json",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"keywords": [
|
||||
"oclif"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"oclif": {
|
||||
"commands": "./lib/commands",
|
||||
"bin": "foam",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
]
|
||||
},
|
||||
"repository": "foambubble/foam",
|
||||
"scripts": {
|
||||
"clean": "rimraf tmp",
|
||||
"build": "tsc -b",
|
||||
"test": "jest",
|
||||
"lint": "echo Missing lint task in CLI package",
|
||||
"cli": "yarn build && ./bin/run",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
|
||||
"version": "oclif-dev readme && git add README.md"
|
||||
},
|
||||
"types": "lib/index.d.ts"
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
URI,
|
||||
isNote,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
export default class Janitor extends Command {
|
||||
static description =
|
||||
'Updates link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli janitor path-to-foam-workspace
|
||||
`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
'without-extensions': flags.boolean({
|
||||
char: 'w',
|
||||
description:
|
||||
'generate link reference definitions without extensions (for legacy support)',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
static args = [{ name: 'workspacePath' }];
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Janitor);
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const config = createConfigFromFolders([URI.file(workspacePath)]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
const notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
spinner.text = 'Generating link definitions';
|
||||
|
||||
const fileWritePromises = notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(
|
||||
note,
|
||||
workspace,
|
||||
!flags['without-extensions']
|
||||
);
|
||||
|
||||
// apply Edits
|
||||
let file = note.source.text;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.uri, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
} else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
getKebabCaseFileName,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
isNote,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { renameFile } from '../utils/rename-file';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
// @todo: Refactor 'migrate' and 'janitor' commands and avoid repeatition
|
||||
export default class Migrate extends Command {
|
||||
static description =
|
||||
'Updates file names, link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli migrate path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
'without-extensions': flags.boolean({
|
||||
char: 'w',
|
||||
description:
|
||||
'generate link reference definitions without extensions (for legacy support)',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
static args = [{ name: 'workspacePath' }];
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Migrate);
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
const config = createConfigFromFolders([workspacePath]);
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
let workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
let notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
// Kebab case file names
|
||||
const fileRename = notes.map(note => {
|
||||
if (note.title != null) {
|
||||
const kebabCasedFileName = getKebabCaseFileName(note.title);
|
||||
if (kebabCasedFileName) {
|
||||
return renameFile(note.uri, kebabCasedFileName);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileRename);
|
||||
|
||||
spinner.text = 'Renaming files';
|
||||
|
||||
// Reinitialize the graph after renaming files
|
||||
workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = 'Generating link definitions';
|
||||
|
||||
const fileWritePromises = await Promise.all(
|
||||
notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(
|
||||
note,
|
||||
workspace,
|
||||
!flags['without-extensions']
|
||||
);
|
||||
|
||||
// apply Edits
|
||||
let file = note.source.text;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.uri, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
} else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export {run} from '@oclif/command'
|
||||
@@ -1,4 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const isValidDirectory = (path: string) =>
|
||||
fs.existsSync(path) && fs.lstatSync(path).isDirectory();
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileUri absolute path for the file that needs to renamed
|
||||
* @param newFileName "new file name" without the extension
|
||||
*/
|
||||
export const renameFile = async (fileUri: URI, newFileName: string) => {
|
||||
const filePath = fileUri.fsPath;
|
||||
const dirName = path.dirname(filePath);
|
||||
const extension = path.extname(filePath);
|
||||
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
|
||||
|
||||
return fs.promises.rename(filePath, newFileUri);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
export const writeFileToDisk = async (fileUri: URI, data: string) => {
|
||||
return fs.promises.writeFile(fileUri.fsPath, data);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { renameFile } from '../src/utils/rename-file';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
const doesFileExist = (path: string) =>
|
||||
fs.promises
|
||||
.access(path)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
describe('renameFile', () => {
|
||||
const fileUri = URI.file('/test/oldFileName.md');
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri.fsPath]: '' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should rename existing file', async () => {
|
||||
expect(await doesFileExist(fileUri.fsPath)).toBe(true);
|
||||
|
||||
renameFile(fileUri, 'new-file-name');
|
||||
|
||||
expect(await doesFileExist(fileUri.fsPath)).toBe(false);
|
||||
expect(await doesFileExist('/test/new-file-name.md')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { writeFileToDisk } from '../src/utils/write-file-to-disk';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
describe('writeFileToDisk', () => {
|
||||
const fileUri = URI.file('./test-file.md');
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri.fsPath]: 'content in the existing file' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.unlinkSync(fileUri.fsPath);
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should overrwrite existing file in the disk with the new data', async () => {
|
||||
const expected = `content in the new file`;
|
||||
await writeFileToDisk(fileUri, expected);
|
||||
const actual = await fs.promises.readFile(fileUri.fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [{ "path": "../foam-core" }]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.10.3",
|
||||
"version": "0.12.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -15,6 +15,10 @@
|
||||
"prepare": "tsdx build --tsconfig ./tsconfig.build.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/plugin-transform-runtime": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam, Services } from './index';
|
||||
import { FoamConfig, Foam, IDataStore } from './index';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { isSome } from './utils';
|
||||
import { isDisposable } from './common/lifecycle';
|
||||
import { Logger } from './utils/log';
|
||||
import { isMarkdownFile } from './utils/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
|
||||
export const bootstrap = async (config: FoamConfig, services: Services) => {
|
||||
export const bootstrap = async (config: FoamConfig, dataStore: IDataStore) => {
|
||||
const plugins = await loadPlugins(config);
|
||||
|
||||
const parserPlugins = plugins.map(p => p.parser).filter(isSome);
|
||||
const parser = createMarkdownParser(parserPlugins);
|
||||
|
||||
const workspace = new FoamWorkspace();
|
||||
const files = await services.dataStore.listFiles();
|
||||
const files = await dataStore.listFiles();
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + uri);
|
||||
if (uri.path.endsWith('md')) {
|
||||
const content = await services.dataStore.read(uri);
|
||||
if (isMarkdownFile(uri)) {
|
||||
const content = await dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(parser.parse(uri, content));
|
||||
}
|
||||
@@ -27,25 +27,30 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
|
||||
);
|
||||
workspace.resolveLinks(true);
|
||||
|
||||
services.dataStore.onDidChange(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidCreate(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
});
|
||||
const listeners = [
|
||||
dataStore.onDidChange(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidCreate(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
|
||||
return {
|
||||
workspace: workspace,
|
||||
config: config,
|
||||
parse: parser.parse,
|
||||
services: services,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
},
|
||||
dispose: () => {
|
||||
isDisposable(services.dataStore) && services.dataStore.dispose();
|
||||
listeners.forEach(l => l.dispose());
|
||||
workspace.dispose();
|
||||
},
|
||||
} as Foam;
|
||||
};
|
||||
|
||||
@@ -452,7 +452,7 @@ interface UriState extends UriComponents {
|
||||
|
||||
const _pathSepMarker = isWindows ? 1 : undefined;
|
||||
|
||||
// This class exists so that URI is compatibile with vscode.Uri (API).
|
||||
// This class exists so that URI is compatible with vscode.Uri (API).
|
||||
class Uri extends URI {
|
||||
_formatted: string | null = null;
|
||||
_fsPath: string | null = null;
|
||||
|
||||
@@ -6,25 +6,37 @@ import {
|
||||
NoteLink,
|
||||
isNote,
|
||||
NoteLinkDefinition,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
NoteParser,
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import { URI } from './common/uri';
|
||||
import { FoamConfig } from './config';
|
||||
import { IDataStore, FileDataStore } from './services/datastore';
|
||||
import { ILogger } from './utils/log';
|
||||
import { IDisposable, isDisposable } from './common/lifecycle';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import * as uris from './utils/uri';
|
||||
import * as positions from './model/position';
|
||||
import * as ranges from './model/range';
|
||||
|
||||
export { uris, positions, ranges };
|
||||
export { IDataStore, FileDataStore };
|
||||
export { ILogger };
|
||||
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
|
||||
export { Event, Emitter } from './common/event';
|
||||
export { FoamConfig };
|
||||
export { isSameUri, parseUri } from './utils/uri';
|
||||
|
||||
export { IDisposable, isDisposable };
|
||||
|
||||
export {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
createMarkdownParser,
|
||||
} from './markdown-provider';
|
||||
|
||||
export {
|
||||
@@ -47,20 +59,26 @@ export {
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
Position,
|
||||
Range,
|
||||
NoteLink,
|
||||
URI,
|
||||
FoamWorkspace,
|
||||
NoteLinkDefinition,
|
||||
NoteParser,
|
||||
isNote,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
};
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: NoteParser;
|
||||
}
|
||||
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
config: FoamConfig;
|
||||
parse: (uri: URI, text: string, eol: string) => Note;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '../index';
|
||||
|
||||
/**
|
||||
@@ -7,12 +10,29 @@ import { TextEdit } from '../index';
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const eol = detectNewline(text) || os.EOL;
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = textEdit.range.start.offset || 0;
|
||||
const endOffset = textEdit.range.end.offset || 0;
|
||||
let startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
let endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
};
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i].length);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Position } from 'unist';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Note } from '../model/note';
|
||||
import { Range, createFromPosition } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
@@ -14,7 +14,7 @@ export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
export interface TextEdit {
|
||||
range: Position;
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
@@ -48,15 +48,12 @@ export const generateLinkReferences = (
|
||||
}
|
||||
|
||||
const padding =
|
||||
note.source.end.column === 1
|
||||
note.source.end.character === 0
|
||||
? note.source.eol
|
||||
: `${note.source.eol}${note.source.eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: {
|
||||
start: note.source.end,
|
||||
end: note.source.end,
|
||||
},
|
||||
range: createFromPosition(note.source.end, note.source.end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
@@ -72,10 +69,7 @@ export const generateLinkReferences = (
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${newReferences}`,
|
||||
range: {
|
||||
start: first.position!.start,
|
||||
end: last.position!.end,
|
||||
},
|
||||
range: createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -114,10 +108,10 @@ export const generateHeading = (note: Note): TextEdit | null => {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
uriToSlug(note.uri)
|
||||
)}${paddingEnd}`,
|
||||
range: {
|
||||
start: note.source.contentStart,
|
||||
end: note.source.contentStart,
|
||||
},
|
||||
range: createFromPosition(
|
||||
note.source.contentStart,
|
||||
note.source.contentStart
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'unist';
|
||||
import { Node, Position as AstPosition } from 'unist';
|
||||
import unified from 'unified';
|
||||
import markdownParse from 'remark-parse';
|
||||
import wikiLinkPlugin from 'remark-wiki-link';
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
isWikilink,
|
||||
getTitle,
|
||||
} from './model/note';
|
||||
import { Position, create as createPos } from './model/position';
|
||||
import { Range, create as createRange } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
extractHashtags,
|
||||
@@ -82,7 +84,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'wikilink',
|
||||
slug: node.value as string,
|
||||
target: node.value as string,
|
||||
position: node.position!,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
@@ -96,6 +98,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'link',
|
||||
target: targetUri,
|
||||
label: label,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -109,7 +112,7 @@ const definitionsPlugin: ParserPlugin = {
|
||||
label: node.label as string,
|
||||
url: node.url as string,
|
||||
title: node.title as string,
|
||||
position: node.position,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -177,8 +180,8 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
definitions: [],
|
||||
source: {
|
||||
text: markdown,
|
||||
contentStart: tree.position!.start,
|
||||
end: tree.position!.end,
|
||||
contentStart: astPointToFoamPosition(tree.position!.start),
|
||||
end: astPointToFoamPosition(tree.position!.end),
|
||||
eol: eol,
|
||||
},
|
||||
};
|
||||
@@ -201,11 +204,10 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
// 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,
|
||||
};
|
||||
note.source.contentStart = createPos(
|
||||
node.position!.end.line! + 2,
|
||||
0
|
||||
);
|
||||
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
try {
|
||||
@@ -243,7 +245,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
|
||||
function getFoamDefinitions(
|
||||
defs: NoteLinkDefinition[],
|
||||
fileEndPoint: Point
|
||||
fileEndPoint: Position
|
||||
): NoteLinkDefinition[] {
|
||||
let previousLine = fileEndPoint.line;
|
||||
let foamDefinitions = [];
|
||||
@@ -254,13 +256,13 @@ function getFoamDefinitions(
|
||||
// if this definition is more than 2 lines above the
|
||||
// previous one below it (or file end), that means we
|
||||
// have exited the trailing definition block, and should bail
|
||||
const start = def.position!.start.line;
|
||||
const start = def.range!.start.line;
|
||||
if (start < previousLine - 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
foamDefinitions.unshift(def);
|
||||
previousLine = def.position!.end.line;
|
||||
previousLine = def.range!.end.line;
|
||||
}
|
||||
|
||||
return foamDefinitions;
|
||||
@@ -315,3 +317,25 @@ export function createMarkdownReferences(
|
||||
.filter(isSome)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the 1-index Point object into the VS Code 0-index Position object
|
||||
* @param point ast Point (1-indexed)
|
||||
* @returns Foam Position (0-indexed)
|
||||
*/
|
||||
const astPointToFoamPosition = (point: Point): Position => {
|
||||
return createPos(point.line - 1, point.column - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the 1-index Position object into the VS Code 0-index Range object
|
||||
* @param position an ast Position object (1-indexed)
|
||||
* @returns Foam Range (0-indexed)
|
||||
*/
|
||||
const astPositionToFoamRange = (pos: AstPosition): Range =>
|
||||
createRange(
|
||||
pos.start.line - 1,
|
||||
pos.start.column - 1,
|
||||
pos.end.line - 1,
|
||||
pos.end.column - 1
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Position, Point } from 'unist';
|
||||
import { URI } from '../common/uri';
|
||||
import { getBasename } from '../utils';
|
||||
export { Position, Point };
|
||||
import { getBasename } from '../utils/uri';
|
||||
import { Position } from './position';
|
||||
import { Range } from './range';
|
||||
|
||||
export interface NoteSource {
|
||||
text: string;
|
||||
contentStart: Point;
|
||||
end: Point;
|
||||
contentStart: Position;
|
||||
end: Position;
|
||||
eol: string;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ export interface WikiLink {
|
||||
type: 'wikilink';
|
||||
slug: string;
|
||||
target: string;
|
||||
position: Position;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface DirectLink {
|
||||
type: 'link';
|
||||
label: string;
|
||||
target: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export type NoteLink = WikiLink | DirectLink;
|
||||
@@ -29,7 +30,7 @@ export interface NoteLinkDefinition {
|
||||
label: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
position?: Position;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface BaseResource {
|
||||
@@ -74,3 +75,11 @@ export const getTitle = (resource: Resource): string => {
|
||||
export const isNote = (resource: Resource): resource is Note => {
|
||||
return resource.type === 'note';
|
||||
};
|
||||
|
||||
export const isPlaceholder = (resource: Resource): resource is Placeholder => {
|
||||
return resource.type === 'placeholder';
|
||||
};
|
||||
|
||||
export const isAttachment = (resource: Resource): resource is Attachment => {
|
||||
return resource.type === 'attachment';
|
||||
};
|
||||
|
||||
87
packages/foam-core/src/model/position.ts
Normal file
87
packages/foam-core/src/model/position.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
export interface Position {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
export const create = (line: number, character: number): Position => ({
|
||||
line,
|
||||
character,
|
||||
});
|
||||
|
||||
export const Min = (...positions: Position[]): Position => {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (isBefore(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const Max = (...positions: Position[]): Position => {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (isAfter(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const isBefore = (p1: Position, p2: Position): boolean => {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character < p2.character;
|
||||
};
|
||||
|
||||
export const isBeforeOrEqual = (p1: Position, p2: Position): boolean => {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character <= p2.character;
|
||||
};
|
||||
|
||||
export const isAfter = (p1: Position, p2: Position): boolean => {
|
||||
return !isBeforeOrEqual(p1, p2);
|
||||
};
|
||||
|
||||
export const isAfterOrEqual = (p1: Position, p2: Position): boolean => {
|
||||
return !isBefore(p1, p2);
|
||||
};
|
||||
|
||||
export const isEqual = (p1: Position, p2: Position): boolean => {
|
||||
return p1.line === p2.line && p1.character === p2.character;
|
||||
};
|
||||
|
||||
export const compareTo = (p1: Position, p2: Position): number => {
|
||||
if (p1.line < p2.line) {
|
||||
return -1;
|
||||
} else if (p1.line > p2.line) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line
|
||||
if (p1.character < p2.character) {
|
||||
return -1;
|
||||
} else if (p1.character > p2.character) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line and character
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
55
packages/foam-core/src/model/range.ts
Normal file
55
packages/foam-core/src/model/range.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Position } from './position';
|
||||
import * as pos from './position';
|
||||
|
||||
export interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
export const create = (
|
||||
startLine: number,
|
||||
startChar: number,
|
||||
endLine?: number,
|
||||
endChar?: number
|
||||
): Range => {
|
||||
const start: Position = {
|
||||
line: startLine,
|
||||
character: startChar,
|
||||
};
|
||||
const end: Position = {
|
||||
line: endLine ?? startLine,
|
||||
character: endChar ?? startChar,
|
||||
};
|
||||
return createFromPosition(start, end);
|
||||
};
|
||||
|
||||
export const createFromPosition = (start: Position, end?: Position) => {
|
||||
end = end ?? start;
|
||||
let first = start;
|
||||
let second = end;
|
||||
if (pos.isAfter(start, end)) {
|
||||
first = end;
|
||||
second = start;
|
||||
}
|
||||
return {
|
||||
start: {
|
||||
line: first.line,
|
||||
character: first.character,
|
||||
},
|
||||
end: {
|
||||
line: second.line,
|
||||
character: second.character,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const containsRange = (range: Range, contained: Range): boolean =>
|
||||
containsPosition(range, contained.start) &&
|
||||
containsPosition(range, contained.end);
|
||||
|
||||
export const containsPosition = (range: Range, position: Position): boolean =>
|
||||
pos.isAfterOrEqual(position, range.start) &&
|
||||
pos.isBeforeOrEqual(position, range.end);
|
||||
|
||||
export const isEqual = (r1: Range, r2: Range): boolean =>
|
||||
pos.isEqual(r1.start, r2.start) && pos.isEqual(r1.end, r2.end);
|
||||
@@ -2,7 +2,8 @@ import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { URI } from '../common/uri';
|
||||
import { Resource, NoteLink, Note } from '../model/note';
|
||||
import { Resource, NoteLink, Note } from './note';
|
||||
import * as ranges from './range';
|
||||
import {
|
||||
computeRelativeURI,
|
||||
isSome,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
parseUri,
|
||||
placeholderUri,
|
||||
isPlaceholder,
|
||||
isSameUri,
|
||||
} from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { IDisposable } from '../index';
|
||||
@@ -17,6 +19,7 @@ import { IDisposable } from '../index';
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
link: NoteLink;
|
||||
};
|
||||
|
||||
export function getReferenceType(
|
||||
@@ -88,7 +91,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
get(uri: URI) {
|
||||
return FoamWorkspace.get(this, uri);
|
||||
}
|
||||
find(uri: URI) {
|
||||
find(uri: URI | string) {
|
||||
return FoamWorkspace.find(this, uri);
|
||||
}
|
||||
set(resource: Resource) {
|
||||
@@ -221,12 +224,12 @@ export class FoamWorkspace implements IDisposable {
|
||||
];
|
||||
}
|
||||
|
||||
public static getLinks(workspace: FoamWorkspace, uri: URI): URI[] {
|
||||
return workspace.links[uri.path]?.map(c => c.target) ?? [];
|
||||
public static getLinks(workspace: FoamWorkspace, uri: URI): Connection[] {
|
||||
return workspace.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public static getBacklinks(workspace: FoamWorkspace, uri: URI): URI[] {
|
||||
return workspace.backlinks[uri.path]?.map(c => c.source) ?? [];
|
||||
public static getBacklinks(workspace: FoamWorkspace, uri: URI): Connection[] {
|
||||
return workspace.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public static set(
|
||||
@@ -310,10 +313,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
throw new Error(
|
||||
'Cannot find note defined by relative path without reference note: ' +
|
||||
resourceId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = computeRelativeURI(reference, relativePath);
|
||||
@@ -333,9 +333,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
delete workspace.resources[id];
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
workspace.resourcesByName[name] = workspace.resourcesByName[name].filter(
|
||||
resId => resId !== id
|
||||
);
|
||||
workspace.resourcesByName[name] =
|
||||
workspace.resourcesByName[name]?.filter(resId => resId !== id) ?? [];
|
||||
if (workspace.resourcesByName[name].length === 0) {
|
||||
delete workspace.resourcesByName[name];
|
||||
}
|
||||
@@ -350,7 +349,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = FoamWorkspace.resolveLink(workspace, resource, link);
|
||||
workspace = FoamWorkspace.connect(workspace, resource.uri, targetUri);
|
||||
workspace = FoamWorkspace.connect(workspace, resource.uri, targetUri, link);
|
||||
});
|
||||
}
|
||||
return workspace;
|
||||
@@ -374,11 +373,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
workspace = patch.removed.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(oldResource, link);
|
||||
return FoamWorkspace.disconnect(ws, oldResource.uri, target);
|
||||
return FoamWorkspace.disconnect(ws, oldResource.uri, target, link);
|
||||
}, workspace);
|
||||
workspace = patch.added.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(newResource, link);
|
||||
return FoamWorkspace.connect(ws, newResource.uri, target);
|
||||
return FoamWorkspace.connect(ws, newResource.uri, target, link);
|
||||
}, workspace);
|
||||
}
|
||||
return workspace;
|
||||
@@ -414,7 +413,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
const resourcesPointedByDeletedNote = workspace.links[uri.path] ?? [];
|
||||
delete workspace.links[uri.path];
|
||||
workspace = resourcesPointedByDeletedNote.reduce(
|
||||
(ws, link) => FoamWorkspace.disconnect(ws, uri, link.target),
|
||||
(ws, connection) =>
|
||||
FoamWorkspace.disconnect(ws, uri, connection.target, connection.link),
|
||||
workspace
|
||||
);
|
||||
|
||||
@@ -428,11 +428,13 @@ export class FoamWorkspace implements IDisposable {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static connect(workspace: FoamWorkspace, source: URI, target: URI) {
|
||||
const connection = {
|
||||
source: source,
|
||||
target: target,
|
||||
};
|
||||
private static connect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI,
|
||||
link: NoteLink
|
||||
) {
|
||||
const connection = { source, target, link };
|
||||
|
||||
workspace.links[source.path] = workspace.links[source.path] ?? [];
|
||||
workspace.links[source.path].push(connection);
|
||||
@@ -442,20 +444,35 @@ export class FoamWorkspace implements IDisposable {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private static disconnect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI
|
||||
target: URI,
|
||||
link: NoteLink | true
|
||||
) {
|
||||
workspace.links[source.path] = workspace.links[source.path]?.filter(
|
||||
c => c.source.path !== source.path || c.target.path !== target.path
|
||||
);
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!isSameUri(source, c.source) || !isSameUri(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
workspace.links[source.path] =
|
||||
workspace.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.links[source.path].length === 0) {
|
||||
delete workspace.links[source.path];
|
||||
}
|
||||
workspace.backlinks[target.path] = workspace.backlinks[target.path]?.filter(
|
||||
c => c.source.path !== source.path || c.target.path !== target.path
|
||||
);
|
||||
workspace.backlinks[target.path] =
|
||||
workspace.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.backlinks[target.path].length === 0) {
|
||||
delete workspace.backlinks[target.path];
|
||||
if (isPlaceholder(target)) {
|
||||
@@ -465,3 +482,13 @@ export class FoamWorkspace implements IDisposable {
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
isSameUri(a.source, b.source) &&
|
||||
isSameUri(a.target, b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: NoteLink, b: NoteLink) =>
|
||||
a.type === b.type && ranges.isEqual(a.range, b.range);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { posix } from 'path';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { hash } from './core';
|
||||
import { URI } from '../common/uri';
|
||||
import { statSync } from 'fs';
|
||||
|
||||
export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(posix.parse(noteUri.path).name);
|
||||
@@ -22,6 +23,8 @@ export const computeRelativePath = (source: URI, target: URI): string => {
|
||||
|
||||
export const getBasename = (uri: URI) => posix.parse(uri.path).name;
|
||||
|
||||
export const getDir = (uri: URI) => URI.file(posix.dirname(uri.path));
|
||||
|
||||
export const computeRelativeURI = (
|
||||
reference: URI,
|
||||
relativeSlug: string
|
||||
@@ -65,6 +68,35 @@ export const placeholderUri = (key: string): URI => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
export const placeholderToResourceUri = (
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI => {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
};
|
||||
|
||||
export const isPlaceholder = (uri: URI): boolean => {
|
||||
return uri.scheme === 'placeholder';
|
||||
};
|
||||
|
||||
export const isSameUri = (a: URI, b: URI) =>
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path && // Note we don't use fsPath for sameness
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query;
|
||||
|
||||
export const isMarkdownFile = (uri: URI): boolean => {
|
||||
return uri.path.endsWith('md') && statSync(uri.fsPath).isFile();
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import path from 'path';
|
||||
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
|
||||
import * as ranges from '../src/model/range';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { parseUri } from '../src/utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const position = {
|
||||
start: { line: 1, column: 1 },
|
||||
end: { line: 1, column: 1 },
|
||||
};
|
||||
const position = ranges.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
@@ -33,30 +33,39 @@ export const createTestNote = (params: {
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
text?: string;
|
||||
root?: URI;
|
||||
}): Note => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
uri: parseUri(root, params.uri),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? null,
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map(link =>
|
||||
'slug' in link
|
||||
? params.links.map((link, index) => {
|
||||
const range = ranges.create(
|
||||
position.start.line + index,
|
||||
position.start.character,
|
||||
position.start.line + index,
|
||||
position.end.character
|
||||
);
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
target: link.slug,
|
||||
position: position,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
target: link.to,
|
||||
label: 'link text',
|
||||
}
|
||||
)
|
||||
range: range,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
source: {
|
||||
eol: eol,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
|
||||
import * as ranges from '../../src/model/range';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
@@ -6,25 +7,21 @@ Logger.setLevel('error');
|
||||
describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit in the end of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 4. this is fourth line`,
|
||||
range: {
|
||||
start: { line: 3, column: 1, offset: 79 },
|
||||
end: { line: 3, column: 1, offset: 79 },
|
||||
},
|
||||
newText: `4. this is fourth line`,
|
||||
range: ranges.create(4, 0, 4, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
4. this is fourth line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
4. this is fourth line`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
@@ -33,23 +30,20 @@ describe('applyTextEdit', () => {
|
||||
|
||||
it('should return text with applied TextEdit at the top of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 1. this is first line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 0 },
|
||||
end: { line: 0, column: 0, offset: 0 },
|
||||
},
|
||||
newText: `1. this is first line\n`,
|
||||
range: ranges.create(1, 0, 1, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
@@ -58,24 +52,21 @@ describe('applyTextEdit', () => {
|
||||
|
||||
it('should return text with applied TextEdit in the middle of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 2. this is the updated second line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 26 },
|
||||
end: { line: 0, column: 0, offset: 53 },
|
||||
},
|
||||
newText: `2. this is the updated second line`,
|
||||
range: ranges.create(2, 0, 2, 100),
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is the updated second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is the updated second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ import * as path from 'path';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services, Note } from '../../src';
|
||||
import { Note } from '../../src';
|
||||
import { URI } from '../../src/common/uri';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { getBasename } from '../../src/utils';
|
||||
import { getBasename } from '../../src/utils/uri';
|
||||
import * as ranges from '../../src/model/range';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -21,10 +22,7 @@ describe('generateHeadings', () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const foam = await bootstrap(config, services);
|
||||
const foam = await bootstrap(config, new FileDataStore(config));
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
@@ -34,18 +32,7 @@ describe('generateHeadings', () => {
|
||||
newText: `# File without Title
|
||||
|
||||
`,
|
||||
range: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
range: ranges.create(0, 0, 0, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
@@ -65,10 +52,7 @@ describe('generateHeadings', () => {
|
||||
|
||||
const expected = {
|
||||
newText: '\n# File with only Frontmatter\n\n',
|
||||
range: {
|
||||
start: { line: 4, column: 1, offset: 60 },
|
||||
end: { line: 4, column: 1, offset: 60 },
|
||||
},
|
||||
range: ranges.create(3, 0, 3, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
|
||||
@@ -2,12 +2,12 @@ import * as path from 'path';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services, Note } from '../../src';
|
||||
import { Note, ranges } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { URI } from '../../src/common/uri';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { getBasename } from '../../src/utils';
|
||||
import { getBasename } from '../../src/utils/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -21,10 +21,9 @@ describe('generateLinkReferences', () => {
|
||||
const config = createConfigFromFolders([
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
_workspace = await bootstrap(config, services).then(foam => foam.workspace);
|
||||
_workspace = await bootstrap(config, new FileDataStore(config)).then(
|
||||
foam => foam.workspace
|
||||
);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
@@ -43,18 +42,7 @@ describe('generateLinkReferences', () => {
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: {
|
||||
start: pointForNote(note, {
|
||||
line: 10,
|
||||
column: 1,
|
||||
offset: 140,
|
||||
}),
|
||||
end: pointForNote(note, {
|
||||
line: 10,
|
||||
column: 1,
|
||||
offset: 140,
|
||||
}),
|
||||
},
|
||||
range: ranges.create(9, 0, 9, 0),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -69,18 +57,7 @@ describe('generateLinkReferences', () => {
|
||||
|
||||
const expected = {
|
||||
newText: '',
|
||||
range: {
|
||||
start: pointForNote(note, {
|
||||
line: 7,
|
||||
column: 1,
|
||||
offset: 105,
|
||||
}),
|
||||
end: pointForNote(note, {
|
||||
line: 9,
|
||||
column: 43,
|
||||
offset: 269,
|
||||
}),
|
||||
},
|
||||
range: ranges.create(6, 0, 8, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -100,18 +77,7 @@ describe('generateLinkReferences', () => {
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: {
|
||||
start: pointForNote(note, {
|
||||
line: 9,
|
||||
column: 1,
|
||||
offset: 145,
|
||||
}),
|
||||
end: pointForNote(note, {
|
||||
line: 11,
|
||||
column: 43,
|
||||
offset: 312,
|
||||
}),
|
||||
},
|
||||
range: ranges.create(8, 0, 10, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
@@ -143,22 +109,3 @@ describe('generateLinkReferences', () => {
|
||||
function textForNote(note: Note, text: string): string {
|
||||
return text.split('\n').join(note.source.eol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will adjust a point to take into account the EOL length
|
||||
* of the note
|
||||
* Necessary when running tests on windows
|
||||
*
|
||||
* @param note the note we are adjusting for
|
||||
* @param pos starting position
|
||||
*/
|
||||
function pointForNote(
|
||||
note: Note,
|
||||
pos: { line: number; column: number; offset: number }
|
||||
) {
|
||||
const rows = pos.line - 1;
|
||||
return {
|
||||
...pos,
|
||||
offset: pos.offset - rows + rows * note.source.eol.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,8 +119,10 @@ this is a [link to intro](#introduction)
|
||||
.set(noteE)
|
||||
.resolveLinks();
|
||||
|
||||
expect(workspace.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(workspace.getLinks(noteA.uri)).toEqual([
|
||||
expect(workspace.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(workspace.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
noteB.uri,
|
||||
noteC.uri,
|
||||
noteD.uri,
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Reference types', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notes workspace', () => {
|
||||
describe('Workspace resources', () => {
|
||||
it('Adds notes to workspace', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(createTestNote({ uri: '/page-a.md' }));
|
||||
@@ -67,6 +67,58 @@ describe('Notes workspace', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace links', () => {
|
||||
it('Supports multiple connections between the same resources', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/note-a.md',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
expect(ws.getBacklinks(noteA.uri)).toEqual([
|
||||
{
|
||||
source: noteB.uri,
|
||||
target: noteA.uri,
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
},
|
||||
{
|
||||
source: noteB.uri,
|
||||
target: noteA.uri,
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('Supports removing a single link amongst several between two resources', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/note-a.md',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
|
||||
});
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
|
||||
expect(ws.getBacklinks(noteA.uri).length).toEqual(2);
|
||||
|
||||
const noteBBis = createTestNote({
|
||||
uri: '/note-b.md',
|
||||
links: [{ to: noteA.uri.path }],
|
||||
});
|
||||
ws.set(noteBBis);
|
||||
expect(ws.getBacklinks(noteA.uri).length).toEqual(1);
|
||||
|
||||
ws.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wikilinks', () => {
|
||||
it('Can be defined with basename, relative path, absolute path, extension', () => {
|
||||
const noteA = createTestNote({
|
||||
@@ -95,7 +147,7 @@ describe('Wikilinks', () => {
|
||||
expect(
|
||||
ws
|
||||
.getLinks(noteA.uri)
|
||||
.map(link => link.path)
|
||||
.map(link => link.target.path)
|
||||
.sort()
|
||||
).toEqual([
|
||||
'/absolute/path/page-d.md',
|
||||
@@ -136,7 +188,7 @@ describe('Wikilinks', () => {
|
||||
expect(
|
||||
ws
|
||||
.getBacklinks(noteA.uri)
|
||||
.map(link => link.path)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
|
||||
});
|
||||
@@ -161,6 +213,7 @@ describe('Wikilinks', () => {
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: noteB.uri,
|
||||
link: expect.objectContaining({ type: 'wikilink', slug: 'page-b' }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,7 +231,13 @@ describe('Wikilinks', () => {
|
||||
.set(noteB2)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB1.uri]);
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([
|
||||
{
|
||||
source: noteA.uri,
|
||||
target: noteB1.uri,
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Resolves path wikilink in case of name conflict', () => {
|
||||
@@ -197,7 +256,10 @@ describe('Wikilinks', () => {
|
||||
.set(noteB3)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB2.uri, noteB3.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
noteB2.uri,
|
||||
noteB3.uri,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Supports attachments', () => {
|
||||
@@ -222,8 +284,12 @@ describe('Wikilinks', () => {
|
||||
.set(attachmentB)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getBacklinks(attachmentA.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(attachmentB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(ws.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Resolves conflicts alphabetically - part 1', () => {
|
||||
@@ -243,7 +309,9 @@ describe('Wikilinks', () => {
|
||||
.set(attachmentABis)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([attachmentABis.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
attachmentABis.uri,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Resolves conflicts alphabetically - part 2', () => {
|
||||
@@ -263,7 +331,9 @@ describe('Wikilinks', () => {
|
||||
.set(attachmentA)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([attachmentABis.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
attachmentABis.uri,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -289,16 +359,28 @@ describe('markdown direct links', () => {
|
||||
expect(
|
||||
ws
|
||||
.getLinks(noteA.uri)
|
||||
.map(link => link.path)
|
||||
.map(link => link.target.path)
|
||||
.sort()
|
||||
).toEqual(['/path/to/another/page-b.md', '/path/to/more/page-c.md']);
|
||||
|
||||
expect(ws.getLinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getLinks(noteB.uri).map(l => l.target)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteA.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
expect(ws.getConnections(noteA.uri)).toEqual([
|
||||
{ source: noteA.uri, target: noteB.uri },
|
||||
{ source: noteA.uri, target: noteC.uri },
|
||||
{ source: noteB.uri, target: noteA.uri },
|
||||
{
|
||||
source: noteA.uri,
|
||||
target: noteB.uri,
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
},
|
||||
{
|
||||
source: noteA.uri,
|
||||
target: noteC.uri,
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
},
|
||||
{
|
||||
source: noteB.uri,
|
||||
target: noteA.uri,
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -315,10 +397,12 @@ describe('Placeholders', () => {
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/somewhere/page-b.md'),
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
});
|
||||
expect(ws.getAllConnections()[1]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/path/to/page-c.md'),
|
||||
link: expect.objectContaining({ type: 'link' }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -333,6 +417,7 @@ describe('Placeholders', () => {
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('page-b'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
});
|
||||
it('Treats wikilink with definition to non-existing file as placeholders', () => {
|
||||
@@ -356,10 +441,12 @@ describe('Placeholders', () => {
|
||||
expect(ws.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/somewhere/page-b.md'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
expect(ws.getAllConnections()[1]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: placeholderUri('/path/to/page-c.md'),
|
||||
link: expect.objectContaining({ type: 'wikilink' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -383,9 +470,9 @@ describe('Updating workspace happy path', () => {
|
||||
.set(noteC)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
@@ -394,18 +481,18 @@ describe('Updating workspace happy path', () => {
|
||||
});
|
||||
ws.set(noteABis);
|
||||
// change is not propagated immediately
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
|
||||
// recompute the links
|
||||
ws.resolveLinks();
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
expect(
|
||||
ws
|
||||
.getBacklinks(noteC.uri)
|
||||
.map(link => link.path)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
|
||||
});
|
||||
@@ -423,8 +510,8 @@ describe('Updating workspace happy path', () => {
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
@@ -443,7 +530,9 @@ describe('Updating workspace happy path', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([placeholderUri('page-b')]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('page-b'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
|
||||
// add note-b
|
||||
@@ -471,8 +560,8 @@ describe('Updating workspace happy path', () => {
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
@@ -493,7 +582,7 @@ describe('Updating workspace happy path', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks();
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('/path/to/another/page-b.md'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
@@ -555,9 +644,9 @@ describe('Monitoring of workspace state', () => {
|
||||
.set(noteC)
|
||||
.resolveLinks(true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.getBacklinks(noteC.uri).map(l => l.source)).toEqual([noteB.uri]);
|
||||
|
||||
// update the note
|
||||
const noteABis = createTestNote({
|
||||
@@ -566,14 +655,15 @@ describe('Monitoring of workspace state', () => {
|
||||
});
|
||||
ws.set(noteABis);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
|
||||
expect(
|
||||
ws
|
||||
.getBacklinks(noteC.uri)
|
||||
.map(link => link.path)
|
||||
.map(link => link.source.path)
|
||||
.sort()
|
||||
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
|
||||
ws.dispose();
|
||||
});
|
||||
|
||||
it('Removing target note should produce placeholder for wikilinks', () => {
|
||||
@@ -589,8 +679,8 @@ describe('Monitoring of workspace state', () => {
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
@@ -598,6 +688,7 @@ describe('Monitoring of workspace state', () => {
|
||||
|
||||
expect(() => ws.get(noteB.uri)).toThrow();
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
ws.dispose();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for wikilinks', () => {
|
||||
@@ -608,7 +699,9 @@ describe('Monitoring of workspace state', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks(true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([placeholderUri('page-b')]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('page-b'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
|
||||
|
||||
// add note-b
|
||||
@@ -620,6 +713,7 @@ describe('Monitoring of workspace state', () => {
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
ws.dispose();
|
||||
});
|
||||
|
||||
it('Removing target note should produce placeholder for direct links', () => {
|
||||
@@ -635,8 +729,8 @@ describe('Monitoring of workspace state', () => {
|
||||
.set(noteB)
|
||||
.resolveLinks(true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
|
||||
expect(ws.getBacklinks(noteB.uri).map(l => l.source)).toEqual([noteA.uri]);
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
|
||||
// remove note-b
|
||||
@@ -646,6 +740,7 @@ describe('Monitoring of workspace state', () => {
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
'placeholder'
|
||||
);
|
||||
ws.dispose();
|
||||
});
|
||||
|
||||
it('Adding note should replace placeholder for direct links', () => {
|
||||
@@ -656,7 +751,7 @@ describe('Monitoring of workspace state', () => {
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(noteA).resolveLinks(true);
|
||||
|
||||
expect(ws.getLinks(noteA.uri)).toEqual([
|
||||
expect(ws.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
placeholderUri('/path/to/another/page-b.md'),
|
||||
]);
|
||||
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
|
||||
@@ -672,6 +767,7 @@ describe('Monitoring of workspace state', () => {
|
||||
|
||||
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
|
||||
expect(ws.get(noteB.uri).type).toEqual('note');
|
||||
ws.dispose();
|
||||
});
|
||||
|
||||
it('removing link to placeholder should remove placeholder', () => {
|
||||
@@ -694,5 +790,6 @@ describe('Monitoring of workspace state', () => {
|
||||
expect(() =>
|
||||
ws.get(placeholderUri('/path/to/another/page-b.md'))
|
||||
).toThrow();
|
||||
ws.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,30 @@ 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.12.0] - 2021-03-22
|
||||
|
||||
Features:
|
||||
|
||||
- Launch daily note on startup (#501 - thanks @ingalles)
|
||||
- Allow absolute directory in daily notes (#482 - thanks @movermeyer)
|
||||
- Navigate wikilinks in Preview even without link definitions (#521)
|
||||
- Workspace navigation (links and wikilinks) powered by Foam (#524)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Ignore directories that have .md extension (#533 - thanks @movermeyer)
|
||||
|
||||
## [0.11.0] - 2021-03-09
|
||||
|
||||
Features:
|
||||
|
||||
- Placeholders Panel: quickly see which placeholders and empty notes are in the workspace (#493 - thanks @joeltjames)
|
||||
- Backlinks panel: now a Foam model powered backlinks panel (#514)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Dataviz: fixed graph node highlighting (#516, #517)
|
||||
|
||||
## [0.10.3] - 2021-03-01
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.10.3",
|
||||
"version": "0.12.0",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -32,8 +32,18 @@
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"markdown.markdownItPlugins": true,
|
||||
"markdown.previewStyles": [
|
||||
"./static/preview/style.css"
|
||||
],
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
"id": "foam-vscode.backlinks",
|
||||
"name": "Backlinks",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Backlinks"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.tags-explorer",
|
||||
"name": "Tag Explorer",
|
||||
@@ -45,6 +55,12 @@
|
||||
"name": "Orphans",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Orphans"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.placeholders",
|
||||
"name": "Placeholders",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Placeholders"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -53,9 +69,17 @@
|
||||
"view": "foam-vscode.tags-explorer",
|
||||
"contents": "No tags found. Notes that contain tags will show up here. You may add tags to a note with a hashtag (#tag) or by adding a tag list to the front matter (tags: tag1, tag2)."
|
||||
},
|
||||
{
|
||||
"view": "foam-vscode.backlinks",
|
||||
"contents": "No backlinks found for selected resource."
|
||||
},
|
||||
{
|
||||
"view": "foam-vscode.orphans",
|
||||
"contents": "No orphans found. Notes that have no backlinks nor links will show up here."
|
||||
},
|
||||
{
|
||||
"view": "foam-vscode.placeholders",
|
||||
"contents": "No placeholders found. Pending links and notes without content will show up here."
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -69,6 +93,16 @@
|
||||
"command": "foam-vscode.group-orphans-off",
|
||||
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-by-folder",
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == false",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-off",
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == true",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
@@ -79,6 +113,18 @@
|
||||
{
|
||||
"command": "foam-vscode.group-orphans-off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-by-folder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"when": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -115,6 +161,10 @@
|
||||
"command": "foam-vscode.create-note-from-template",
|
||||
"title": "Foam: Create New Note From Template"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"title": "Foam: Open Resource"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-orphans-by-folder",
|
||||
"title": "Foam: Group Orphans By Folder",
|
||||
@@ -125,6 +175,16 @@
|
||||
"title": "Foam: Don't Group Orphans",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-by-folder",
|
||||
"title": "Foam: Group Placeholders By Folder",
|
||||
"icon": "$(list-tree)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.group-placeholders-off",
|
||||
"title": "Foam: Don't Group Placeholders",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-new-template",
|
||||
"title": "Foam: Create New Template"
|
||||
@@ -170,6 +230,11 @@
|
||||
"Disable wikilink definitions generation"
|
||||
]
|
||||
},
|
||||
"foam.openDailyNote.onStartup": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"default": false
|
||||
},
|
||||
"foam.openDailyNote.fileExtension": {
|
||||
"type": "string",
|
||||
"scope": "resource",
|
||||
@@ -222,6 +287,30 @@
|
||||
"markdownDescription": "Group orphans report entries by.",
|
||||
"scope": "resource"
|
||||
},
|
||||
"foam.placeholders.exclude": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"default": [],
|
||||
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the placeholders report. To ignore the all the content of a given folder, use `**<folderName>/**/*`",
|
||||
"scope": "resource"
|
||||
},
|
||||
"foam.placeholders.groupBy": {
|
||||
"type": [
|
||||
"string"
|
||||
],
|
||||
"enum": [
|
||||
"off",
|
||||
"folder"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Disable grouping",
|
||||
"Group by folder"
|
||||
],
|
||||
"default": "folder",
|
||||
"markdownDescription": "Group blank note report entries by.",
|
||||
"scope": "resource"
|
||||
},
|
||||
"foam.dateSnippets.afterCompletion": {
|
||||
"type": "string",
|
||||
"default": "createNote",
|
||||
@@ -279,6 +368,7 @@
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/remove-markdown": "^0.1.1",
|
||||
"@types/vscode": "^1.47.1",
|
||||
@@ -290,6 +380,7 @@
|
||||
"jest": "^26.2.2",
|
||||
"jest-environment-vscode": "^1.0.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"markdown-it": "^12.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^26.4.4",
|
||||
"typescript": "^3.8.3",
|
||||
@@ -297,8 +388,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"foam-core": "^0.10.3",
|
||||
"foam-core": "^0.12.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"remove-markdown": "^0.3.0"
|
||||
}
|
||||
|
||||
74
packages/foam-vscode/src/dated-notes.test.ts
Normal file
74
packages/foam-vscode/src/dated-notes.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { getDailyNotePath } from './dated-notes';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories (Posix paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'journal/subdir');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`journal[\\\\/]subdir[\\\\/]${isoDate}.md$`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification (Posix paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', '/absolute_path/journal');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`^/absolute_path/journal/${isoDate}.md`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Adds the root directory to relative directories (Windows paths)', async () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'journal\\subdir');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`journal[\\\\/]subdir[\\\\/]${isoDate}.md$`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Uses absolute directories without modification (Windows paths)', async () => {
|
||||
// While technically the test passes on all OS's, it's only because the test is overly loose.
|
||||
// On Posix systems, this test actually does modify the path, since Windows style paths are
|
||||
// considered to be relative paths. So while this test passes on Posix systems, it is not
|
||||
// because it treats it as an absolute path, but rather that the test doesn't check the same thing.
|
||||
// This was considered "good enough" instead of introducing a dependency like `skip-if` to skip the
|
||||
// test on Posix systems.
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const isoDate = `${year}-0${month}-0${day}`;
|
||||
|
||||
await workspace
|
||||
.getConfiguration('foam')
|
||||
.update('openDailyNote.directory', 'C:\\absolute_path\\journal');
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
expect(getDailyNotePath(foamConfiguration, date).path).toMatch(
|
||||
new RegExp(`/C:[\\\\/]absolute_path[\\\\/]journal[\\\\/]${isoDate}.md`)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import { dirname, join } from 'path';
|
||||
import dateFormat from 'dateformat';
|
||||
import * as fs from 'fs';
|
||||
import { docConfig, focusNote, pathExists } from './utils';
|
||||
import { isAbsolute } from 'path';
|
||||
import { docConfig, focusNote, getDirname, pathExists } from './utils';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
@@ -17,13 +18,24 @@ async function openDailyNoteFor(date?: Date) {
|
||||
);
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
}
|
||||
function getDailyNotePath(configuration: WorkspaceConfiguration, date: Date) {
|
||||
const rootDirectory = workspace.workspaceFolders[0].uri.fsPath;
|
||||
|
||||
function getDailyNotePath(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
): URI {
|
||||
const dailyNoteDirectory: string =
|
||||
configuration.get('openDailyNote.directory') ?? '.';
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
|
||||
return join(rootDirectory, dailyNoteDirectory, dailyNoteFilename);
|
||||
if (isAbsolute(dailyNoteDirectory)) {
|
||||
return URI.joinPath(URI.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
} else {
|
||||
return URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
dailyNoteDirectory,
|
||||
dailyNoteFilename
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getDailyNoteFileName(
|
||||
@@ -42,7 +54,7 @@ function getDailyNoteFileName(
|
||||
|
||||
async function createDailyNoteIfNotExists(
|
||||
configuration: WorkspaceConfiguration,
|
||||
dailyNotePath: string,
|
||||
dailyNotePath: URI,
|
||||
currentDate: Date
|
||||
) {
|
||||
if (await pathExists(dailyNotePath)) {
|
||||
@@ -56,7 +68,7 @@ async function createDailyNoteIfNotExists(
|
||||
configuration.get('openDailyNote.filenameFormat');
|
||||
|
||||
await fs.promises.writeFile(
|
||||
dailyNotePath,
|
||||
dailyNotePath.fsPath,
|
||||
`# ${dateFormat(currentDate, titleFormat, false)}${docConfig.eol}${
|
||||
docConfig.eol
|
||||
}`
|
||||
@@ -65,11 +77,11 @@ async function createDailyNoteIfNotExists(
|
||||
return true;
|
||||
}
|
||||
|
||||
async function createDailyNoteDirectoryIfNotExists(dailyNotePath: string) {
|
||||
const dailyNoteDirectory = dirname(dailyNotePath);
|
||||
async function createDailyNoteDirectoryIfNotExists(dailyNotePath: URI) {
|
||||
const dailyNoteDirectory = getDirname(dailyNotePath);
|
||||
|
||||
if (!(await pathExists(dailyNoteDirectory))) {
|
||||
await fs.promises.mkdir(dailyNoteDirectory, { recursive: true });
|
||||
await fs.promises.mkdir(dailyNoteDirectory.fsPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import {
|
||||
bootstrap,
|
||||
FoamConfig,
|
||||
Foam,
|
||||
Services,
|
||||
Logger,
|
||||
FileDataStore,
|
||||
} from 'foam-core';
|
||||
import { bootstrap, FoamConfig, Foam, Logger, FileDataStore } from 'foam-core';
|
||||
|
||||
import { features } from './features';
|
||||
import { getConfigFromVscode } from './services/config';
|
||||
@@ -24,19 +17,24 @@ export async function activate(context: ExtensionContext) {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
const dataStore = new FileDataStore(config, watcher);
|
||||
|
||||
const services: Services = {
|
||||
dataStore: dataStore,
|
||||
};
|
||||
const foamPromise: Promise<Foam> = bootstrap(config, services);
|
||||
const foamPromise: Promise<Foam> = bootstrap(config, dataStore);
|
||||
|
||||
features.forEach(f => {
|
||||
f.activate(context, foamPromise);
|
||||
});
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
|
||||
|
||||
context.subscriptions.push(dataStore, foam, watcher);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) => {
|
||||
return res.reduce((acc: markdownit, r: any) => {
|
||||
return r.extendMarkdownIt ? r.extendMarkdownIt(acc) : acc;
|
||||
}, md);
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
Logger.error('An error occurred while bootstrapping Foam', e);
|
||||
window.showErrorMessage(
|
||||
|
||||
148
packages/foam-vscode/src/features/backlinks.spec.ts
Normal file
148
packages/foam-vscode/src/features/backlinks.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { workspace, window } from 'vscode';
|
||||
import { URI, FoamWorkspace, IDataStore } from 'foam-core';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createNote,
|
||||
createTestNote,
|
||||
} from '../test/test-utils';
|
||||
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
|
||||
describe('Backlinks panel', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
await createNote(noteA);
|
||||
await createNote(noteB);
|
||||
await createNote(noteC);
|
||||
});
|
||||
afterAll(async () => {
|
||||
ws.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
const rootUri = workspace.workspaceFolders[0].uri;
|
||||
const ws = new FoamWorkspace();
|
||||
const dataStore = {
|
||||
read: uri => {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
isMatch: uri => uri.path.endsWith('.md'),
|
||||
} as IDataStore;
|
||||
|
||||
const noteA = createTestNote({
|
||||
root: rootUri,
|
||||
uri: './note-a.md',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
root: rootUri,
|
||||
uri: './note-b.md',
|
||||
links: [{ slug: 'note-a' }, { slug: 'note-a' }],
|
||||
});
|
||||
const noteC = createTestNote({
|
||||
root: rootUri,
|
||||
uri: './note-c.md',
|
||||
links: [{ slug: 'note-a' }],
|
||||
});
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.resolveLinks(true);
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(ws, dataStore);
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
provider.target = undefined;
|
||||
});
|
||||
|
||||
// Skipping these as still figuring out how to interact with the provider
|
||||
// running in the test instance of VS Code
|
||||
it.skip('does not target excluded files', async () => {
|
||||
provider.target = URI.file('/excluded-file.txt');
|
||||
expect(await provider.getChildren()).toEqual([]);
|
||||
});
|
||||
it.skip('targets active editor', async () => {
|
||||
const docA = await workspace.openTextDocument(noteA.uri);
|
||||
const docB = await workspace.openTextDocument(noteB.uri);
|
||||
|
||||
await window.showTextDocument(docA);
|
||||
expect(provider.target).toEqual(noteA.uri);
|
||||
|
||||
await window.showTextDocument(docB);
|
||||
expect(provider.target).toEqual(noteB.uri);
|
||||
});
|
||||
|
||||
it('shows linking resources alphaetically by name', async () => {
|
||||
provider.target = noteA.uri;
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.map(n => n.resource.uri.path)).toEqual([
|
||||
noteB.uri.path,
|
||||
noteC.uri.path,
|
||||
]);
|
||||
});
|
||||
it('shows references in range order', async () => {
|
||||
provider.target = noteA.uri;
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
)) as BacklinkTreeItem[];
|
||||
expect(linksFromB.map(l => l.link)).toEqual(
|
||||
noteB.links.sort(
|
||||
(a, b) => a.range.start.character - b.range.start.character
|
||||
)
|
||||
);
|
||||
});
|
||||
it('navigates to the document if clicking on note', async () => {
|
||||
provider.target = noteA.uri;
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes[0].command).toMatchObject({
|
||||
command: OPEN_COMMAND.command,
|
||||
arguments: [expect.objectContaining({ resource: noteB.uri })],
|
||||
});
|
||||
});
|
||||
it('navigates to document with link selection if clicking on backlink', async () => {
|
||||
provider.target = noteA.uri;
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
)) as BacklinkTreeItem[];
|
||||
expect(linksFromB[0].command).toMatchObject({
|
||||
command: 'vscode.open',
|
||||
arguments: [
|
||||
noteB.uri,
|
||||
{
|
||||
selection: expect.arrayContaining([]),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it('refreshes upon changes in the workspace', async () => {
|
||||
let notes: ResourceTreeItem[] = [];
|
||||
provider.target = noteA.uri;
|
||||
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(2);
|
||||
|
||||
const noteD = createTestNote({
|
||||
root: rootUri,
|
||||
uri: './note-d.md',
|
||||
});
|
||||
ws.set(noteD);
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(2);
|
||||
|
||||
const noteDBis = createTestNote({
|
||||
root: rootUri,
|
||||
uri: './note-d.md',
|
||||
links: [{ slug: 'note-a' }],
|
||||
});
|
||||
ws.set(noteDBis);
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(3);
|
||||
expect(notes.map(n => n.resource.uri.path)).toEqual(
|
||||
[noteB.uri, noteC.uri, noteD.uri].map(uri => uri.path)
|
||||
);
|
||||
});
|
||||
});
|
||||
159
packages/foam-vscode/src/features/backlinks.ts
Normal file
159
packages/foam-vscode/src/features/backlinks.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { groupBy } from 'lodash';
|
||||
import {
|
||||
Foam,
|
||||
FoamWorkspace,
|
||||
IDataStore,
|
||||
isNote,
|
||||
NoteLink,
|
||||
Resource,
|
||||
isSameUri,
|
||||
URI,
|
||||
Range,
|
||||
} from 'foam-core';
|
||||
import { getNoteTooltip } from '../utils';
|
||||
import { FoamFeature } from '../types';
|
||||
import { ResourceTreeItem } from '../utils/grouped-resources-tree-data-provider';
|
||||
import { Position } from 'unist';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(
|
||||
foam.workspace,
|
||||
foam.services.dataStore
|
||||
);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
provider.target = vscode.window.activeTextEditor?.document.uri;
|
||||
await provider.refresh();
|
||||
});
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.backlinks', provider),
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
export default feature;
|
||||
|
||||
const isBefore = (a: Range, b: Range) =>
|
||||
a.start.line - b.start.line || a.start.character - b.start.character;
|
||||
|
||||
export class BacklinksTreeDataProvider
|
||||
implements vscode.TreeDataProvider<BacklinkPanelTreeItem> {
|
||||
public target?: URI = undefined;
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeDataEmitter = new vscode.EventEmitter<BacklinkPanelTreeItem | undefined | void>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private dataStore: IDataStore
|
||||
) {}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeDataEmitter.fire();
|
||||
}
|
||||
|
||||
getTreeItem(item: BacklinkPanelTreeItem): vscode.TreeItem {
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(item?: ResourceTreeItem): Thenable<BacklinkPanelTreeItem[]> {
|
||||
const uri = this.target;
|
||||
if (item) {
|
||||
const resource = item.resource;
|
||||
if (!isNote(resource)) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
isSameUri(this.workspace.resolveLink(resource, link), uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
const lines = (await this.dataStore.read(resource.uri)).split('\n');
|
||||
if (link.range.start.line < lines.length) {
|
||||
const line = lines[link.range.start.line];
|
||||
let start = Math.max(0, link.range.start.character - 15);
|
||||
const ellipsis = start === 0 ? '' : '...';
|
||||
|
||||
item.label = `${link.range.start.line}: ${ellipsis}${line.substr(
|
||||
start,
|
||||
300
|
||||
)}`;
|
||||
item.tooltip = getNoteTooltip(line);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
return backlinkRefs;
|
||||
}
|
||||
|
||||
if (!uri || !this.dataStore.isMatch(uri)) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const backlinksByResourcePath = groupBy(
|
||||
this.workspace.getConnections(uri).filter(c => isSameUri(c.target, uri)),
|
||||
b => b.source.path
|
||||
);
|
||||
|
||||
const resources = Object.keys(backlinksByResourcePath)
|
||||
.map(res => backlinksByResourcePath[res][0].source)
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.filter(isNote)
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map(note => {
|
||||
const connections = backlinksByResourcePath[
|
||||
note.uri.path
|
||||
].sort((a, b) => isBefore(a.link.range, b.link.range));
|
||||
const item = new ResourceTreeItem(
|
||||
note,
|
||||
this.dataStore,
|
||||
vscode.TreeItemCollapsibleState.Expanded
|
||||
);
|
||||
item.description = `(${connections.length}) ${item.description}`;
|
||||
return item;
|
||||
});
|
||||
return Promise.resolve(resources);
|
||||
}
|
||||
|
||||
resolveTreeItem(item: BacklinkPanelTreeItem): Promise<BacklinkPanelTreeItem> {
|
||||
return item.resolveTreeItem();
|
||||
}
|
||||
}
|
||||
|
||||
export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
public readonly link: NoteLink
|
||||
) {
|
||||
super(
|
||||
link.type === 'wikilink' ? link.slug : link.label,
|
||||
vscode.TreeItemCollapsibleState.None
|
||||
);
|
||||
this.label = `${link.range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [resource.uri, { selection: link.range }],
|
||||
title: 'Go to link',
|
||||
};
|
||||
}
|
||||
|
||||
resolveTreeItem(): Promise<BacklinkTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
|
||||
type BacklinkPanelTreeItem = ResourceTreeItem | BacklinkTreeItem;
|
||||
@@ -91,11 +91,9 @@ async function createNoteFromTemplate(): Promise<void> {
|
||||
URI.joinPath(templatesDir, selectedTemplate)
|
||||
);
|
||||
const snippet = new SnippetString(templateText.toString());
|
||||
await workspace.fs.writeFile(
|
||||
URI.file(filename),
|
||||
new TextEncoder().encode('')
|
||||
);
|
||||
await focusNote(filename, true);
|
||||
const filenameURI = URI.file(filename);
|
||||
await workspace.fs.writeFile(filenameURI, new TextEncoder().encode(''));
|
||||
await focusNote(filenameURI, true);
|
||||
await window.activeTextEditor.insertSnippet(snippet);
|
||||
}
|
||||
|
||||
@@ -125,11 +123,12 @@ async function createNewTemplate(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const filenameURI = URI.file(filename);
|
||||
await workspace.fs.writeFile(
|
||||
URI.file(filename),
|
||||
filenameURI,
|
||||
new TextEncoder().encode(templateContent)
|
||||
);
|
||||
await focusNote(filename, false);
|
||||
await focusNote(filenameURI, false);
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
|
||||
@@ -156,41 +156,26 @@ async function getWebviewContent(
|
||||
context: vscode.ExtensionContext,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const webviewPath = vscode.Uri.file(
|
||||
path.join(context.extensionPath, 'static', 'dataviz.html')
|
||||
);
|
||||
const file = await vscode.workspace.fs.readFile(webviewPath);
|
||||
const text = new TextDecoder('utf-8').decode(file);
|
||||
const datavizPath = [context.extensionPath, 'static', 'dataviz'];
|
||||
|
||||
const webviewUri = (fileName: string) =>
|
||||
panel.webview
|
||||
.asWebviewUri(
|
||||
vscode.Uri.file(path.join(context.extensionPath, 'static', fileName))
|
||||
)
|
||||
.toString();
|
||||
|
||||
const graphDirectory = path.join('graphs', 'default');
|
||||
const textWithVariables = text
|
||||
.replace(
|
||||
'${graphPath}', // eslint-disable-line
|
||||
'{{' + path.join(graphDirectory, 'graph.js') + '}}'
|
||||
)
|
||||
.replace(
|
||||
'${graphStylesPath}', // eslint-disable-line
|
||||
'{{' + path.join(graphDirectory, 'graph.css') + '}}'
|
||||
const getWebviewUri = (fileName: string) =>
|
||||
panel.webview.asWebviewUri(
|
||||
vscode.Uri.file(path.join(...datavizPath, fileName))
|
||||
);
|
||||
|
||||
// Basic templating. Will replace the script paths with the
|
||||
// appropriate webview URI.
|
||||
const filled = textWithVariables.replace(
|
||||
/<script data-replace src="([^"]+")/g,
|
||||
match => {
|
||||
const indexHtml = await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.file(path.join(...datavizPath, 'index.html'))
|
||||
);
|
||||
|
||||
// Replace the script paths with the appropriate webview URI.
|
||||
const filled = new TextDecoder('utf-8')
|
||||
.decode(indexHtml)
|
||||
.replace(/<script data-replace src="([^"]+")/g, match => {
|
||||
const fileName = match
|
||||
.slice('<script data-replace src="'.length, -1)
|
||||
.trim();
|
||||
return '<script src="' + webviewUri(fileName) + '"';
|
||||
}
|
||||
);
|
||||
return '<script src="' + getWebviewUri(fileName).toString() + '"';
|
||||
});
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
106
packages/foam-vscode/src/features/document-link-provider.spec.ts
Normal file
106
packages/foam-vscode/src/features/document-link-provider.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamWorkspace, createMarkdownParser, uris } from 'foam-core';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils';
|
||||
import { LinkProvider } from './document-link-provider';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
|
||||
describe('Document links provider', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('should not return any link for empty documents', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const { uri, content } = await createFile('');
|
||||
ws.set(parser.parse(uri, content)).resolveLinks();
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not return any link for documents without links', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const { uri, content } = await createFile(
|
||||
'This is some content without links'
|
||||
);
|
||||
ws.set(parser.parse(uri, content)).resolveLinks();
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should support wikilinks', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(`this is a link to [[${fileB.name}]].`);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
ws.set(noteA)
|
||||
.set(noteB)
|
||||
.resolveLinks();
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 27));
|
||||
});
|
||||
|
||||
it('should support regular links', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileB = await createFile('# File B');
|
||||
const fileA = await createFile(
|
||||
`this is a link to [a file](./${fileB.base}).`
|
||||
);
|
||||
ws.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content))
|
||||
.resolveLinks();
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 38));
|
||||
});
|
||||
|
||||
it('should support placeholders', async () => {
|
||||
const ws = new FoamWorkspace();
|
||||
const fileA = await createFile(`this is a link to [[a placeholder]].`);
|
||||
ws.set(parser.parse(fileA.uri, fileA.content)).resolveLinks();
|
||||
|
||||
const { doc } = await showInEditor(fileA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(uris.placeholderUri('a placeholder'))
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 35));
|
||||
});
|
||||
});
|
||||
107
packages/foam-vscode/src/features/document-link-provider.ts
Normal file
107
packages/foam-vscode/src/features/document-link-provider.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, FoamWorkspace, NoteParser, uris } from 'foam-core';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNote, mdDocSelector } from '../utils';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { toVsCodeRange } from '../utils/vsc-utils';
|
||||
|
||||
const linkDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
color: { id: 'textLink.foreground' },
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
color: { id: 'editorWarning.foreground' },
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
let activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
function updateDecorations() {
|
||||
if (!activeEditor) {
|
||||
return;
|
||||
}
|
||||
const note = foam.services.parser.parse(
|
||||
activeEditor.document.uri,
|
||||
activeEditor.document.getText()
|
||||
);
|
||||
let linkRanges = [];
|
||||
let placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = foam.workspace.resolveLink(note, link);
|
||||
if (linkUri.scheme === 'placeholder') {
|
||||
placeholderRanges.push(link.range);
|
||||
} else {
|
||||
linkRanges.push(link.range);
|
||||
}
|
||||
});
|
||||
activeEditor.setDecorations(linkDecoration, linkRanges);
|
||||
activeEditor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
}
|
||||
|
||||
const debouncedUpdateDecorations = debounce(updateDecorations, 500);
|
||||
|
||||
debouncedUpdateDecorations();
|
||||
|
||||
context.subscriptions.push(
|
||||
// Link Provider
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
new LinkProvider(foam.workspace, foam.services.parser)
|
||||
),
|
||||
// Decorations for links
|
||||
linkDecoration,
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
if (editor) {
|
||||
debouncedUpdateDecorations();
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (activeEditor && event.document === activeEditor.document) {
|
||||
debouncedUpdateDecorations();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
constructor(private workspace: FoamWorkspace, private parser: NoteParser) {}
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
|
||||
if (isNote(resource)) {
|
||||
return resource.links.map(link => {
|
||||
const target = this.workspace.resolveLink(resource, link);
|
||||
const command = OPEN_COMMAND.asURI(target);
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
toVsCodeRange(link.range),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = uris.isPlaceholder(target)
|
||||
? `Create note for '${target.path}'`
|
||||
: `Go to ${target.fsPath}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
@@ -8,6 +8,11 @@ import tagsExplorer from './tags-tree-view';
|
||||
import createFromTemplate from './create-from-template';
|
||||
import openRandomNote from './open-random-note';
|
||||
import orphans from './orphans';
|
||||
import placeholders from './placeholders';
|
||||
import backlinks from './backlinks';
|
||||
import utilityCommands from './utility-commands';
|
||||
import documentLinkProvider from './document-link-provider';
|
||||
import previewNavigation from './preview-navigation';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
export const features: FoamFeature[] = [
|
||||
@@ -21,4 +26,9 @@ export const features: FoamFeature[] = [
|
||||
openDatedNote,
|
||||
createFromTemplate,
|
||||
orphans,
|
||||
placeholders,
|
||||
backlinks,
|
||||
documentLinkProvider,
|
||||
utilityCommands,
|
||||
previewNavigation,
|
||||
];
|
||||
|
||||
@@ -14,13 +14,15 @@ import {
|
||||
generateHeading,
|
||||
Foam,
|
||||
Note,
|
||||
ranges,
|
||||
} from 'foam-core';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../settings';
|
||||
import { astPositionToVsCodePosition, isNote } from '../utils';
|
||||
import { isNote } from '../utils';
|
||||
import { toVsCodePosition, toVsCodeRange } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -158,17 +160,17 @@ async function runJanitor(foam: Foam) {
|
||||
// before heading, since inserting a heading changes line numbers below
|
||||
if (definitions) {
|
||||
updatedDefinitionListCount += 1;
|
||||
const start = astPositionToVsCodePosition(definitions.range.start);
|
||||
const end = astPositionToVsCodePosition(definitions.range.end);
|
||||
const start = definitions.range.start;
|
||||
const end = definitions.range.end;
|
||||
|
||||
const range = new Range(start, end);
|
||||
editBuilder.replace(range, definitions!.newText);
|
||||
const range = ranges.createFromPosition(start, end);
|
||||
editBuilder.replace(toVsCodeRange(range), definitions!.newText);
|
||||
}
|
||||
|
||||
if (heading) {
|
||||
updatedHeadingCount += 1;
|
||||
const start = astPositionToVsCodePosition(heading.range.start);
|
||||
editBuilder.replace(start, heading.newText);
|
||||
const start = heading.range.start;
|
||||
editBuilder.replace(toVsCodePosition(start), heading.newText);
|
||||
}
|
||||
});
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { ExtensionContext, commands, workspace } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { openDailyNoteFor } from '../dated-notes';
|
||||
|
||||
@@ -7,6 +7,11 @@ const feature: FoamFeature = {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', openDailyNoteFor)
|
||||
);
|
||||
if (
|
||||
workspace.getConfiguration('foam').get('openDailyNote.onStartup', false)
|
||||
) {
|
||||
commands.executeCommand('foam-vscode.open-daily-note');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const feature: FoamFeature = {
|
||||
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
|
||||
}
|
||||
|
||||
focusNote(notes[randomNoteIndex].uri.path, false);
|
||||
focusNote(notes[randomNoteIndex].uri, false);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,110 +1,32 @@
|
||||
import { OrphansProvider, Directory, OrphansProviderConfig } from './orphans';
|
||||
import { OrphansConfigGroupBy } from '../settings';
|
||||
import { FoamWorkspace } from 'foam-core';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { isOrphan } from './orphans';
|
||||
|
||||
describe('orphans', () => {
|
||||
const orphanA = createTestNote({
|
||||
uri: '/path/orphan-a.md',
|
||||
title: 'Orphan A',
|
||||
const orphanA = createTestNote({
|
||||
uri: '/path/orphan-a.md',
|
||||
title: 'Orphan A',
|
||||
});
|
||||
|
||||
const nonOrphan1 = createTestNote({
|
||||
uri: '/path/non-orphan-1.md',
|
||||
});
|
||||
|
||||
const nonOrphan2 = createTestNote({
|
||||
uri: '/path/non-orphan-2.md',
|
||||
links: [{ slug: 'non-orphan-1' }],
|
||||
});
|
||||
|
||||
const workspace = new FoamWorkspace()
|
||||
.set(orphanA)
|
||||
.set(nonOrphan1)
|
||||
.set(nonOrphan2)
|
||||
.resolveLinks();
|
||||
|
||||
describe('isOrphan', () => {
|
||||
it('should return true when a note with no connections is provided', () => {
|
||||
expect(isOrphan(workspace, orphanA)).toBeTruthy();
|
||||
});
|
||||
const orphanB = createTestNote({
|
||||
uri: '/path-bis/orphan-b.md',
|
||||
title: 'Orphan B',
|
||||
});
|
||||
const orphanC = createTestNote({
|
||||
uri: '/path-exclude/orphan-c.md',
|
||||
title: 'Orphan C',
|
||||
});
|
||||
|
||||
const workspace = new FoamWorkspace()
|
||||
.set(orphanA)
|
||||
.set(orphanB)
|
||||
.set(orphanC)
|
||||
.set(createTestNote({ uri: '/path/non-orphan-1.md' }))
|
||||
.set(
|
||||
createTestNote({
|
||||
uri: '/path/non-orphan-2.md',
|
||||
links: [{ slug: 'non-orphan-1' }],
|
||||
})
|
||||
)
|
||||
.resolveLinks();
|
||||
|
||||
const dataStore = { read: () => '' } as any;
|
||||
|
||||
// Mock config
|
||||
const config: OrphansProviderConfig = {
|
||||
exclude: ['path-exclude/**/*'],
|
||||
groupBy: OrphansConfigGroupBy.Folder,
|
||||
workspacesFsPaths: [''],
|
||||
};
|
||||
|
||||
it('should return the orphans as a folder tree', async () => {
|
||||
const provider = new OrphansProvider(workspace, dataStore, config);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path',
|
||||
description: '1 orphan',
|
||||
notes: [{ title: 'Orphan A' }],
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-bis',
|
||||
description: '1 orphan',
|
||||
notes: [{ title: 'Orphan B' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the orphans in a directory', async () => {
|
||||
const provider = new OrphansProvider(workspace, dataStore, config);
|
||||
const directory = new Directory('/path', [orphanA as any]);
|
||||
const result = await provider.getChildren(directory);
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: 'Orphan A',
|
||||
description: '/path/orphan-a.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the flattened orphans', async () => {
|
||||
const mockConfig = { ...config, groupBy: OrphansConfigGroupBy.Off };
|
||||
const provider = new OrphansProvider(workspace, dataStore, mockConfig);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: 'Orphan A',
|
||||
description: '/path/orphan-a.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: 'Orphan B',
|
||||
description: '/path-bis/orphan-b.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the orphans without exclusion', async () => {
|
||||
const mockConfig = { ...config, exclude: [] };
|
||||
const provider = new OrphansProvider(workspace, dataStore, mockConfig);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-exclude',
|
||||
description: '1 orphan',
|
||||
notes: [{ title: 'Orphan C' }],
|
||||
},
|
||||
]);
|
||||
it('should return false when a note with connections is provided', () => {
|
||||
expect(isOrphan(workspace, nonOrphan1)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, IDataStore, Note, URI, FoamWorkspace } from 'foam-core';
|
||||
import micromatch from 'micromatch';
|
||||
import {
|
||||
getOrphansConfig,
|
||||
OrphansConfig,
|
||||
OrphansConfigGroupBy,
|
||||
} from '../settings';
|
||||
import { Foam, FoamWorkspace, isNote, Resource } from 'foam-core';
|
||||
import { getOrphansConfig } from '../settings';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, getContainsTooltip, isNote } from '../utils';
|
||||
import { GroupedResourcesTreeDataProvider } from '../utils/grouped-resources-tree-data-provider';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -16,190 +10,36 @@ const feature: FoamFeature = {
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const workspacesFsPaths = vscode.workspace.workspaceFolders.map(
|
||||
dir => dir.uri.fsPath
|
||||
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(
|
||||
dir => dir.uri
|
||||
);
|
||||
const provider = new OrphansProvider(
|
||||
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
foam.workspace,
|
||||
foam.services.dataStore,
|
||||
{
|
||||
...getOrphansConfig(),
|
||||
workspacesFsPaths,
|
||||
}
|
||||
'orphans',
|
||||
'orphan',
|
||||
(resource: Resource) => isOrphan(foam.workspace, resource),
|
||||
getOrphansConfig(),
|
||||
workspacesURIs
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
|
||||
vscode.commands.registerCommand(
|
||||
'foam-vscode.group-orphans-by-folder',
|
||||
() => provider.setGroupBy(OrphansConfigGroupBy.Folder)
|
||||
),
|
||||
vscode.commands.registerCommand('foam-vscode.group-orphans-off', () =>
|
||||
provider.setGroupBy(OrphansConfigGroupBy.Off)
|
||||
),
|
||||
...provider.commands,
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
export class OrphansProvider
|
||||
implements vscode.TreeDataProvider<OrphanTreeItem> {
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<OrphanTreeItem | undefined | void> = new vscode.EventEmitter<OrphanTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<OrphanTreeItem | undefined | void> = this._onDidChangeTreeData.event;
|
||||
|
||||
private groupBy: OrphansConfigGroupBy = OrphansConfigGroupBy.Folder;
|
||||
private exclude: string[] = [];
|
||||
private orphans: Note[] = [];
|
||||
private root = vscode.workspace.workspaceFolders[0].uri.path;
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private dataStore: IDataStore,
|
||||
config: OrphansProviderConfig
|
||||
) {
|
||||
this.groupBy = config.groupBy;
|
||||
this.exclude = this.getGlobs(config.workspacesFsPaths, config.exclude);
|
||||
this.setContext();
|
||||
this.computeOrphans();
|
||||
}
|
||||
|
||||
setGroupBy(groupBy: OrphansConfigGroupBy): void {
|
||||
this.groupBy = groupBy;
|
||||
this.setContext();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private setContext(): void {
|
||||
vscode.commands.executeCommand(
|
||||
'setContext',
|
||||
'foam-vscode.orphans-grouped-by-folder',
|
||||
this.groupBy === OrphansConfigGroupBy.Folder
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.computeOrphans();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
getTreeItem(item: OrphanTreeItem): vscode.TreeItem {
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(directory?: Directory): Thenable<OrphanTreeItem[]> {
|
||||
if (!directory && this.groupBy === OrphansConfigGroupBy.Folder) {
|
||||
const directories = Object.entries(this.getOrphansByDirectory())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([dir, orphans]) => new Directory(dir, orphans));
|
||||
return Promise.resolve(directories);
|
||||
}
|
||||
|
||||
if (directory) {
|
||||
const orphans = directory.notes.map(o => new Orphan(o));
|
||||
return Promise.resolve(orphans);
|
||||
}
|
||||
|
||||
const orphans = this.orphans.map(o => new Orphan(o));
|
||||
return Promise.resolve(orphans);
|
||||
}
|
||||
|
||||
async resolveTreeItem(item: OrphanTreeItem): Promise<OrphanTreeItem> {
|
||||
if (item instanceof Orphan) {
|
||||
const content = await this.dataStore.read(item.note.uri);
|
||||
item.tooltip = getNoteTooltip(content);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private computeOrphans(): void {
|
||||
this.orphans = this.workspace
|
||||
.list()
|
||||
.filter(isNote)
|
||||
.filter(note => this.workspace.getConnections(note.uri).length === 0)
|
||||
.filter(note => !this.isMatch(note.uri))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
private isMatch(uri: URI) {
|
||||
return micromatch.isMatch(uri.fsPath, this.exclude);
|
||||
}
|
||||
|
||||
private getGlobs(fsPaths: string[], globs: string[]): string[] {
|
||||
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
||||
for (const fsPath of fsPaths) {
|
||||
let folder = fsPath.replace(/\\/g, '/');
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
exclude.push(...globs.map(g => `${folder}/${g}`));
|
||||
}
|
||||
|
||||
return exclude;
|
||||
}
|
||||
|
||||
private getOrphansByDirectory(): OrphansByDirectory {
|
||||
const orphans: OrphansByDirectory = {};
|
||||
for (const orphan of this.orphans) {
|
||||
const p = orphan.uri.path.replace(this.root, '');
|
||||
const { dir } = path.parse(p);
|
||||
|
||||
if (orphans[dir]) {
|
||||
orphans[dir].push(orphan);
|
||||
} else {
|
||||
orphans[dir] = [orphan];
|
||||
}
|
||||
}
|
||||
|
||||
for (const k in orphans) {
|
||||
orphans[k].sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
return orphans;
|
||||
export function isOrphan(workspace: FoamWorkspace, resource: Resource) {
|
||||
if (isNote(resource)) {
|
||||
return workspace.getConnections(resource.uri).length === 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OrphansProviderConfig extends OrphansConfig {
|
||||
workspacesFsPaths: string[];
|
||||
}
|
||||
|
||||
type OrphansByDirectory = { [key: string]: Note[] };
|
||||
|
||||
type OrphanTreeItem = Orphan | Directory;
|
||||
|
||||
class Orphan extends vscode.TreeItem {
|
||||
constructor(public readonly note: Note) {
|
||||
super(note.title, vscode.TreeItemCollapsibleState.None);
|
||||
this.description = note.uri.path;
|
||||
this.tooltip = undefined;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
title: 'Open File',
|
||||
arguments: [note.uri],
|
||||
};
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon('note');
|
||||
contextValue = 'orphan';
|
||||
}
|
||||
|
||||
export class Directory extends vscode.TreeItem {
|
||||
constructor(public readonly dir: string, public readonly notes: Note[]) {
|
||||
super(dir, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
const s = this.notes.length > 1 ? 's' : '';
|
||||
this.description = `${this.notes.length} orphan${s}`;
|
||||
const titles = this.notes.map(n => n.title);
|
||||
this.tooltip = getContainsTooltip(titles);
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon('folder');
|
||||
contextValue = 'directory';
|
||||
}
|
||||
|
||||
105
packages/foam-vscode/src/features/placeholders.test.ts
Normal file
105
packages/foam-vscode/src/features/placeholders.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
createAttachment,
|
||||
createPlaceholder,
|
||||
createTestNote,
|
||||
} from '../test/test-utils';
|
||||
import { isPlaceholderResource } from './placeholders';
|
||||
|
||||
describe('isPlaceholderResource', () => {
|
||||
it('should return true when a placeholder', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createPlaceholder({
|
||||
uri: '',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when an empty note is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: '',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when an empty note is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: '',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing only whitespace is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: ' \n\t\n\t ',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing only a title is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: '# Title',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when a note containing a title followed by whitespace is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: '# Title \n\t\n \t \n ',
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when there is more than one line containing more than just whitespace', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: '# Title\nA line that is not the title\nAnother line',
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when there is at least one line of non-text content', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createTestNote({
|
||||
uri: '',
|
||||
text: 'A line that is not the title\n',
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when an attachment is provided', () => {
|
||||
expect(
|
||||
isPlaceholderResource(
|
||||
createAttachment({
|
||||
uri: '',
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
59
packages/foam-vscode/src/features/placeholders.ts
Normal file
59
packages/foam-vscode/src/features/placeholders.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam, Resource, isNote, isPlaceholder } from 'foam-core';
|
||||
import { getPlaceholdersConfig } from '../settings';
|
||||
import { FoamFeature } from '../types';
|
||||
import { GroupedResourcesTreeDataProvider } from '../utils/grouped-resources-tree-data-provider';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(
|
||||
dir => dir.uri
|
||||
);
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
foam.workspace,
|
||||
foam.services.dataStore,
|
||||
'placeholders',
|
||||
'placeholder',
|
||||
isPlaceholderResource,
|
||||
getPlaceholdersConfig(),
|
||||
workspacesURIs
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
'foam-vscode.placeholders',
|
||||
provider
|
||||
),
|
||||
...provider.commands,
|
||||
foam.workspace.onDidAdd(() => provider.refresh()),
|
||||
foam.workspace.onDidUpdate(() => provider.refresh()),
|
||||
foam.workspace.onDidDelete(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
export function isPlaceholderResource(resource: Resource) {
|
||||
if (isPlaceholder(resource)) {
|
||||
// A placeholder is, by default, blank
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNote(resource)) {
|
||||
const contentLines = resource.source.text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.filter(line => !line.startsWith('#'));
|
||||
|
||||
return contentLines.length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
34
packages/foam-vscode/src/features/preview-navigation.spec.ts
Normal file
34
packages/foam-vscode/src/features/preview-navigation.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from 'foam-core';
|
||||
import { createPlaceholder, createTestNote } from '../test/test-utils';
|
||||
import { markdownItWithFoamLinks } from './preview-navigation';
|
||||
|
||||
describe('Link generation in preview', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
title: 'My note title',
|
||||
});
|
||||
const placeholder = createPlaceholder({
|
||||
uri: 'placeholder',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA).set(placeholder);
|
||||
const md = markdownItWithFoamLinks(MarkdownIt(), ws);
|
||||
|
||||
it('generates a link to a note', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='${noteA.uri.fsPath}'>note-a</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a link to a placeholder resource', () => {
|
||||
expect(md.render(`[[placeholder]]`)).toEqual(
|
||||
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">placeholder</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a placeholder link to an unknown slug', () => {
|
||||
expect(md.render(`[[random-text]]`)).toEqual(
|
||||
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">random-text</a></p>\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
55
packages/foam-vscode/src/features/preview-navigation.ts
Normal file
55
packages/foam-vscode/src/features/preview-navigation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as vscode from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { Foam, FoamWorkspace, Logger } from 'foam-core';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
_context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) =>
|
||||
markdownItWithFoamLinks(md, foam.workspace),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownItWithFoamLinks = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'connect-wikilinks',
|
||||
regex: /\[\[([^\[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
try {
|
||||
const resource = workspace.find(wikilink);
|
||||
if (resource == null) {
|
||||
return getPlaceholderLink(wikilink);
|
||||
}
|
||||
switch (resource.type) {
|
||||
case 'note':
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='${resource.uri.fsPath}'>${wikilink}</a>`;
|
||||
case 'attachment':
|
||||
return `<a class='foam-attachment-link' title='attachment' href='${resource.uri.fsPath}'>${wikilink}</a>`;
|
||||
case 'placeholder':
|
||||
return getPlaceholderLink(wikilink);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
e
|
||||
);
|
||||
return getPlaceholderLink(wikilink);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getPlaceholderLink = (content: string) =>
|
||||
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
|
||||
|
||||
export default feature;
|
||||
52
packages/foam-vscode/src/features/utility-commands.ts
Normal file
52
packages/foam-vscode/src/features/utility-commands.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { commands } from 'vscode';
|
||||
import { createNoteFromPlacehoder, focusNote, isSome } from '../utils';
|
||||
|
||||
export const OPEN_COMMAND = {
|
||||
command: 'foam-vscode.open-resource',
|
||||
title: 'Foam: Open Resource',
|
||||
|
||||
execute: async (params: { resource: vscode.Uri }) => {
|
||||
const { resource } = params;
|
||||
switch (resource.scheme) {
|
||||
case 'file':
|
||||
return vscode.commands.executeCommand('vscode.open', resource);
|
||||
|
||||
case 'placeholder':
|
||||
const newNote = await createNoteFromPlacehoder(resource);
|
||||
|
||||
if (isSome(newNote)) {
|
||||
const title = resource.path.split('/').slice(-1);
|
||||
const snippet = new vscode.SnippetString(
|
||||
'# ${1:' + title + '}\n\n$0'
|
||||
);
|
||||
await focusNote(newNote, true);
|
||||
await vscode.window.activeTextEditor.insertSnippet(snippet);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'attachment':
|
||||
return vscode.window.showInformationMessage(
|
||||
'Opening attachments is not supported yet'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
asURI: (resource: vscode.Uri) =>
|
||||
vscode.Uri.parse(
|
||||
`command:${OPEN_COMMAND.command}?${encodeURIComponent(
|
||||
JSON.stringify({ resource: resource })
|
||||
)}`
|
||||
),
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(OPEN_COMMAND.command, OPEN_COMMAND.execute)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -72,7 +72,7 @@ const feature: FoamFeature = {
|
||||
|
||||
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
|
||||
foam.workspace.set(
|
||||
foam.parse(document.uri, document.getText(), docConfig.eol)
|
||||
foam.services.parser.parse(document.uri, document.getText())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,19 +39,27 @@ export function getFoamLoggerLevel(): LogLevel {
|
||||
}
|
||||
|
||||
/** Retrieve the orphans configuration */
|
||||
export function getOrphansConfig(): OrphansConfig {
|
||||
export function getOrphansConfig(): GroupedResourcesConfig {
|
||||
const orphansConfig = workspace.getConfiguration('foam.orphans');
|
||||
const exclude: string[] = orphansConfig.get('exclude');
|
||||
const groupBy: OrphansConfigGroupBy = orphansConfig.get('groupBy');
|
||||
const groupBy: GroupedResoucesConfigGroupBy = orphansConfig.get('groupBy');
|
||||
return { exclude, groupBy };
|
||||
}
|
||||
|
||||
export interface OrphansConfig {
|
||||
exclude: string[];
|
||||
groupBy: OrphansConfigGroupBy;
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
const placeholderCfg = workspace.getConfiguration('foam.placeholders');
|
||||
const exclude: string[] = placeholderCfg.get('exclude');
|
||||
const groupBy: GroupedResoucesConfigGroupBy = placeholderCfg.get('groupBy');
|
||||
return { exclude, groupBy };
|
||||
}
|
||||
|
||||
export enum OrphansConfigGroupBy {
|
||||
export interface GroupedResourcesConfig {
|
||||
exclude: string[];
|
||||
groupBy: GroupedResoucesConfigGroupBy;
|
||||
}
|
||||
|
||||
export enum GroupedResoucesConfigGroupBy {
|
||||
Folder = 'folder',
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* We use the following convention in Foam:
|
||||
* - *.test.ts are unit tests
|
||||
* they might still rely on vscode API and hence will be run in this environment, but
|
||||
* are fundamentally about testing functions in isolations
|
||||
* are fundamentally about testing functions in isolation
|
||||
* - *.spec.ts are integration tests
|
||||
* they will make direct use of the vscode API to be invoked as commands, create editors,
|
||||
* and so on..
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
// TODO: this file has some utility functions also present in foam-core testing
|
||||
// they should be consolidated
|
||||
|
||||
import { URI, Attachment, NoteLinkDefinition, Note } from 'foam-core';
|
||||
import * as vscode from 'vscode';
|
||||
import path from 'path';
|
||||
import {
|
||||
URI,
|
||||
Attachment,
|
||||
NoteLinkDefinition,
|
||||
Note,
|
||||
Placeholder,
|
||||
parseUri,
|
||||
ranges,
|
||||
} from 'foam-core';
|
||||
import { TextEncoder } from 'util';
|
||||
|
||||
const position = {
|
||||
start: { line: 1, column: 1 },
|
||||
end: { line: 1, column: 1 },
|
||||
};
|
||||
const position = ranges.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
@@ -19,6 +27,13 @@ const eol = '\n';
|
||||
*/
|
||||
export const strToUri = URI.file;
|
||||
|
||||
export const createPlaceholder = (params: { uri: string }): Placeholder => {
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
type: 'placeholder',
|
||||
};
|
||||
};
|
||||
|
||||
export const createAttachment = (params: { uri: string }): Attachment => {
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
@@ -32,30 +47,39 @@ export const createTestNote = (params: {
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
text?: string;
|
||||
root?: URI;
|
||||
}): Note => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
uri: parseUri(root, params.uri),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? null,
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map(link =>
|
||||
'slug' in link
|
||||
? params.links.map((link, index) => {
|
||||
const range = ranges.create(
|
||||
position.start.line + index,
|
||||
position.start.character,
|
||||
position.start.line + index,
|
||||
position.end.character
|
||||
);
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
target: link.slug,
|
||||
position: position,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
target: link.to,
|
||||
label: 'link text',
|
||||
}
|
||||
)
|
||||
range: range,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
source: {
|
||||
eol: eol,
|
||||
@@ -65,3 +89,61 @@ export const createTestNote = (params: {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const cleanWorkspace = async () => {
|
||||
const files = await vscode.workspace.findFiles('**', '.vscode');
|
||||
await Promise.all(files.map(f => vscode.workspace.fs.delete(f)));
|
||||
};
|
||||
|
||||
export const wait = (ms: number) =>
|
||||
new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const showInEditor = async (uri: URI) => {
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
return { doc, editor };
|
||||
};
|
||||
|
||||
export const closeEditors = async () => {
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
await wait(100);
|
||||
};
|
||||
|
||||
const chars = 'abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWYXZ1234567890';
|
||||
export const randomString = (len = 5) =>
|
||||
new Array(len)
|
||||
.fill('')
|
||||
.map(() => chars.charAt(Math.floor(Math.random() * chars.length)))
|
||||
.join('');
|
||||
|
||||
/**
|
||||
* Creates a file with a some content.
|
||||
*
|
||||
* @param content the file content
|
||||
* @param path relative file path
|
||||
* @returns an object containing various information about the file created
|
||||
*/
|
||||
export const createFile = async (content: string, filepath?: string) => {
|
||||
const rootUri = vscode.workspace.workspaceFolders[0].uri;
|
||||
filepath = filepath ?? randomString() + '.md';
|
||||
const uri = vscode.Uri.joinPath(rootUri, filepath);
|
||||
const filenameComponents = path.parse(uri.fsPath);
|
||||
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(content));
|
||||
return { uri, content, ...filenameComponents };
|
||||
};
|
||||
|
||||
export const createNote = (r: Note) => {
|
||||
let content = `# ${r.title}
|
||||
|
||||
some content and ${r.links
|
||||
.map(l =>
|
||||
l.type === 'wikilink' ? `[[${l.slug}]]` : `[${l.label}](${l.target})`
|
||||
)
|
||||
.join(' some content between links.\n')}
|
||||
last line.
|
||||
`;
|
||||
return vscode.workspace.fs.writeFile(
|
||||
r.uri,
|
||||
new TextEncoder().encode(content)
|
||||
);
|
||||
};
|
||||
|
||||
5
packages/foam-vscode/src/types.d.ts
vendored
5
packages/foam-vscode/src/types.d.ts
vendored
@@ -2,5 +2,8 @@ import { ExtensionContext } from 'vscode';
|
||||
import { Foam } from 'foam-core';
|
||||
|
||||
export interface FoamFeature {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void;
|
||||
activate: (
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => Promise<any> | void;
|
||||
}
|
||||
|
||||
@@ -6,21 +6,17 @@ import {
|
||||
Position,
|
||||
TextEditor,
|
||||
workspace,
|
||||
Uri,
|
||||
Selection,
|
||||
MarkdownString,
|
||||
version,
|
||||
Uri,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { Logger, Resource, Note } from 'foam-core';
|
||||
import { Logger, Resource, Note, uris, URI } from 'foam-core';
|
||||
import matter from 'gray-matter';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
interface Point {
|
||||
line: number;
|
||||
column: number;
|
||||
offset?: number;
|
||||
}
|
||||
import { TextEncoder } from 'util';
|
||||
import { posix } from 'path';
|
||||
|
||||
export const docConfig = { tab: ' ', eol: '\r\n' };
|
||||
|
||||
@@ -92,15 +88,6 @@ export function dropExtension(path: string): string {
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param point ast position (1-indexed)
|
||||
* @returns VSCode position (0-indexed)
|
||||
*/
|
||||
export const astPositionToVsCodePosition = (point: Point): Position => {
|
||||
return new Position(point.line - 1, point.column - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for the "Copy to Clipboard Without Brackets" command
|
||||
*
|
||||
@@ -144,14 +131,23 @@ export function toTitleCase(word: string): string {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URI that represents the dirname of a URI
|
||||
*
|
||||
* @param uri The URI to get the dirname from
|
||||
*/
|
||||
export function getDirname(uri: URI): URI {
|
||||
return URI.file(posix.parse(uri.path).dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given path exists in the file system
|
||||
*
|
||||
* @param path The path to verify
|
||||
*/
|
||||
export function pathExists(path: string) {
|
||||
export function pathExists(path: URI) {
|
||||
return fs.promises
|
||||
.access(path, fs.constants.F_OK)
|
||||
.access(path.fsPath, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
@@ -179,8 +175,8 @@ export function isNone<T>(
|
||||
return value == null; // eslint-disable-line
|
||||
}
|
||||
|
||||
export async function focusNote(notePath: string, moveCursorToEnd: boolean) {
|
||||
const document = await workspace.openTextDocument(Uri.file(notePath));
|
||||
export async function focusNote(notePath: URI, moveCursorToEnd: boolean) {
|
||||
const document = await workspace.openTextDocument(notePath);
|
||||
const editor = await window.showTextDocument(document);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
@@ -267,3 +263,28 @@ export function stripImages(markdown: string): string {
|
||||
export const isNote = (resource: Resource): resource is Note => {
|
||||
return resource.type === 'note';
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a note from the given placeholder Uri.
|
||||
*
|
||||
* @param placeholder the placeholder Uri
|
||||
* @returns the Uri of the created note, or `null`
|
||||
* if the Uri was not a placeholder or no reference directory could be found
|
||||
*/
|
||||
export const createNoteFromPlacehoder = async (
|
||||
placeholder: Uri
|
||||
): Promise<Uri | null> => {
|
||||
const basedir =
|
||||
workspace.workspaceFolders.length > 0
|
||||
? workspace.workspaceFolders[0].uri
|
||||
: window.activeTextEditor?.document.uri
|
||||
? uris.getDir(window.activeTextEditor!.document.uri)
|
||||
: null;
|
||||
|
||||
if (isSome(basedir)) {
|
||||
const target = uris.placeholderToResourceUri(basedir, placeholder);
|
||||
await workspace.fs.writeFile(target, new TextEncoder().encode(''));
|
||||
return target;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { FoamWorkspace, getTitle, Resource } from 'foam-core';
|
||||
import { OPEN_COMMAND } from '../features/utility-commands';
|
||||
import {
|
||||
GroupedResoucesConfigGroupBy,
|
||||
GroupedResourcesConfig,
|
||||
} from '../settings';
|
||||
import { createTestNote, strToUri } from '../test/test-utils';
|
||||
import {
|
||||
DirectoryTreeItem,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
} from './grouped-resources-tree-data-provider';
|
||||
|
||||
describe('GroupedResourcesTreeDataProvider', () => {
|
||||
const isMatch = (resource: Resource) => {
|
||||
return getTitle(resource).length === 3;
|
||||
};
|
||||
const matchingNote1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
|
||||
const matchingNote2 = createTestNote({
|
||||
uri: '/path-bis/XYZ.md',
|
||||
title: 'XYZ',
|
||||
});
|
||||
const excludedPathNote = createTestNote({
|
||||
uri: '/path-exclude/HIJ.m',
|
||||
title: 'HIJ',
|
||||
});
|
||||
const notMatchingNote = createTestNote({
|
||||
uri: '/path-bis/ABCDEFG.md',
|
||||
title: 'ABCDEFG',
|
||||
});
|
||||
|
||||
const workspace = new FoamWorkspace()
|
||||
.set(matchingNote1)
|
||||
.set(matchingNote2)
|
||||
.set(excludedPathNote)
|
||||
.set(notMatchingNote)
|
||||
.resolveLinks();
|
||||
|
||||
const dataStore = { read: () => '' } as any;
|
||||
|
||||
// Mock config
|
||||
const config: GroupedResourcesConfig = {
|
||||
exclude: ['path-exclude/**/*'],
|
||||
groupBy: GroupedResoucesConfigGroupBy.Folder,
|
||||
};
|
||||
|
||||
it('should return the grouped resources as a folder tree', async () => {
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
workspace,
|
||||
dataStore,
|
||||
'length3',
|
||||
'note',
|
||||
isMatch,
|
||||
config,
|
||||
[strToUri('')]
|
||||
);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path',
|
||||
description: '1 note',
|
||||
resources: [{ title: matchingNote1.title }],
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-bis',
|
||||
description: '1 note',
|
||||
resources: [{ title: matchingNote2.title }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the grouped resources in a directory', async () => {
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
workspace,
|
||||
dataStore,
|
||||
'length3',
|
||||
'note',
|
||||
isMatch,
|
||||
config,
|
||||
[strToUri('')]
|
||||
);
|
||||
const directory = new DirectoryTreeItem(
|
||||
'/path',
|
||||
[matchingNote1 as any],
|
||||
'note'
|
||||
);
|
||||
const result = await provider.getChildren(directory);
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: 'ABC',
|
||||
description: '/path/ABC.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the flattened resources', async () => {
|
||||
const mockConfig = {
|
||||
...config,
|
||||
groupBy: GroupedResoucesConfigGroupBy.Off,
|
||||
};
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
workspace,
|
||||
dataStore,
|
||||
'length3',
|
||||
'note',
|
||||
isMatch,
|
||||
mockConfig,
|
||||
[strToUri('')]
|
||||
);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: matchingNote1.title,
|
||||
description: '/path/ABC.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
},
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: matchingNote2.title,
|
||||
description: '/path-bis/XYZ.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the grouped resources without exclusion', async () => {
|
||||
const mockConfig = { ...config, exclude: [] };
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
workspace,
|
||||
dataStore,
|
||||
'length3',
|
||||
'note',
|
||||
isMatch,
|
||||
mockConfig,
|
||||
[strToUri('')]
|
||||
);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-exclude',
|
||||
description: '1 note',
|
||||
resources: [{ title: excludedPathNote.title }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should dynamically set the description', async () => {
|
||||
const description = 'test description';
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
workspace,
|
||||
dataStore,
|
||||
'length3',
|
||||
description,
|
||||
isMatch,
|
||||
config,
|
||||
[strToUri('')]
|
||||
);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path',
|
||||
description: `1 ${description}`,
|
||||
resources: expect.anything(),
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-bis',
|
||||
description: `1 ${description}`,
|
||||
resources: expect.anything(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { IDataStore, URI, FoamWorkspace, Resource, getTitle } from 'foam-core';
|
||||
import micromatch from 'micromatch';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResoucesConfigGroupBy,
|
||||
} from '../settings';
|
||||
import { getContainsTooltip, getNoteTooltip } from '../utils';
|
||||
import { OPEN_COMMAND } from '../features/utility-commands';
|
||||
|
||||
/**
|
||||
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
|
||||
* iterate over each Resource in the FoamWorkspace, call the provided filter predicate, and
|
||||
* display the Resources.
|
||||
*
|
||||
* **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file:
|
||||
* ```
|
||||
* foam-vscode.group-${providerId}-by-folder
|
||||
* foam-vscode.group-${providerId}-off
|
||||
* ```
|
||||
* Where `providerId` is the same string provided to the constructor. You must also register the commands in your context subscriptions as follows:
|
||||
* ```
|
||||
* const provider = new GroupedResourcesTreeDataProvider(
|
||||
...
|
||||
);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
'foam-vscode.placeholders',
|
||||
provider
|
||||
),
|
||||
...provider.commands,
|
||||
);
|
||||
```
|
||||
* @export
|
||||
* @class GroupedResourcesTreeDataProvider
|
||||
* @implements {vscode.TreeDataProvider<GroupedResourceTreeItem>}
|
||||
*/
|
||||
export class GroupedResourcesTreeDataProvider
|
||||
implements vscode.TreeDataProvider<GroupedResourceTreeItem> {
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<GroupedResourceTreeItem | undefined | void> = new vscode.EventEmitter<GroupedResourceTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<GroupedResourceTreeItem | undefined | void> = this._onDidChangeTreeData.event;
|
||||
// prettier-ignore
|
||||
private groupBy: GroupedResoucesConfigGroupBy = GroupedResoucesConfigGroupBy.Folder;
|
||||
private exclude: string[] = [];
|
||||
private resources: Resource[] = [];
|
||||
private root = vscode.workspace.workspaceFolders[0].uri.path;
|
||||
|
||||
/**
|
||||
* Creates an instance of GroupedResourcesTreeDataProvider.
|
||||
* **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file:
|
||||
* ```
|
||||
* foam-vscode.group-${providerId}-by-folder
|
||||
* foam-vscode.group-${providerId}-off
|
||||
* ```
|
||||
* Where `providerId` is the same string provided to this constructor. You must also register the commands in your context subscriptions as follows:
|
||||
* ```
|
||||
* const provider = new GroupedResourcesTreeDataProvider(
|
||||
...
|
||||
);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
'foam-vscode.placeholders',
|
||||
provider
|
||||
),
|
||||
...provider.commands,
|
||||
);
|
||||
```
|
||||
* @param {FoamWorkspace} workspace
|
||||
* @param {IDataStore} dataStore
|
||||
* @param {string} providerId A **unique** providerId, this will be used to generate necessary commands within the provider.
|
||||
* @param {string} resourceName A display name used in the explorer view
|
||||
* @param {(resource: Resource, index: number) => boolean} filterPredicate A filter function called on each Resource within the workspace
|
||||
* @param {GroupedResourcesConfig} config
|
||||
* @param {URI[]} workspaceUris The workspace URIs
|
||||
* @memberof GroupedResourcesTreeDataProvider
|
||||
*/
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private dataStore: IDataStore,
|
||||
private providerId: string,
|
||||
private resourceName: string,
|
||||
private filterPredicate: (resource: Resource, index: number) => boolean,
|
||||
config: GroupedResourcesConfig,
|
||||
workspaceUris: URI[]
|
||||
) {
|
||||
this.groupBy = config.groupBy;
|
||||
this.exclude = this.getGlobs(workspaceUris, config.exclude);
|
||||
this.setContext();
|
||||
this.computeResources();
|
||||
}
|
||||
|
||||
public get commands() {
|
||||
return [
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.group-${this.providerId}-by-folder`,
|
||||
() => this.setGroupBy(GroupedResoucesConfigGroupBy.Folder)
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.group-${this.providerId}-off`,
|
||||
() => this.setGroupBy(GroupedResoucesConfigGroupBy.Off)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
setGroupBy(groupBy: GroupedResoucesConfigGroupBy): void {
|
||||
this.groupBy = groupBy;
|
||||
this.setContext();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private setContext(): void {
|
||||
vscode.commands.executeCommand(
|
||||
'setContext',
|
||||
`foam-vscode.${this.providerId}-grouped-by-folder`,
|
||||
this.groupBy === GroupedResoucesConfigGroupBy.Folder
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.computeResources();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
getTreeItem(item: GroupedResourceTreeItem): vscode.TreeItem {
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(
|
||||
directory?: DirectoryTreeItem
|
||||
): Thenable<GroupedResourceTreeItem[]> {
|
||||
if (!directory && this.groupBy === GroupedResoucesConfigGroupBy.Folder) {
|
||||
const directories = Object.entries(this.getGroupedResourcesByDirectory())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(
|
||||
([dir, resources]) =>
|
||||
new DirectoryTreeItem(dir, resources, this.resourceName)
|
||||
);
|
||||
return Promise.resolve(directories);
|
||||
}
|
||||
|
||||
if (directory) {
|
||||
const resources = directory.resources.map(
|
||||
o => new ResourceTreeItem(o, this.dataStore)
|
||||
);
|
||||
return Promise.resolve(resources);
|
||||
}
|
||||
|
||||
const resources = this.resources.map(
|
||||
o => new ResourceTreeItem(o, this.dataStore)
|
||||
);
|
||||
return Promise.resolve(resources);
|
||||
}
|
||||
|
||||
resolveTreeItem(
|
||||
item: GroupedResourceTreeItem
|
||||
): Promise<GroupedResourceTreeItem> {
|
||||
return item.resolveTreeItem();
|
||||
}
|
||||
|
||||
private computeResources(): void {
|
||||
this.resources = this.workspace
|
||||
.list()
|
||||
.filter(this.filterPredicate)
|
||||
.filter(resource => !this.isMatch(resource.uri))
|
||||
.sort(this.sort);
|
||||
}
|
||||
|
||||
private isMatch(uri: URI) {
|
||||
return micromatch.isMatch(uri.fsPath, this.exclude);
|
||||
}
|
||||
|
||||
private getGlobs(fsURI: URI[], globs: string[]): string[] {
|
||||
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
||||
for (const fsPath of fsURI) {
|
||||
let folder = fsPath.path.replace(/\\/g, '/');
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
exclude.push(...globs.map(g => `${folder}/${g}`));
|
||||
}
|
||||
|
||||
return exclude;
|
||||
}
|
||||
|
||||
private getGroupedResourcesByDirectory(): ResourceByDirectory {
|
||||
const resourcesByDirectory: ResourceByDirectory = {};
|
||||
for (const resource of this.resources) {
|
||||
const p = resource.uri.path.replace(this.root, '');
|
||||
const { dir } = path.parse(p);
|
||||
|
||||
if (resourcesByDirectory[dir]) {
|
||||
resourcesByDirectory[dir].push(resource);
|
||||
} else {
|
||||
resourcesByDirectory[dir] = [resource];
|
||||
}
|
||||
}
|
||||
|
||||
for (const k in resourcesByDirectory) {
|
||||
resourcesByDirectory[k].sort(this.sort);
|
||||
}
|
||||
|
||||
return resourcesByDirectory;
|
||||
}
|
||||
|
||||
private sort(a: Resource, b: Resource) {
|
||||
const titleA = getTitle(a);
|
||||
const titleB = getTitle(b);
|
||||
return titleA.localeCompare(titleB);
|
||||
}
|
||||
}
|
||||
|
||||
type ResourceByDirectory = { [key: string]: Resource[] };
|
||||
|
||||
type GroupedResourceTreeItem = ResourceTreeItem | DirectoryTreeItem;
|
||||
|
||||
export class ResourceTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
private readonly dataStore: IDataStore,
|
||||
collapsibleState = vscode.TreeItemCollapsibleState.None
|
||||
) {
|
||||
super(getTitle(resource), collapsibleState);
|
||||
this.contextValue = 'resource';
|
||||
this.description = resource.uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(resource.uri)?.uri.path,
|
||||
''
|
||||
);
|
||||
this.tooltip = undefined;
|
||||
this.command = {
|
||||
command: OPEN_COMMAND.command,
|
||||
title: OPEN_COMMAND.title,
|
||||
arguments: [
|
||||
{
|
||||
resource: resource.uri,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let iconStr: string;
|
||||
switch (this.resource.type) {
|
||||
case 'attachment':
|
||||
iconStr = 'file-media';
|
||||
break;
|
||||
case 'placeholder':
|
||||
iconStr = 'new-file';
|
||||
break;
|
||||
case 'note':
|
||||
default:
|
||||
iconStr = 'note';
|
||||
break;
|
||||
}
|
||||
this.iconPath = new vscode.ThemeIcon(iconStr);
|
||||
}
|
||||
|
||||
async resolveTreeItem(): Promise<ResourceTreeItem> {
|
||||
if (this instanceof ResourceTreeItem) {
|
||||
const content = await this.dataStore?.read(this.resource.uri);
|
||||
this.tooltip = content
|
||||
? getNoteTooltip(content)
|
||||
: getTitle(this.resource);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectoryTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly dir: string,
|
||||
public readonly resources: Resource[],
|
||||
itemLabel: string
|
||||
) {
|
||||
super(dir || 'Not Created', vscode.TreeItemCollapsibleState.Collapsed);
|
||||
const s = this.resources.length > 1 ? 's' : '';
|
||||
this.description = `${this.resources.length} ${itemLabel}${s}`;
|
||||
const titles = this.resources.map(getTitle);
|
||||
this.tooltip = getContainsTooltip(titles);
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon('folder');
|
||||
contextValue = 'directory';
|
||||
|
||||
resolveTreeItem(): Promise<GroupedResourceTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
8
packages/foam-vscode/src/utils/vsc-utils.ts
Normal file
8
packages/foam-vscode/src/utils/vsc-utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Position, Range } from 'vscode';
|
||||
import { Position as FoamPosition, Range as FoamRange } from 'foam-core';
|
||||
|
||||
export const toVsCodePosition = (p: FoamPosition): Position =>
|
||||
new Position(p.line, p.character);
|
||||
|
||||
export const toVsCodeRange = (r: FoamRange): Range =>
|
||||
new Range(r.start.line, r.start.character, r.end.line, r.end.character);
|
||||
@@ -191,11 +191,14 @@ function initDataviz(channel) {
|
||||
const size = sizeScale(info.neighbors.length);
|
||||
const { fill, border } = getNodeColor(node.id, model);
|
||||
const fontSize = model.style.fontSize / globalScale;
|
||||
const nodeState = getNodeState(node.id, model);
|
||||
const textColor = fill.copy({
|
||||
opacity:
|
||||
getNodeState(node.id, model) === 'highlighted'
|
||||
nodeState === 'regular'
|
||||
? labelAlpha(globalScale)
|
||||
: nodeState === 'highlighted'
|
||||
? 1
|
||||
: labelAlpha(globalScale),
|
||||
: Math.min(labelAlpha(globalScale), fill.opacity),
|
||||
});
|
||||
const label = info.title;
|
||||
|
||||
@@ -248,7 +251,7 @@ function getNodeColor(nodeId, model) {
|
||||
case 'regular':
|
||||
return { fill: typeFill, border: typeFill };
|
||||
case 'lessened':
|
||||
const transparent = d3.rgb(typeFill).copy({ opacity: 0.5 });
|
||||
const transparent = d3.rgb(typeFill).copy({ opacity: 0.05 });
|
||||
return { fill: transparent, border: transparent };
|
||||
case 'highlighted':
|
||||
return {
|
||||
@@ -22,6 +22,6 @@
|
||||
4. open this file in a browser
|
||||
-->
|
||||
<!-- <script src="./test-data.js"></script> -->
|
||||
<script data-replace src="./graphs/default/graph.js"></script>
|
||||
<script data-replace src="./graph.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
packages/foam-vscode/static/preview/style.css
Normal file
8
packages/foam-vscode/static/preview/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.foam-placeholder-link {
|
||||
color: red;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.foam-note-link,
|
||||
.foam-attachment-link {
|
||||
}
|
||||
16
readme.md
16
readme.md
@@ -1,10 +1,11 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<img src="packages/foam-vscode/icon/FOAM_ICON_256.png" width="100" align="right"/>
|
||||
|
||||
# Foam
|
||||
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://foambubble.github.io/join-discord/g)
|
||||
@@ -49,8 +50,8 @@ Foam is licensed under the [MIT license](LICENSE).
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[wiki-links]: docs/wiki-links.md "Wiki Links"
|
||||
[Getting started]: docs/index.md "Getting started"
|
||||
[Graph Visualisation]: docs/graph-visualisation.md "Graph Visualisation"
|
||||
[Backlinking]: docs/backlinking.md "Backlinking"
|
||||
[Graph Visualisation]: docs/features/graph-visualisation.md "Graph Visualisation"
|
||||
[Backlinking]: docs/features/backlinking.md "Backlinking"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
## Contributors ✨
|
||||
@@ -144,6 +145,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"target": "ES2019",
|
||||
"paths": {
|
||||
"foam-core": ["./packages/foam-core/src"],
|
||||
"foam-cli": ["./packages/foam-cli/src"],
|
||||
"foam-vscode": ["./packages/foam-vscode/src"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user