mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 15:08:01 -05:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea03b86338 | ||
|
|
449c062566 | ||
|
|
880c2e3d3b | ||
|
|
17cb619480 | ||
|
|
6deae95d80 | ||
|
|
1c0ebb8af7 | ||
|
|
fe56823e76 | ||
|
|
6956e0779a | ||
|
|
c8f1f8e03a | ||
|
|
9589071760 | ||
|
|
6f65c10746 | ||
|
|
e04409e74f | ||
|
|
dba0a72d98 | ||
|
|
fd23b1d010 | ||
|
|
cfb946a5f2 | ||
|
|
32c3a484d6 | ||
|
|
4195797024 | ||
|
|
fa405f5f65 | ||
|
|
4fd573b9e4 | ||
|
|
f613e1b9e2 | ||
|
|
0ada7d8e2c | ||
|
|
8b39bcdf16 | ||
|
|
6073dc246d | ||
|
|
5b671d59a8 | ||
|
|
8abea48b5c | ||
|
|
2eeb2e156b | ||
|
|
dc76660a63 | ||
|
|
e8eeffa4ca | ||
|
|
7d4f5e1532 | ||
|
|
e7749cd52b | ||
|
|
c6a4eab744 | ||
|
|
c88bd6f2f0 | ||
|
|
304a803310 | ||
|
|
632c41ac5f | ||
|
|
ec636809d8 | ||
|
|
af43a31ae8 | ||
|
|
7235af70dd | ||
|
|
de84541692 | ||
|
|
84fab168ce | ||
|
|
4f116cfc88 | ||
|
|
fd71dbe557 | ||
|
|
df4bf5a5cb | ||
|
|
122db20695 | ||
|
|
3b40e26a83 | ||
|
|
bbe44ea21b | ||
|
|
59bb2eb38f | ||
|
|
97f87692b6 | ||
|
|
4f76a6b24a | ||
|
|
c822589733 |
@@ -788,6 +788,51 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "iam-yan",
|
||||
"name": "Yan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48427014?v=4",
|
||||
"profile": "https://github.com/iam-yan",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jimt",
|
||||
"name": "Jim Tittsler",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/180326?v=4",
|
||||
"profile": "https://WikiEducator.org/User:JimTittsler",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MalcolmMielle",
|
||||
"name": "Malcolm Mielle",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4457840?v=4",
|
||||
"profile": "http://malcolmmielle.wordpress.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "veesar",
|
||||
"name": "Veesar",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/74916913?v=4",
|
||||
"profile": "https://snippets.page/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bentongxyz",
|
||||
"name": "bentongxyz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/60358804?v=4",
|
||||
"profile": "https://github.com/bentongxyz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -5,14 +5,40 @@
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"env": { "node": true, "es6": true },
|
||||
"plugins": ["@typescript-eslint", "import", "jest"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/class-name-casing": "warn",
|
||||
"@typescript-eslint/semi": "warn",
|
||||
"curly": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"require-await": "warn"
|
||||
}
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": ["**/src/test/**", "**/src/**/*{test,spec}.ts"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/core-modules": ["vscode"],
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["**/core/common/**", "*.js"],
|
||||
"reportUnusedDisableDirectives": true
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ dist
|
||||
docs/_site
|
||||
docs/.sass-cache
|
||||
docs/.jekyll-metadata
|
||||
.test-workspace
|
||||
|
||||
38
.vscode/launch.json
vendored
38
.vscode/launch.json
vendored
@@ -6,15 +6,20 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "Debug Jest Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["workspace", "foam-vscode", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"args": ["--runInBand"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/foam-vscode/.test-workspace",
|
||||
"--disable-extensions",
|
||||
"--disable-workspace-trust",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode",
|
||||
"--extensionTestsPath=${workspaceFolder}/packages/foam-vscode/out/test/suite"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"name": "Run VSCode Extension",
|
||||
@@ -24,8 +29,25 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/packages/foam-vscode"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/packages/foam-vscode/out/**/*.js"],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/packages/foam-vscode/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"cwd": "${workspaceFolder}/packages/foam-vscode",
|
||||
"runtimeExecutable": "yarn",
|
||||
"args": [
|
||||
"jest",
|
||||
"--runInBand",
|
||||
"--watchAll=false"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -20,13 +20,13 @@
|
||||
"**/node_modules/**/*",
|
||||
"packages/**/*"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"jest.autoRun": "off",
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn jest",
|
||||
"gitdoc.enabled": false,
|
||||
"jest.autoEnable": false,
|
||||
"jest.runAllTestsFirst": false,
|
||||
"search.mode": "reuseEditor"
|
||||
}
|
||||
|
||||
27
docs/dev/releasing-foam.md
Normal file
27
docs/dev/releasing-foam.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Releasing Foam
|
||||
|
||||
1. Get to the latest code
|
||||
- `git checkout master && git fetch && git rebase`
|
||||
2. Sanity checks
|
||||
- `yarn reset`
|
||||
- `yarn test`
|
||||
3. Update change log
|
||||
- `./packages/foam-vscode/CHANGELOG.md`
|
||||
- `git add *`
|
||||
- `git commit -m"Preparation for next release"`
|
||||
4. Update version
|
||||
- `$ cd packages/foam-vscode`
|
||||
- `foam-vscode$ yarn lerna version <version>` (where `version` is `patch/minor/major`)
|
||||
- `cd ../..`
|
||||
5. Package extension
|
||||
- `$ yarn vscode:package-extension`
|
||||
6. Publish extension
|
||||
- `$ yarn vscode:publish-extension`
|
||||
7. Update the release notes in GitHub
|
||||
- in GitHub, top right, click on "releases"
|
||||
- select "tags" in top left
|
||||
- select the tag that was just released, click "edit" and copy release information from changelog
|
||||
- publish (no need to attach artifacts)
|
||||
8. Annouce on Discord
|
||||
|
||||
Steps 1 to 6 should really be replaced by a GitHub action...
|
||||
@@ -1,6 +1,6 @@
|
||||
# Backlinking
|
||||
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) **Backlinks Explorer**
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the **Backlinks Explorer**
|
||||
|
||||
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "backlinks" and run the **Explorer: Focus on Backlinks** view.
|
||||
- Keep this pane always visible to discover relationships between your thoughts
|
||||
|
||||
@@ -47,8 +47,6 @@ In addition, you can also use variables provided by Foam:
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
|
||||
|
||||
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables. See [#693](https://github.com/foambubble/foam/issues/693).
|
||||
|
||||
### `FOAM_DATE_*` variables
|
||||
|
||||
Foam defines its own set of datetime variables that have a similar behaviour as [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
|
||||
|
||||
@@ -218,6 +218,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="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Wikilinks can refer to any note or attachment in the repo: `[[note.md]]`, `[[doc
|
||||
|
||||
The usual wikilink syntax without extension refers to notes: `[[wikilink]]` and `[[wikilink.md]]` are equivalent.
|
||||
|
||||
The goal of wikilinks is to uniquily identify a file in a repo, no matter in which directory it lives.
|
||||
The goal of wikilinks is to uniquely identify a file in a repo, no matter in which directory it lives.
|
||||
|
||||
Sometimes in a repo you can have files with the same name in different directories.
|
||||
Foam allows you to identify those files using the minimum effort needed to disambiguate them.
|
||||
@@ -67,10 +67,6 @@ Basically we could say as a rule:
|
||||
|
||||
## Compatibility with other apps
|
||||
|
||||
Foam's identifiers are a super set of Obsidian's: all Obsidian links are supported by Foam, but Foam multi-part identifier (scenario 6) is only supported by Foam.
|
||||
|
||||
To improve compatibility this option should either be behind a configuration key, or it should be easily updated e.g. via the janitor.
|
||||
|
||||
| Scenario | Obsidian | Foam |
|
||||
| --------------------------- | ------------------------------- | ------------------------------- |
|
||||
| 1 `[[notes]]` | ✔ unique identifier in repo | ✔ unique identifier in repo |
|
||||
@@ -78,7 +74,7 @@ To improve compatibility this option should either be behind a configuration key
|
||||
| 3 `[[work/notes]]` | ✔ valid path from repo root | ✔ valid identifier in repo |
|
||||
| 4 `[[project/house/todo]]` | ✔ valid path from repo root | ✔ valid unique identifier |
|
||||
| 5 `[[/project/house/todo]]` | ✔ valid path from repo root | ✔ valid path from repo root |
|
||||
| 6 `[[house/todo]]` | ✘ incorrect path from repo root | ✔ valid unique identifier |
|
||||
| 6 `[[house/todo]]` | ✔ valid unique identifier | ✔ valid unique identifier |
|
||||
| 7 `[[todo]]` | ✘ ambiguous identifier | ✘ ambiguous identifier |
|
||||
| 8 `[[/house/todo]]` | ✘ incorrect path from repo root | ✘ incorrect path from repo root |
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# GitHub Pages
|
||||
|
||||
- In VSCode workspace settings set `"foam.edit.linkReferenceDefinitions": "withoutExtensions"`
|
||||
- Execute the “Foam: Run Janitor” command from the command palette.
|
||||
- [Turn **GitHub Pages** on in your repository settings](https://guides.github.com/features/pages/).
|
||||
- The default GitHub Pages template is called [Primer](https://github.com/pages-themes/primer). See Primer docs for how to customise html layouts and templates.
|
||||
- GitHub Pages is built on [Jekyll](https://jekyllrb.com/), so it supports things like permalinks, front matter metadata etc.
|
||||
1. In VSCode workspace settings set `"foam.edit.linkReferenceDefinitions": "withoutExtensions"`
|
||||
2. Execute the “Foam: Run Janitor” command from the command palette.
|
||||
3. [Turn **GitHub Pages** on in your repository settings](https://guides.github.com/features/pages/).
|
||||
- The default GitHub Pages template is called [Primer](https://github.com/pages-themes/primer). See Primer docs for how to customise html layouts and templates.
|
||||
- GitHub Pages is built on [Jekyll](https://jekyllrb.com/), so it supports things like permalinks, front matter metadata etc.
|
||||
|
||||
## How to publish locally
|
||||
|
||||
|
||||
@@ -2,13 +2,175 @@
|
||||
|
||||
You don't have to use GitHub to serve Foam pages. You can also use GitLab.
|
||||
|
||||
Gitlab pages can be kept private for private repo, so that your notes are still private.
|
||||
|
||||
## Setup a project
|
||||
|
||||
### Generate the directory from GitHub
|
||||
|
||||
Generate a solution using the [Foam template].
|
||||
Generate a solution using the [Foam template](https://github.com/foambubble/foam-template).
|
||||
|
||||
Change the remote to GitLab, or copy all the files into a new GitLab repo.
|
||||
Change the remote to GitLab, or copy all the files into a new GitLab repo
|
||||
|
||||
## Publishing pages with Gatsby
|
||||
|
||||
### Setup the Gatsby config
|
||||
|
||||
Add a .gatsby-config.js file where:
|
||||
|
||||
* `$REPO_NAME` correspond to the name of your gtlab repo.
|
||||
* `$USER_NAME` correspond to your gitlab username.
|
||||
|
||||
```js
|
||||
const path = require("path");
|
||||
const pathPrefix = `/$REPO_NAME`;
|
||||
|
||||
// Change me
|
||||
const siteMetadata = {
|
||||
title: "A title",
|
||||
shortName: "A short name",
|
||||
description: "",
|
||||
imageUrl: "/graph-visualisation.jpg",
|
||||
siteUrl: "https://$USER_NAME.gitlab.io",
|
||||
};
|
||||
module.exports = {
|
||||
siteMetadata,
|
||||
pathPrefix,
|
||||
flags: {
|
||||
DEV_SSR: true,
|
||||
},
|
||||
plugins: [
|
||||
`gatsby-plugin-sharp`,
|
||||
{
|
||||
resolve: "gatsby-theme-primer-wiki",
|
||||
options: {
|
||||
defaultColorMode: "night",
|
||||
icon: "./path_to/logo.png",
|
||||
sidebarComponents: ["tag", "category"],
|
||||
nav: [
|
||||
{
|
||||
title: "Github",
|
||||
url: "https://github.com/$USER_NAME/",
|
||||
},
|
||||
{
|
||||
title: "Gitlab",
|
||||
url: "https://gitlab.com/$USER_NAME/",
|
||||
},
|
||||
],
|
||||
editUrl:
|
||||
"https://gitlab.com/$USER_NAME/$REPO_NAME/tree/main/",
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: "gatsby-source-filesystem",
|
||||
options: {
|
||||
name: "content",
|
||||
path: `${__dirname}`,
|
||||
ignore: [`**/\.*/**/*`],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
resolve: "gatsby-plugin-manifest",
|
||||
options: {
|
||||
name: siteMetadata.title,
|
||||
short_name: siteMetadata.shortName,
|
||||
start_url: pathPrefix,
|
||||
background_color: `#f7f0eb`,
|
||||
display: `standalone`,
|
||||
icon: path.resolve(__dirname, "./path_to/logo.png"),
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-sitemap`,
|
||||
},
|
||||
{
|
||||
resolve: "gatsby-plugin-robots-txt",
|
||||
options: {
|
||||
host: siteMetadata.siteUrl,
|
||||
sitemap: `${siteMetadata.siteUrl}/sitemap/sitemap-index.xml`,
|
||||
policy: [{ userAgent: "*", allow: "/" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
And a `package.json` file containing:
|
||||
|
||||
```json
|
||||
{
|
||||
"private": true,
|
||||
"name": "wiki",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"develop": "gatsby develop -H 0.0.0.0",
|
||||
"start": "gatsby develop -H 0.0.0.0",
|
||||
"build": "gatsby build",
|
||||
"clean": "gatsby clean",
|
||||
"serve": "gatsby serve",
|
||||
"test": "echo test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primer/react": "^34.1.0",
|
||||
"@primer/css": "^17.5.0",
|
||||
"foam-cli": "^0.11.0",
|
||||
"gatsby": "^3.12.0",
|
||||
"gatsby-plugin-manifest": "^3.12.0",
|
||||
"gatsby-plugin-robots-txt": "^1.6.9",
|
||||
"gatsby-plugin-sitemap": "^5.4.0",
|
||||
"gatsby-source-filesystem": "^3.12.0",
|
||||
"gatsby-theme-primer-wiki": "^1.14.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The theme will be based on [gatsby-theme-primer-wiki](https://github.com/theowenyoung/gatsby-theme-primer-wiki).
|
||||
|
||||
To test the theme locally first run `yarn install` and then use `gatsby develop` to serve the website.
|
||||
See gatsby documentation for more details.
|
||||
|
||||
### Set-up the CI for deployment
|
||||
|
||||
Create a `.gitlab-ci.yml` file containing:
|
||||
|
||||
```yml
|
||||
# To contribute improvements to CI/CD templates, please follow the Development guide at:
|
||||
# https://docs.gitlab.com/ee/development/cicd/templates.html
|
||||
# This specific template is located at:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
|
||||
|
||||
image: node:latest
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
# This folder is cached between builds
|
||||
# https://docs.gitlab.com/ee/ci/yaml/index.html#cache
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
# Enables git-lab CI caching. Both .cache and public must be cached, otherwise builds will fail.
|
||||
- .cache/
|
||||
- public/
|
||||
script:
|
||||
- yarn install
|
||||
- ./node_modules/.bin/gatsby build --prefix-paths
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
```
|
||||
|
||||
This pipeline will now serve your website on every push to the main branch of your project.
|
||||
|
||||
## Publish with Jekyll
|
||||
|
||||
### Add a _config.yaml
|
||||
|
||||
@@ -47,14 +209,14 @@ gem "jekyll-optional-front-matter"
|
||||
|
||||
Commit the file and push it to gitlab.
|
||||
|
||||
## Setup CI/CD
|
||||
### Setup CI/CD
|
||||
|
||||
1. From the project home in GitLab click `Set up CI/CD`
|
||||
2. Choose `Jekyll` as your template from the template dropdown
|
||||
3. Click `commit`
|
||||
4. Now when you go to CI / CD > Pipelines, you should see the code running
|
||||
|
||||
## Troubleshooting
|
||||
### Troubleshooting
|
||||
|
||||
- *Could not locate Gemfile* - You didn't follow the steps above to [#Add a Gemlock file]
|
||||
- *Conversion error: Jekyll::Converters::Scss encountered an error while converting* You need to reference a theme.
|
||||
|
||||
@@ -10,6 +10,10 @@ Foam enables you to Link pages together using `[[file-name]]` annotations (i.e.
|
||||
|
||||
> If the `F12` shortcut feels unnatural you can rebind it at File > Preferences > Keyboard Shortcuts by searching for `editor.action.revealDefinition`.
|
||||
|
||||
## Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
|
||||
## Markdown compatibility
|
||||
|
||||
The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) extension automatically generates [[link-reference-definitions]] at the bottom of the file to make wikilinks compatible with Markdown tools and parsers.
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.16.0"
|
||||
"version": "0.17.4"
|
||||
}
|
||||
|
||||
0
packages/foam-vscode/.test-workspace/.keep
Normal file
0
packages/foam-vscode/.test-workspace/.keep
Normal file
@@ -4,6 +4,55 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [0.17.4] - 2022-02-13
|
||||
|
||||
Fixes and Improvements:
|
||||
- Improvements to Foam variables in templates (#882 - thanks @movermeyer)
|
||||
- Foam variables can now be used just any other VS Code variables, including in combination with placeholders and transformers
|
||||
|
||||
## [0.17.3] - 2022-01-14
|
||||
|
||||
Fixes and Improvements:
|
||||
- Fixed autocompletion with tags (#885 - thanks @memeplex)
|
||||
- Improved "Open Daily Note" to be usabled in tasks (#897 - thanks @MCluck90)
|
||||
|
||||
## [0.17.2] - 2021-12-22
|
||||
|
||||
Fixes and Improvements:
|
||||
- Improved support for wikilinks in titles (#878)
|
||||
- Use syntax injection for wikilinks (#876 - thanks @memeplex)
|
||||
- Fix when applying text edits in last line
|
||||
|
||||
Internal:
|
||||
- DX: Clean up of testing setup (#881 - thanks @memeplex)
|
||||
|
||||
## [0.17.1] - 2021-12-16
|
||||
|
||||
Fixes and Improvements:
|
||||
- Decorate markdown files only (#857)
|
||||
- Fix template placeholders issue (#859)
|
||||
- Improved replacement range for link completion
|
||||
|
||||
Internal:
|
||||
- Major URI/path handling refactoring (#858 - thanks @memeplex)
|
||||
|
||||
## [0.17.0] - 2021-12-08
|
||||
|
||||
Features:
|
||||
|
||||
- Added first class support for sections (#856)
|
||||
- Sections can be referred to in wikilinks
|
||||
- Sections can be embedded
|
||||
- Autocompletion for sections
|
||||
- Diagnostic for sections
|
||||
- Embed sections
|
||||
|
||||
## [0.16.1] - 2021-11-30
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed diagnostic bug triggered when file had same suffix (#851)
|
||||
|
||||
## [0.16.0] - 2021-11-24
|
||||
|
||||
Features:
|
||||
|
||||
@@ -58,6 +58,11 @@ Embed the content from other notes.
|
||||
|
||||

|
||||
|
||||
### Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections.
|
||||
Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
|
||||
### Link Alias
|
||||
|
||||
Foam supports link aliasing, so you can have a `[[wikilink]]`, or a `[[wikilink|alias]]`.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
plugins: [["@babel/plugin-transform-runtime", { helpers: false }]]
|
||||
};
|
||||
@@ -82,7 +82,7 @@ module.exports = {
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
modulePathIgnorePatterns: ['.vscode-test'],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
@@ -91,7 +91,7 @@ module.exports = {
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
preset: 'ts-jest',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
@@ -126,13 +126,13 @@ module.exports = {
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node"
|
||||
testEnvironment: 'node',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
@@ -152,7 +152,10 @@ module.exports = {
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
// This is overridden in every runCLI invocation but it's here as the default
|
||||
// for vscode-jest. We only want unit tests in the test explorer (sidebar),
|
||||
// since spec tests require the entire extension host to be launched before.
|
||||
testRegex: ['\\.test\\.ts$'],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.4",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -37,6 +37,26 @@
|
||||
"markdown.previewStyles": [
|
||||
"./static/preview/style.css"
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"path": "./syntaxes/injection.json",
|
||||
"scopeName": "foam.wikilink.injection",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
]
|
||||
}
|
||||
],
|
||||
"colors": [
|
||||
{
|
||||
"id": "foam.placeholder",
|
||||
"description": "Color of foam placeholders.",
|
||||
"defaults": {
|
||||
"dark": "editorWarning.foreground",
|
||||
"light": "editorWarning.foreground",
|
||||
"highContrast": "editorWarning.foreground"
|
||||
}
|
||||
}
|
||||
],
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
@@ -356,7 +376,9 @@
|
||||
"build": "tsc -p ./",
|
||||
"pretest": "yarn build",
|
||||
"test": "node ./out/test/run-tests.js",
|
||||
"pretest:unit": "yarn build",
|
||||
"test:unit": "node ./out/test/run-tests.js --unit",
|
||||
"pretest:e2e": "yarn build",
|
||||
"test:e2e": "node ./out/test/run-tests.js --e2e",
|
||||
"lint": "tsdx lint src",
|
||||
"clean": "rimraf out",
|
||||
@@ -372,12 +394,7 @@
|
||||
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx && yarn npm-cleanup"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
"@babel/plugin-transform-runtime": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
@@ -388,12 +405,12 @@
|
||||
"@types/vscode": "^1.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"babel-jest": "^26.2.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.2.2",
|
||||
"jest-environment-vscode": "^1.0.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"markdown-it": "^12.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -407,7 +424,6 @@
|
||||
"dateformat": "^3.0.3",
|
||||
"detect-newline": "^3.1.0",
|
||||
"fast-array-diff": "^1.0.1",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
811
packages/foam-vscode/src/core/common/snippetParser.test.ts
Normal file
811
packages/foam-vscode/src/core/common/snippetParser.test.ts
Normal file
@@ -0,0 +1,811 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Originally taken from https://github.com/microsoft/vscode/blob/d31496c866683bdbccfc85bc11a3107d6c789b52/src/vs/editor/contrib/snippet/test/snippetParser.test.ts
|
||||
* Here was the license:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2015 - present Microsoft Corporation
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from './snippetParser';
|
||||
|
||||
describe('SnippetParser', () => {
|
||||
|
||||
test('Scanner', () => {
|
||||
|
||||
const scanner = new Scanner();
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('abc');
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('{{abc}}');
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('abc() ');
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Format);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('abc 123');
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Format);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Int);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('$foo');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('$foo_bar');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('$foo-bar');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dash);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('${foo}');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('${1223:foo}');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Int);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Colon);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
|
||||
scanner.text('\\${}');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Backslash);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
|
||||
scanner.text('${foo/regex/format/option}');
|
||||
assert.strictEqual(scanner.next().type, TokenType.Dollar);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
|
||||
assert.strictEqual(scanner.next().type, TokenType.VariableName);
|
||||
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
|
||||
assert.strictEqual(scanner.next().type, TokenType.EOF);
|
||||
});
|
||||
|
||||
function assertText(value: string, expected: string) {
|
||||
const p = new SnippetParser();
|
||||
const actual = p.text(value);
|
||||
assert.strictEqual(actual, expected);
|
||||
}
|
||||
|
||||
function assertMarker(input: TextmateSnippet | Marker[] | string, ...ctors: Function[]) {
|
||||
let marker: Marker[];
|
||||
if (input instanceof TextmateSnippet) {
|
||||
marker = input.children;
|
||||
} else if (typeof input === 'string') {
|
||||
const p = new SnippetParser();
|
||||
marker = p.parse(input).children;
|
||||
} else {
|
||||
marker = input;
|
||||
}
|
||||
while (marker.length > 0) {
|
||||
let m = marker.pop();
|
||||
let ctor = ctors.pop()!;
|
||||
assert.ok(m instanceof ctor);
|
||||
}
|
||||
assert.strictEqual(marker.length, ctors.length);
|
||||
assert.strictEqual(marker.length, 0);
|
||||
}
|
||||
|
||||
function assertTextAndMarker(value: string, escaped: string, ...ctors: Function[]) {
|
||||
assertText(value, escaped);
|
||||
assertMarker(value, ...ctors);
|
||||
}
|
||||
|
||||
function assertEscaped(value: string, expected: string) {
|
||||
const actual = SnippetParser.escape(value);
|
||||
assert.strictEqual(actual, expected);
|
||||
}
|
||||
|
||||
test('Parser, escaped', function () {
|
||||
assertEscaped('foo$0', 'foo\\$0');
|
||||
assertEscaped('foo\\$0', 'foo\\\\\\$0');
|
||||
assertEscaped('f$1oo$0', 'f\\$1oo\\$0');
|
||||
assertEscaped('${1:foo}$0', '\\${1:foo\\}\\$0');
|
||||
assertEscaped('$', '\\$');
|
||||
});
|
||||
|
||||
test('Parser, text', () => {
|
||||
assertText('$', '$');
|
||||
assertText('\\\\$', '\\$');
|
||||
assertText('{', '{');
|
||||
assertText('\\}', '}');
|
||||
assertText('\\abc', '\\abc');
|
||||
assertText('foo${f:\\}}bar', 'foo}bar');
|
||||
assertText('\\{', '\\{');
|
||||
assertText('I need \\\\\\$', 'I need \\$');
|
||||
assertText('\\', '\\');
|
||||
assertText('\\{{', '\\{{');
|
||||
assertText('{{', '{{');
|
||||
assertText('{{dd', '{{dd');
|
||||
assertText('}}', '}}');
|
||||
assertText('ff}}', 'ff}}');
|
||||
|
||||
assertText('farboo', 'farboo');
|
||||
assertText('far{{}}boo', 'far{{}}boo');
|
||||
assertText('far{{123}}boo', 'far{{123}}boo');
|
||||
assertText('far\\{{123}}boo', 'far\\{{123}}boo');
|
||||
assertText('far{{id:bern}}boo', 'far{{id:bern}}boo');
|
||||
assertText('far{{id:bern {{basel}}}}boo', 'far{{id:bern {{basel}}}}boo');
|
||||
assertText('far{{id:bern {{id:basel}}}}boo', 'far{{id:bern {{id:basel}}}}boo');
|
||||
assertText('far{{id:bern {{id2:basel}}}}boo', 'far{{id:bern {{id2:basel}}}}boo');
|
||||
});
|
||||
|
||||
|
||||
test('Parser, TM text', () => {
|
||||
assertTextAndMarker('foo${1:bar}}', 'foobar}', Text, Placeholder, Text);
|
||||
assertTextAndMarker('foo${1:bar}${2:foo}}', 'foobarfoo}', Text, Placeholder, Placeholder, Text);
|
||||
|
||||
assertTextAndMarker('foo${1:bar\\}${2:foo}}', 'foobar}foo', Text, Placeholder);
|
||||
|
||||
let [, placeholder] = new SnippetParser().parse('foo${1:bar\\}${2:foo}}').children;
|
||||
let { children } = (<Placeholder>placeholder);
|
||||
|
||||
assert.strictEqual((<Placeholder>placeholder).index, 1);
|
||||
assert.ok(children[0] instanceof Text);
|
||||
assert.strictEqual(children[0].toString(), 'bar}');
|
||||
assert.ok(children[1] instanceof Placeholder);
|
||||
assert.strictEqual(children[1].toString(), 'foo');
|
||||
});
|
||||
|
||||
test('Parser, placeholder', () => {
|
||||
assertTextAndMarker('farboo', 'farboo', Text);
|
||||
assertTextAndMarker('far{{}}boo', 'far{{}}boo', Text);
|
||||
assertTextAndMarker('far{{123}}boo', 'far{{123}}boo', Text);
|
||||
assertTextAndMarker('far\\{{123}}boo', 'far\\{{123}}boo', Text);
|
||||
});
|
||||
|
||||
test('Parser, literal code', () => {
|
||||
assertTextAndMarker('far`123`boo', 'far`123`boo', Text);
|
||||
assertTextAndMarker('far\\`123\\`boo', 'far\\`123\\`boo', Text);
|
||||
});
|
||||
|
||||
test('Parser, variables/tabstop', () => {
|
||||
assertTextAndMarker('$far-boo', '-boo', Variable, Text);
|
||||
assertTextAndMarker('\\$far-boo', '$far-boo', Text);
|
||||
assertTextAndMarker('far$farboo', 'far', Text, Variable);
|
||||
assertTextAndMarker('far${farboo}', 'far', Text, Variable);
|
||||
assertTextAndMarker('$123', '', Placeholder);
|
||||
assertTextAndMarker('$farboo', '', Variable);
|
||||
assertTextAndMarker('$far12boo', '', Variable);
|
||||
assertTextAndMarker('000_${far}_000', '000__000', Text, Variable, Text);
|
||||
assertTextAndMarker('FFF_${TM_SELECTED_TEXT}_FFF$0', 'FFF__FFF', Text, Variable, Text, Placeholder);
|
||||
});
|
||||
|
||||
test('Parser, variables/placeholder with defaults', () => {
|
||||
assertTextAndMarker('${name:value}', 'value', Variable);
|
||||
assertTextAndMarker('${1:value}', 'value', Placeholder);
|
||||
assertTextAndMarker('${1:bar${2:foo}bar}', 'barfoobar', Placeholder);
|
||||
|
||||
assertTextAndMarker('${name:value', '${name:value', Text);
|
||||
assertTextAndMarker('${1:bar${2:foobar}', '${1:barfoobar', Text, Placeholder);
|
||||
});
|
||||
|
||||
test('Parser, variable transforms', function () {
|
||||
assertTextAndMarker('${foo///}', '', Variable);
|
||||
assertTextAndMarker('${foo/regex/format/gmi}', '', Variable);
|
||||
assertTextAndMarker('${foo/([A-Z][a-z])/format/}', '', Variable);
|
||||
|
||||
// invalid regex
|
||||
assertTextAndMarker('${foo/([A-Z][a-z])/format/GMI}', '${foo/([A-Z][a-z])/format/GMI}', Text);
|
||||
assertTextAndMarker('${foo/([A-Z][a-z])/format/funky}', '${foo/([A-Z][a-z])/format/funky}', Text);
|
||||
assertTextAndMarker('${foo/([A-Z][a-z]/format/}', '${foo/([A-Z][a-z]/format/}', Text);
|
||||
|
||||
// tricky regex
|
||||
assertTextAndMarker('${foo/m\\/atch/$1/i}', '', Variable);
|
||||
assertMarker('${foo/regex\/format/options}', Text);
|
||||
|
||||
// incomplete
|
||||
assertTextAndMarker('${foo///', '${foo///', Text);
|
||||
assertTextAndMarker('${foo/regex/format/options', '${foo/regex/format/options', Text);
|
||||
|
||||
// format string
|
||||
assertMarker('${foo/.*/${0:fooo}/i}', Variable);
|
||||
assertMarker('${foo/.*/${1}/i}', Variable);
|
||||
assertMarker('${foo/.*/$1/i}', Variable);
|
||||
assertMarker('${foo/.*/This-$1-encloses/i}', Variable);
|
||||
assertMarker('${foo/.*/complex${1:else}/i}', Variable);
|
||||
assertMarker('${foo/.*/complex${1:-else}/i}', Variable);
|
||||
assertMarker('${foo/.*/complex${1:+if}/i}', Variable);
|
||||
assertMarker('${foo/.*/complex${1:?if:else}/i}', Variable);
|
||||
assertMarker('${foo/.*/complex${1:/upcase}/i}', Variable);
|
||||
|
||||
});
|
||||
|
||||
test('Parser, placeholder transforms', function () {
|
||||
assertTextAndMarker('${1///}', '', Placeholder);
|
||||
assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder);
|
||||
assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder);
|
||||
|
||||
// tricky regex
|
||||
assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder);
|
||||
assertMarker('${1/regex\/format/options}', Text);
|
||||
|
||||
// incomplete
|
||||
assertTextAndMarker('${1///', '${1///', Text);
|
||||
assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text);
|
||||
});
|
||||
|
||||
test('No way to escape forward slash in snippet regex #36715', function () {
|
||||
assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable);
|
||||
});
|
||||
|
||||
test('No way to escape forward slash in snippet format section #37562', function () {
|
||||
assertMarker('${TM_SELECTED_TEXT/a/\\/$1/g}', Variable);
|
||||
assertMarker('${TM_SELECTED_TEXT/a/in\\/$1ner/g}', Variable);
|
||||
assertMarker('${TM_SELECTED_TEXT/a/end\\//g}', Variable);
|
||||
});
|
||||
|
||||
test('Parser, placeholder with choice', () => {
|
||||
|
||||
assertTextAndMarker('${1|one,two,three|}', 'one', Placeholder);
|
||||
assertTextAndMarker('${1|one|}', 'one', Placeholder);
|
||||
assertTextAndMarker('${1|one1,two2|}', 'one1', Placeholder);
|
||||
assertTextAndMarker('${1|one1\\,two2|}', 'one1,two2', Placeholder);
|
||||
assertTextAndMarker('${1|one1\\|two2|}', 'one1|two2', Placeholder);
|
||||
assertTextAndMarker('${1|one1\\atwo2|}', 'one1\\atwo2', Placeholder);
|
||||
assertTextAndMarker('${1|one,two,three,|}', '${1|one,two,three,|}', Text);
|
||||
assertTextAndMarker('${1|one,', '${1|one,', Text);
|
||||
|
||||
const p = new SnippetParser();
|
||||
const snippet = p.parse('${1|one,two,three|}');
|
||||
assertMarker(snippet, Placeholder);
|
||||
const expected = [Placeholder, Text, Text, Text];
|
||||
snippet.walk(marker => {
|
||||
assert.strictEqual(marker, expected.shift());
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('Snippet choices: unable to escape comma and pipe, #31521', function () {
|
||||
assertTextAndMarker('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(not, not);', Text, Placeholder, Text);
|
||||
});
|
||||
|
||||
test('Marker, toTextmateString()', function () {
|
||||
|
||||
function assertTextsnippetString(input: string, expected: string): void {
|
||||
const snippet = new SnippetParser().parse(input);
|
||||
const actual = snippet.toTextmateString();
|
||||
assert.strictEqual(actual, expected);
|
||||
}
|
||||
|
||||
assertTextsnippetString('$1', '$1');
|
||||
assertTextsnippetString('\\$1', '\\$1');
|
||||
assertTextsnippetString('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});');
|
||||
assertTextsnippetString('console.log(${1|not\\, not, \\| five, 5, 1 23|});', 'console.log(${1|not\\, not, \\| five, 5, 1 23|});');
|
||||
assertTextsnippetString('this is text', 'this is text');
|
||||
assertTextsnippetString('this ${1:is ${2:nested with $var}}', 'this ${1:is ${2:nested with ${var}}}');
|
||||
assertTextsnippetString('this ${1:is ${2:nested with $var}}}', 'this ${1:is ${2:nested with ${var}}}\\}');
|
||||
});
|
||||
|
||||
test('Marker, toTextmateString() <-> identity', function () {
|
||||
|
||||
function assertIdent(input: string): void {
|
||||
// full loop: (1) parse input, (2) generate textmate string, (3) parse, (4) ensure both trees are equal
|
||||
const snippet = new SnippetParser().parse(input);
|
||||
const input2 = snippet.toTextmateString();
|
||||
const snippet2 = new SnippetParser().parse(input2);
|
||||
|
||||
function checkCheckChildren(marker1: Marker, marker2: Marker) {
|
||||
assert.ok(marker1 instanceof Object.getPrototypeOf(marker2).constructor);
|
||||
assert.ok(marker2 instanceof Object.getPrototypeOf(marker1).constructor);
|
||||
|
||||
assert.strictEqual(marker1.children.length, marker2.children.length);
|
||||
assert.strictEqual(marker1.toString(), marker2.toString());
|
||||
|
||||
for (let i = 0; i < marker1.children.length; i++) {
|
||||
checkCheckChildren(marker1.children[i], marker2.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
checkCheckChildren(snippet, snippet2);
|
||||
}
|
||||
|
||||
assertIdent('$1');
|
||||
assertIdent('\\$1');
|
||||
assertIdent('console.log(${1|not\\, not, five, 5, 1 23|});');
|
||||
assertIdent('console.log(${1|not\\, not, \\| five, 5, 1 23|});');
|
||||
assertIdent('this is text');
|
||||
assertIdent('this ${1:is ${2:nested with $var}}');
|
||||
assertIdent('this ${1:is ${2:nested with $var}}}');
|
||||
assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');
|
||||
});
|
||||
|
||||
test('Parser, choise marker', () => {
|
||||
const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');
|
||||
|
||||
assert.strictEqual(placeholders.length, 1);
|
||||
assert.ok(placeholders[0].choice instanceof Choice);
|
||||
assert.ok(placeholders[0].children[0] instanceof Choice);
|
||||
assert.strictEqual((<Choice>placeholders[0].children[0]).options.length, 3);
|
||||
|
||||
assertText('${1|one,two,three|}', 'one');
|
||||
assertText('\\${1|one,two,three|}', '${1|one,two,three|}');
|
||||
assertText('${1\\|one,two,three|}', '${1\\|one,two,three|}');
|
||||
assertText('${1||}', '${1||}');
|
||||
});
|
||||
|
||||
test('Backslash character escape in choice tabstop doesn\'t work #58494', function () {
|
||||
|
||||
const { placeholders } = new SnippetParser().parse('${1|\\,,},$,\\|,\\\\|}');
|
||||
assert.strictEqual(placeholders.length, 1);
|
||||
assert.ok(placeholders[0].choice instanceof Choice);
|
||||
});
|
||||
|
||||
test('Parser, only textmate', () => {
|
||||
const p = new SnippetParser();
|
||||
assertMarker(p.parse('far{{}}boo'), Text);
|
||||
assertMarker(p.parse('far{{123}}boo'), Text);
|
||||
assertMarker(p.parse('far\\{{123}}boo'), Text);
|
||||
|
||||
assertMarker(p.parse('far$0boo'), Text, Placeholder, Text);
|
||||
assertMarker(p.parse('far${123}boo'), Text, Placeholder, Text);
|
||||
assertMarker(p.parse('far\\${123}boo'), Text);
|
||||
});
|
||||
|
||||
test('Parser, real world', () => {
|
||||
let marker = new SnippetParser().parse('console.warn(${1: $TM_SELECTED_TEXT })').children;
|
||||
|
||||
assert.strictEqual(marker[0].toString(), 'console.warn(');
|
||||
assert.ok(marker[1] instanceof Placeholder);
|
||||
assert.strictEqual(marker[2].toString(), ')');
|
||||
|
||||
const placeholder = <Placeholder>marker[1];
|
||||
assert.strictEqual(placeholder.index, 1);
|
||||
assert.strictEqual(placeholder.children.length, 3);
|
||||
assert.ok(placeholder.children[0] instanceof Text);
|
||||
assert.ok(placeholder.children[1] instanceof Variable);
|
||||
assert.ok(placeholder.children[2] instanceof Text);
|
||||
assert.strictEqual(placeholder.children[0].toString(), ' ');
|
||||
assert.strictEqual(placeholder.children[1].toString(), '');
|
||||
assert.strictEqual(placeholder.children[2].toString(), ' ');
|
||||
|
||||
const nestedVariable = <Variable>placeholder.children[1];
|
||||
assert.strictEqual(nestedVariable.name, 'TM_SELECTED_TEXT');
|
||||
assert.strictEqual(nestedVariable.children.length, 0);
|
||||
|
||||
marker = new SnippetParser().parse('$TM_SELECTED_TEXT').children;
|
||||
assert.strictEqual(marker.length, 1);
|
||||
assert.ok(marker[0] instanceof Variable);
|
||||
});
|
||||
|
||||
test('Parser, transform example', () => {
|
||||
let { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0');
|
||||
|
||||
//${1:name}
|
||||
assert.ok(children[0] instanceof Placeholder);
|
||||
assert.strictEqual(children[0].children.length, 1);
|
||||
assert.strictEqual(children[0].children[0].toString(), 'name');
|
||||
assert.strictEqual((<Placeholder>children[0]).transform, undefined);
|
||||
|
||||
// :
|
||||
assert.ok(children[1] instanceof Text);
|
||||
assert.strictEqual(children[1].toString(), ' : ');
|
||||
|
||||
//${2:type}
|
||||
assert.ok(children[2] instanceof Placeholder);
|
||||
assert.strictEqual(children[2].children.length, 1);
|
||||
assert.strictEqual(children[2].children[0].toString(), 'type');
|
||||
|
||||
//${3/\\s:=(.*)/${1:+ :=}${1}/}
|
||||
assert.ok(children[3] instanceof Placeholder);
|
||||
assert.strictEqual(children[3].children.length, 0);
|
||||
assert.notStrictEqual((<Placeholder>children[3]).transform, undefined);
|
||||
let transform = (<Placeholder>children[3]).transform!;
|
||||
assert.deepStrictEqual(transform.regexp, /\s:=(.*)/);
|
||||
assert.strictEqual(transform.children.length, 2);
|
||||
assert.ok(transform.children[0] instanceof FormatString);
|
||||
assert.strictEqual((<FormatString>transform.children[0]).index, 1);
|
||||
assert.strictEqual((<FormatString>transform.children[0]).ifValue, ' :=');
|
||||
assert.ok(transform.children[1] instanceof FormatString);
|
||||
assert.strictEqual((<FormatString>transform.children[1]).index, 1);
|
||||
assert.ok(children[4] instanceof Text);
|
||||
assert.strictEqual(children[4].toString(), ';\n');
|
||||
|
||||
});
|
||||
|
||||
// TODO @jrieken making this strictEqul causes circular json conversion errors
|
||||
test('Parser, default placeholder values', () => {
|
||||
|
||||
assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder);
|
||||
|
||||
const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err}`, error:$1').children;
|
||||
|
||||
assert.strictEqual((<Placeholder>p1).index, 1);
|
||||
assert.strictEqual((<Placeholder>p1).children.length, 1);
|
||||
assert.strictEqual((<Text>(<Placeholder>p1).children[0]).toString(), 'err');
|
||||
|
||||
assert.strictEqual((<Placeholder>p2).index, 1);
|
||||
assert.strictEqual((<Placeholder>p2).children.length, 1);
|
||||
assert.strictEqual((<Text>(<Placeholder>p2).children[0]).toString(), 'err');
|
||||
});
|
||||
|
||||
// TODO @jrieken making this strictEqul causes circular json conversion errors
|
||||
test('Parser, default placeholder values and one transform', () => {
|
||||
|
||||
assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder);
|
||||
|
||||
const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children;
|
||||
|
||||
assert.strictEqual((<Placeholder>p3).index, 1);
|
||||
assert.strictEqual((<Placeholder>p3).children.length, 1);
|
||||
assert.strictEqual((<Text>(<Placeholder>p3).children[0]).toString(), 'err');
|
||||
assert.strictEqual((<Placeholder>p3).transform, undefined);
|
||||
|
||||
assert.strictEqual((<Placeholder>p4).index, 1);
|
||||
assert.strictEqual((<Placeholder>p4).children.length, 1);
|
||||
assert.strictEqual((<Text>(<Placeholder>p4).children[0]).toString(), 'err');
|
||||
assert.notStrictEqual((<Placeholder>p4).transform, undefined);
|
||||
});
|
||||
|
||||
test('Repeated snippet placeholder should always inherit, #31040', function () {
|
||||
assertText('${1:foo}-abc-$1', 'foo-abc-foo');
|
||||
assertText('${1:foo}-abc-${1}', 'foo-abc-foo');
|
||||
assertText('${1:foo}-abc-${1:bar}', 'foo-abc-foo');
|
||||
assertText('${1}-abc-${1:foo}', 'foo-abc-foo');
|
||||
});
|
||||
|
||||
test('backspace esapce in TM only, #16212', () => {
|
||||
const actual = new SnippetParser().text('Foo \\\\${abc}bar');
|
||||
assert.strictEqual(actual, 'Foo \\bar');
|
||||
});
|
||||
|
||||
test('colon as variable/placeholder value, #16717', () => {
|
||||
let actual = new SnippetParser().text('${TM_SELECTED_TEXT:foo:bar}');
|
||||
assert.strictEqual(actual, 'foo:bar');
|
||||
|
||||
actual = new SnippetParser().text('${1:foo:bar}');
|
||||
assert.strictEqual(actual, 'foo:bar');
|
||||
});
|
||||
|
||||
test('incomplete placeholder', () => {
|
||||
assertTextAndMarker('${1:}', '', Placeholder);
|
||||
});
|
||||
|
||||
test('marker#len', () => {
|
||||
|
||||
function assertLen(template: string, ...lengths: number[]): void {
|
||||
const snippet = new SnippetParser().parse(template, true);
|
||||
snippet.walk(m => {
|
||||
const expected = lengths.shift();
|
||||
assert.strictEqual(m.len(), expected);
|
||||
return true;
|
||||
});
|
||||
assert.strictEqual(lengths.length, 0);
|
||||
}
|
||||
|
||||
assertLen('text$0', 4, 0);
|
||||
assertLen('$1text$0', 0, 4, 0);
|
||||
assertLen('te$1xt$0', 2, 0, 2, 0);
|
||||
assertLen('errorContext: `${1:err}`, error: $0', 15, 0, 3, 10, 0);
|
||||
assertLen('errorContext: `${1:err}`, error: $1$0', 15, 0, 3, 10, 0, 3, 0);
|
||||
assertLen('$TM_SELECTED_TEXT$0', 0, 0);
|
||||
assertLen('${TM_SELECTED_TEXT:def}$0', 0, 3, 0);
|
||||
});
|
||||
|
||||
test('parser, parent node', function () {
|
||||
let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
|
||||
|
||||
assert.strictEqual(snippet.placeholders.length, 3);
|
||||
let [first, second] = snippet.placeholders;
|
||||
assert.strictEqual(first.index, 1);
|
||||
assert.strictEqual(second.index, 2);
|
||||
assert.ok(second.parent === first);
|
||||
assert.ok(first.parent === snippet);
|
||||
|
||||
snippet = new SnippetParser().parse('${VAR:default${1:value}}$0', true);
|
||||
assert.strictEqual(snippet.placeholders.length, 2);
|
||||
[first] = snippet.placeholders;
|
||||
assert.strictEqual(first.index, 1);
|
||||
|
||||
assert.ok(snippet.children[0] instanceof Variable);
|
||||
assert.ok(first.parent === snippet.children[0]);
|
||||
});
|
||||
|
||||
test('TextmateSnippet#enclosingPlaceholders', () => {
|
||||
let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
|
||||
let [first, second] = snippet.placeholders;
|
||||
|
||||
assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []);
|
||||
assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]);
|
||||
});
|
||||
|
||||
test('TextmateSnippet#offset', () => {
|
||||
let snippet = new SnippetParser().parse('te$1xt', true);
|
||||
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
|
||||
assert.strictEqual(snippet.offset(snippet.children[1]), 2);
|
||||
assert.strictEqual(snippet.offset(snippet.children[2]), 2);
|
||||
|
||||
snippet = new SnippetParser().parse('${TM_SELECTED_TEXT:def}', true);
|
||||
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
|
||||
assert.strictEqual(snippet.offset((<Variable>snippet.children[0]).children[0]), 0);
|
||||
|
||||
// forgein marker
|
||||
assert.strictEqual(snippet.offset(new Text('foo')), -1);
|
||||
});
|
||||
|
||||
test('TextmateSnippet#placeholder', () => {
|
||||
let snippet = new SnippetParser().parse('te$1xt$0', true);
|
||||
let placeholders = snippet.placeholders;
|
||||
assert.strictEqual(placeholders.length, 2);
|
||||
|
||||
snippet = new SnippetParser().parse('te$1xt$1$0', true);
|
||||
placeholders = snippet.placeholders;
|
||||
assert.strictEqual(placeholders.length, 3);
|
||||
|
||||
|
||||
snippet = new SnippetParser().parse('te$1xt$2$0', true);
|
||||
placeholders = snippet.placeholders;
|
||||
assert.strictEqual(placeholders.length, 3);
|
||||
|
||||
snippet = new SnippetParser().parse('${1:bar${2:foo}bar}$0', true);
|
||||
placeholders = snippet.placeholders;
|
||||
assert.strictEqual(placeholders.length, 3);
|
||||
});
|
||||
|
||||
test('TextmateSnippet#replace 1/2', function () {
|
||||
let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
|
||||
|
||||
assert.strictEqual(snippet.placeholders.length, 3);
|
||||
const [, second] = snippet.placeholders;
|
||||
assert.strictEqual(second.index, 2);
|
||||
|
||||
const enclosing = snippet.enclosingPlaceholders(second);
|
||||
assert.strictEqual(enclosing.length, 1);
|
||||
assert.strictEqual(enclosing[0].index, 1);
|
||||
|
||||
let nested = new SnippetParser().parse('ddd$1eee$0', true);
|
||||
snippet.replace(second, nested.children);
|
||||
|
||||
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
|
||||
assert.strictEqual(snippet.placeholders.length, 4);
|
||||
assert.strictEqual(snippet.placeholders[0].index, 1);
|
||||
assert.strictEqual(snippet.placeholders[1].index, 1);
|
||||
assert.strictEqual(snippet.placeholders[2].index, 0);
|
||||
assert.strictEqual(snippet.placeholders[3].index, 0);
|
||||
|
||||
const newEnclosing = snippet.enclosingPlaceholders(snippet.placeholders[1]);
|
||||
assert.ok(newEnclosing[0] === snippet.placeholders[0]);
|
||||
assert.strictEqual(newEnclosing.length, 1);
|
||||
assert.strictEqual(newEnclosing[0].index, 1);
|
||||
});
|
||||
|
||||
test('TextmateSnippet#replace 2/2', function () {
|
||||
let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
|
||||
|
||||
assert.strictEqual(snippet.placeholders.length, 3);
|
||||
const [, second] = snippet.placeholders;
|
||||
assert.strictEqual(second.index, 2);
|
||||
|
||||
let nested = new SnippetParser().parse('dddeee$0', true);
|
||||
snippet.replace(second, nested.children);
|
||||
|
||||
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
|
||||
assert.strictEqual(snippet.placeholders.length, 3);
|
||||
});
|
||||
|
||||
test('Snippet order for placeholders, #28185', function () {
|
||||
|
||||
const _10 = new Placeholder(10);
|
||||
const _2 = new Placeholder(2);
|
||||
|
||||
assert.strictEqual(Placeholder.compareByIndex(_10, _2), 1);
|
||||
});
|
||||
|
||||
test('Maximum call stack size exceeded, #28983', function () {
|
||||
new SnippetParser().parse('${1:${foo:${1}}}');
|
||||
});
|
||||
|
||||
test('Snippet can freeze the editor, #30407', function () {
|
||||
|
||||
const seen = new Set<Marker>();
|
||||
|
||||
seen.clear();
|
||||
new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend').walk(marker => {
|
||||
assert.ok(!seen.has(marker));
|
||||
seen.add(marker);
|
||||
return true;
|
||||
});
|
||||
|
||||
seen.clear();
|
||||
new SnippetParser().parse('${1:${FOO:abc$1def}}').walk(marker => {
|
||||
assert.ok(!seen.has(marker));
|
||||
seen.add(marker);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('Snippets: make parser ignore `${0|choice|}`, #31599', function () {
|
||||
assertTextAndMarker('${0|foo,bar|}', '${0|foo,bar|}', Text);
|
||||
assertTextAndMarker('${1|foo,bar|}', 'foo', Placeholder);
|
||||
});
|
||||
|
||||
|
||||
test('Transform -> FormatString#resolve', function () {
|
||||
|
||||
// shorthand functions
|
||||
assert.strictEqual(new FormatString(1, 'upcase').resolve('foo'), 'FOO');
|
||||
assert.strictEqual(new FormatString(1, 'downcase').resolve('FOO'), 'foo');
|
||||
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar');
|
||||
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat');
|
||||
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo');
|
||||
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo');
|
||||
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo');
|
||||
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo');
|
||||
assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input');
|
||||
|
||||
// if
|
||||
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(undefined), '');
|
||||
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(''), '');
|
||||
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve('bar'), 'foo');
|
||||
|
||||
// else
|
||||
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(undefined), 'foo');
|
||||
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(''), 'foo');
|
||||
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve('bar'), 'bar');
|
||||
|
||||
// if-else
|
||||
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(undefined), 'foo');
|
||||
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(''), 'foo');
|
||||
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar');
|
||||
});
|
||||
|
||||
test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function () {
|
||||
const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"');
|
||||
assert.strictEqual(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"');
|
||||
});
|
||||
|
||||
test('[BUG] HTML attribute suggestions: Snippet session does not have end-position set, #33147', function () {
|
||||
|
||||
const { placeholders } = new SnippetParser().parse('src="$1"', true);
|
||||
const [first, second] = placeholders;
|
||||
|
||||
assert.strictEqual(placeholders.length, 2);
|
||||
assert.strictEqual(first.index, 1);
|
||||
assert.strictEqual(second.index, 0);
|
||||
|
||||
});
|
||||
|
||||
test('Snippet optional transforms are not applied correctly when reusing the same variable, #37702', function () {
|
||||
|
||||
const transform = new Transform();
|
||||
transform.appendChild(new FormatString(1, 'upcase'));
|
||||
transform.appendChild(new FormatString(2, 'upcase'));
|
||||
transform.regexp = /^(.)|-(.)/g;
|
||||
|
||||
assert.strictEqual(transform.resolve('my-file-name'), 'MyFileName');
|
||||
|
||||
const clone = transform.clone();
|
||||
assert.strictEqual(clone.resolve('my-file-name'), 'MyFileName');
|
||||
});
|
||||
|
||||
test('problem with snippets regex #40570', function () {
|
||||
|
||||
const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}');
|
||||
assertMarker(snippet, Variable);
|
||||
});
|
||||
|
||||
test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function () {
|
||||
let transform = new Transform();
|
||||
transform.appendChild(new Text('bar'));
|
||||
transform.regexp = new RegExp('foo', 'gi');
|
||||
assert.strictEqual(transform.toTextmateString(), '/foo/bar/ig');
|
||||
});
|
||||
|
||||
test('Snippet parser freeze #53144', function () {
|
||||
let snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}');
|
||||
assertMarker(snippet, Placeholder);
|
||||
});
|
||||
|
||||
test('snippets variable not resolved in JSON proposal #52931', function () {
|
||||
assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder);
|
||||
});
|
||||
|
||||
test('Mirroring sequence of nested placeholders not selected properly on backjumping #58736', function () {
|
||||
let snippet = new SnippetParser().parse('${3:nest1 ${1:nest2 ${2:nest3}}} $3');
|
||||
assert.strictEqual(snippet.children.length, 3);
|
||||
assert.ok(snippet.children[0] instanceof Placeholder);
|
||||
assert.ok(snippet.children[1] instanceof Text);
|
||||
assert.ok(snippet.children[2] instanceof Placeholder);
|
||||
|
||||
function assertParent(marker: Marker) {
|
||||
marker.children.forEach(assertParent);
|
||||
if (!(marker instanceof Placeholder)) {
|
||||
return;
|
||||
}
|
||||
let found = false;
|
||||
let m: Marker = marker;
|
||||
while (m && !found) {
|
||||
if (m.parent === snippet) {
|
||||
found = true;
|
||||
}
|
||||
m = m.parent;
|
||||
}
|
||||
assert.ok(found);
|
||||
}
|
||||
let [, , clone] = snippet.children;
|
||||
assertParent(clone);
|
||||
});
|
||||
|
||||
test('Backspace can\'t be escaped in snippet variable transforms #65412', function () {
|
||||
|
||||
let snippet = new SnippetParser().parse('namespace ${TM_DIRECTORY/[\\/]/\\\\/g};');
|
||||
assertMarker(snippet, Text, Variable, Text);
|
||||
});
|
||||
|
||||
test('Snippet cannot escape closing bracket inside conditional insertion variable replacement #78883', function () {
|
||||
|
||||
let snippet = new SnippetParser().parse('${TM_DIRECTORY/(.+)/${1:+import { hello \\} from world}/}');
|
||||
let variable = <Variable>snippet.children[0];
|
||||
assert.strictEqual(snippet.children.length, 1);
|
||||
assert.ok(variable instanceof Variable);
|
||||
assert.ok(variable.transform);
|
||||
assert.strictEqual(variable.transform!.children.length, 1);
|
||||
assert.ok(variable.transform!.children[0] instanceof FormatString);
|
||||
assert.strictEqual((<FormatString>variable.transform!.children[0]).ifValue, 'import { hello } from world');
|
||||
assert.strictEqual((<FormatString>variable.transform!.children[0]).elseValue, undefined);
|
||||
});
|
||||
|
||||
test('Snippet escape backslashes inside conditional insertion variable replacement #80394', function () {
|
||||
|
||||
let snippet = new SnippetParser().parse('${CURRENT_YEAR/(.+)/${1:+\\\\}/}');
|
||||
let variable = <Variable>snippet.children[0];
|
||||
assert.strictEqual(snippet.children.length, 1);
|
||||
assert.ok(variable instanceof Variable);
|
||||
assert.ok(variable.transform);
|
||||
assert.strictEqual(variable.transform!.children.length, 1);
|
||||
assert.ok(variable.transform!.children[0] instanceof FormatString);
|
||||
assert.strictEqual((<FormatString>variable.transform!.children[0]).ifValue, '\\');
|
||||
assert.strictEqual((<FormatString>variable.transform!.children[0]).elseValue, undefined);
|
||||
});
|
||||
});
|
||||
1166
packages/foam-vscode/src/core/common/snippetParser.ts
Normal file
1166
packages/foam-vscode/src/core/common/snippetParser.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,8 @@ export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const eol = detectNewline(text) || os.EOL;
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
let startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
let endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
const endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
@@ -34,5 +34,5 @@ const getOffset = (
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i].length);
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MarkdownResourceProvider } from '../markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
@@ -16,11 +15,11 @@ describe('generateHeadings', () => {
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([URI.joinPath(TEST_DATA_DIR, '__scaffold__')]);
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MarkdownResourceProvider } from '../markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
@@ -13,14 +12,15 @@ Logger.setLevel('error');
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
// TODO slug must be reserved for actual slugs, not file names
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Resource;
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([URI.joinPath(TEST_DATA_DIR, '__scaffold__')]);
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
@@ -7,13 +6,10 @@ import {
|
||||
} from '../markdown-provider';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
@@ -60,7 +56,7 @@ export const generateLinkReferences = (
|
||||
const first = note.definitions[0];
|
||||
const last = note.definitions[note.definitions.length - 1];
|
||||
|
||||
var nonGeneratedReferenceDefinitions = note.definitions;
|
||||
let nonGeneratedReferenceDefinitions = note.definitions;
|
||||
|
||||
// if we have more definitions then referenced pages AND the page refers to a page
|
||||
// we expect non-generated link definitions to be present
|
||||
@@ -119,7 +115,7 @@ export const generateLinkReferences = (
|
||||
return null;
|
||||
}
|
||||
|
||||
var fullReferences = `${newReferences}`;
|
||||
let fullReferences = `${newReferences}`;
|
||||
// If there are any non-generated definitions, add those to the output as well
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
@@ -168,7 +164,7 @@ export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
|
||||
return {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
uriToSlug(note.uri)
|
||||
note.uri.getName()
|
||||
)}${paddingEnd}`,
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
@@ -176,14 +172,3 @@ export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileName
|
||||
* @returns null if file name is already in kebab case otherise returns
|
||||
* the kebab cased file name
|
||||
*/
|
||||
export const getKebabCaseFileName = (fileName: string) => {
|
||||
const kebabCasedFileName = slugger.slug(fileName);
|
||||
return kebabCasedFileName === fileName ? null : kebabCasedFileName;
|
||||
};
|
||||
|
||||
@@ -5,11 +5,10 @@ import {
|
||||
} from './markdown-provider';
|
||||
import { DirectLink, WikiLink } from './model/note';
|
||||
import { Logger } from './utils/log';
|
||||
import { uriToSlug } from './utils/slug';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamGraph } from './model/graph';
|
||||
import { Range } from './model/range';
|
||||
import { createTestWorkspace } from '../test/test-utils';
|
||||
import { createTestWorkspace, getRandomURI } from '../test/test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -40,50 +39,45 @@ const pageE = `
|
||||
# Page E
|
||||
`;
|
||||
|
||||
const createNoteFromMarkdown = (path: string, content: string) =>
|
||||
createMarkdownParser([]).parse(URI.file(path), content);
|
||||
const createNoteFromMarkdown = (content: string, path?: string) =>
|
||||
createMarkdownParser([]).parse(
|
||||
path ? URI.file(path) : getRandomURI(),
|
||||
content
|
||||
);
|
||||
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
workspace.set(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
workspace.set(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
workspace.set(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
workspace.set(createNoteFromMarkdown('/page-d.md', pageD));
|
||||
workspace.set(createNoteFromMarkdown('/page-e.md', pageE));
|
||||
workspace.set(createNoteFromMarkdown(pageA, '/page-a.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageB, '/page-b.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageC, '/page-c.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageD, '/page-d.md'));
|
||||
workspace.set(createNoteFromMarkdown(pageE, '/page-e.md'));
|
||||
|
||||
expect(
|
||||
workspace
|
||||
.list()
|
||||
.map(n => n.uri)
|
||||
.map(uriToSlug)
|
||||
.map(n => n.uri.getName())
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
|
||||
it('Ingores external links', () => {
|
||||
it('Ignores external links', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to google](https://www.google.com)
|
||||
`
|
||||
`this is a [link to google](https://www.google.com)`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Ignores references to sections in the same file', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to intro](#introduction)
|
||||
`
|
||||
`this is a [link to intro](#introduction)`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Parses internal links correctly', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is a [link to page b](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
@@ -95,7 +89,6 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses links that have formatting in label', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [**link** with __formatting__](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
@@ -107,11 +100,11 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/page-a.md', pageA);
|
||||
const noteB = createNoteFromMarkdown('/page-b.md', pageB);
|
||||
const noteC = createNoteFromMarkdown('/page-c.md', pageC);
|
||||
const noteD = createNoteFromMarkdown('/Page D.md', pageD);
|
||||
const noteE = createNoteFromMarkdown('/page e.md', pageE);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/page-a.md');
|
||||
const noteB = createNoteFromMarkdown(pageB, '/page-b.md');
|
||||
const noteC = createNoteFromMarkdown(pageC, '/page-c.md');
|
||||
const noteD = createNoteFromMarkdown(pageD, '/Page D.md');
|
||||
const noteE = createNoteFromMarkdown(pageE, '/page e.md');
|
||||
|
||||
workspace
|
||||
.set(noteA)
|
||||
@@ -134,7 +127,6 @@ this is a [link to intro](#introduction)
|
||||
|
||||
it('Parses backlinks with an alias', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
|
||||
);
|
||||
expect(note.links.length).toEqual(2);
|
||||
@@ -151,9 +143,7 @@ this is a [link to intro](#introduction)
|
||||
});
|
||||
|
||||
it('Skips wikilinks in codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some text with our [[first-wikilink]].
|
||||
|
||||
\`\`\`
|
||||
@@ -161,8 +151,7 @@ this is inside a [[codeblock]]
|
||||
\`\`\`
|
||||
|
||||
this is some text with our [[second-wikilink]].
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.links.map(l => l.label)).toEqual([
|
||||
'first-wikilink',
|
||||
'second-wikilink',
|
||||
@@ -170,16 +159,13 @@ this is some text with our [[second-wikilink]].
|
||||
});
|
||||
|
||||
it('Skips wikilinks in inlined codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some text with our [[first-wikilink]].
|
||||
|
||||
this is \`inside a [[codeblock]]\`
|
||||
|
||||
this is some text with our [[second-wikilink]].
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.links.map(l => l.label)).toEqual([
|
||||
'first-wikilink',
|
||||
'second-wikilink',
|
||||
@@ -189,71 +175,71 @@ this is some text with our [[second-wikilink]].
|
||||
|
||||
describe('Note Title', () => {
|
||||
it('should initialize note title if heading exists', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-a.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Page A
|
||||
this note has a title
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(note.title).toBe('Page A');
|
||||
});
|
||||
|
||||
it('should support wikilinks and urls in title', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Page A with [[wikilink]] and a [url](https://google.com)
|
||||
this note has a title
|
||||
`);
|
||||
expect(note.title).toBe('Page A with wikilink and a url');
|
||||
});
|
||||
|
||||
it('should default to file name if heading does not exist', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-d.md',
|
||||
`
|
||||
This file has no heading.
|
||||
`
|
||||
`This file has no heading.`,
|
||||
'/page-d.md'
|
||||
);
|
||||
|
||||
expect(note.title).toEqual('page-d');
|
||||
});
|
||||
|
||||
it('should give precedence to frontmatter title over other headings', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.title).toBe('Note Title');
|
||||
});
|
||||
|
||||
it('should support numbers', () => {
|
||||
const note1 = createNoteFromMarkdown('/157.md', `hello`);
|
||||
const note1 = createNoteFromMarkdown(`hello`, '/157.md');
|
||||
expect(note1.title).toBe('157');
|
||||
|
||||
const note2 = createNoteFromMarkdown('/157.md', `# 158`);
|
||||
const note2 = createNoteFromMarkdown(`# 158`, '/157.md');
|
||||
expect(note2.title).toBe('158');
|
||||
|
||||
const note3 = createNoteFromMarkdown(
|
||||
'/157.md',
|
||||
`
|
||||
---
|
||||
title: 159
|
||||
---
|
||||
|
||||
# 158
|
||||
`
|
||||
`,
|
||||
'/157.md'
|
||||
);
|
||||
expect(note3.title).toBe('159');
|
||||
});
|
||||
|
||||
it('should not break on empty titles (see #276)', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/Hello Page.md',
|
||||
`
|
||||
#
|
||||
|
||||
this note has an empty title line
|
||||
`
|
||||
`,
|
||||
'/Hello Page.md'
|
||||
);
|
||||
expect(note.title).toEqual('Hello Page');
|
||||
});
|
||||
@@ -261,47 +247,38 @@ this note has an empty title line
|
||||
|
||||
describe('frontmatter', () => {
|
||||
it('should parse yaml frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title`
|
||||
);
|
||||
# Other Note Title`);
|
||||
|
||||
expect(note.properties.title).toBe('Note Title');
|
||||
expect(note.properties.date).toBe('20-12-12');
|
||||
});
|
||||
|
||||
it('should parse empty frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
---
|
||||
|
||||
# Empty Frontmatter
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
|
||||
it('should not fail when there are issues with parsing frontmatter', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
title: - one
|
||||
- two
|
||||
- #
|
||||
---
|
||||
|
||||
`
|
||||
);
|
||||
`);
|
||||
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
@@ -310,11 +287,11 @@ title: - one
|
||||
describe('wikilinks definitions', () => {
|
||||
it('can generate links without file extension when includeExtension = false', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
|
||||
|
||||
const noExtRefs = createMarkdownReferences(workspace, noteA.uri, false);
|
||||
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
|
||||
@@ -322,11 +299,11 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
it('can generate links with file extension when includeExtension = true', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
|
||||
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
|
||||
@@ -334,11 +311,11 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
it('use relative paths', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir2/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir3/page-c.md', pageC));
|
||||
.set(createNoteFromMarkdown(pageB, '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown(pageC, '/dir3/page-c.md'));
|
||||
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual([
|
||||
@@ -350,13 +327,10 @@ describe('wikilinks definitions', () => {
|
||||
|
||||
describe('tags plugin', () => {
|
||||
it('can find tags in the text of the note', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
# this is a #heading
|
||||
#this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags).toEqual([
|
||||
{ label: 'heading', range: Range.create(1, 12, 1, 20) },
|
||||
{ label: 'this', range: Range.create(2, 0, 2, 5) },
|
||||
@@ -367,16 +341,13 @@ describe('tags plugin', () => {
|
||||
});
|
||||
|
||||
it('will skip tags in codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some #text that includes #tags we #care-about.
|
||||
|
||||
\`\`\`
|
||||
this is a #codeblock
|
||||
\`\`\`
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'text',
|
||||
'tags',
|
||||
@@ -385,13 +356,9 @@ this is a #codeblock
|
||||
});
|
||||
|
||||
it('will skip tags in inlined codeblocks', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
this is some #text that includes #tags we #care-about.
|
||||
this is a \`inlined #codeblock\`
|
||||
`
|
||||
);
|
||||
this is a \`inlined #codeblock\` `);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'text',
|
||||
'tags',
|
||||
@@ -399,16 +366,13 @@ this is a \`inlined #codeblock\`
|
||||
]);
|
||||
});
|
||||
it('can find tags as text in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: hello, world this_is_good
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'hello',
|
||||
'world',
|
||||
@@ -420,16 +384,13 @@ this is some #text that includes #tags we #care-about.
|
||||
});
|
||||
|
||||
it('can find tags as array in yaml', () => {
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: [hello, world, this_is_good]
|
||||
---
|
||||
# this is a heading
|
||||
this is some #text that includes #tags we #care-about.
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags.map(t => t.label)).toEqual([
|
||||
'hello',
|
||||
'world',
|
||||
@@ -444,16 +405,13 @@ this is some #text that includes #tags we #care-about.
|
||||
// For now it's enough to just get the YAML block range
|
||||
// in the future we might want to be more specific
|
||||
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'/dir1/page-a.md',
|
||||
`
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: [hello, world, this_is_good]
|
||||
---
|
||||
# this is a heading
|
||||
this is some text
|
||||
`
|
||||
);
|
||||
`);
|
||||
expect(noteA.tags[0]).toEqual({
|
||||
label: 'hello',
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
@@ -461,6 +419,45 @@ this is some text
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sections plugin', () => {
|
||||
it('should find sections within the note', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Section 1
|
||||
|
||||
This is the content of section 1.
|
||||
|
||||
## Section 1.1
|
||||
|
||||
This is the content of section 1.1.
|
||||
|
||||
# Section 2
|
||||
|
||||
This is the content of section 2.
|
||||
`);
|
||||
expect(note.sections).toHaveLength(3);
|
||||
expect(note.sections[0].label).toEqual('Section 1');
|
||||
expect(note.sections[0].range).toEqual(Range.create(1, 0, 9, 0));
|
||||
expect(note.sections[1].label).toEqual('Section 1.1');
|
||||
expect(note.sections[1].range).toEqual(Range.create(5, 0, 9, 0));
|
||||
expect(note.sections[2].label).toEqual('Section 2');
|
||||
expect(note.sections[2].range).toEqual(Range.create(9, 0, 13, 0));
|
||||
});
|
||||
|
||||
it('should support wikilinks and links in the section label', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
# Section with [[wikilink]]
|
||||
|
||||
This is the content of section with wikilink
|
||||
|
||||
## Section with [url](https://google.com)
|
||||
|
||||
This is the content of section with url`);
|
||||
expect(note.sections).toHaveLength(2);
|
||||
expect(note.sections[0].label).toEqual('Section with wikilink');
|
||||
expect(note.sections[1].label).toEqual('Section with url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser plugins', () => {
|
||||
const testPlugin: ParserPlugin = {
|
||||
visit: (node, note) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Node, Position as AstPosition } from 'unist';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Point, Node, Position as AstPosition } from 'unist';
|
||||
import unified from 'unified';
|
||||
import markdownParse from 'remark-parse';
|
||||
import wikiLinkPlugin from 'remark-wiki-link';
|
||||
import frontmatterPlugin from 'remark-frontmatter';
|
||||
import { parse as parseYAML } from 'yaml';
|
||||
import visit from 'unist-util-visit';
|
||||
import { Parent, Point } from 'unist';
|
||||
import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import {
|
||||
@@ -17,13 +17,7 @@ import {
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
extractHashtags,
|
||||
extractTagsFromProp,
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { extractHashtags, extractTagsFromProp, isNone, isSome } from './utils';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
@@ -69,7 +63,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + URI.toString(uri));
|
||||
Logger.info('Found: ' + uri.toString());
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
@@ -100,15 +94,26 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return URI.isMarkdownFile(uri);
|
||||
return uri.isMarkdown();
|
||||
}
|
||||
|
||||
read(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
let content = await this.dataStore.read(uri);
|
||||
if (isSome(content) && uri.fragment) {
|
||||
const resource = this.parser.parse(uri, content);
|
||||
const section = Resource.findSection(resource, uri.fragment);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
@@ -123,27 +128,39 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
) {
|
||||
let targetUri: URI | undefined;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
case 'wikilink': {
|
||||
const definitionUri = resource.definitions.find(
|
||||
def => def.label === link.target
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, resource.uri);
|
||||
const definedUri = resource.uri.resolve(definitionUri);
|
||||
targetUri =
|
||||
workspace.find(definedUri, resource.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
const [target, section] = link.target.split('#');
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
target === ''
|
||||
? resource.uri
|
||||
: workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
|
||||
if (section) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
}
|
||||
case 'link': {
|
||||
const [target, section] = link.target.split('#');
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(URI.resolve(link.target, resource.uri).path);
|
||||
workspace.find(target, resource.uri)?.uri ??
|
||||
URI.placeholder(resource.uri.resolve(link.target).path);
|
||||
if (section && !targetUri.isPlaceholder()) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return targetUri;
|
||||
}
|
||||
@@ -161,9 +178,9 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
*/
|
||||
const getTextFromChildren = (root: Node): string => {
|
||||
let text = '';
|
||||
visit(root, 'text', node => {
|
||||
if (node.type === 'text') {
|
||||
text = text + (node as any).value;
|
||||
visit(root, node => {
|
||||
if (node.type === 'text' || node.type === 'wikiLink') {
|
||||
text = text + ((node as any).value || '');
|
||||
}
|
||||
});
|
||||
return text;
|
||||
@@ -186,7 +203,7 @@ const tagsPlugin: ParserPlugin = {
|
||||
if (node.type === 'text') {
|
||||
const tags = extractHashtags((node as any).value);
|
||||
tags.forEach(tag => {
|
||||
let start = astPointToFoamPosition(node.position!.start);
|
||||
const start = astPointToFoamPosition(node.position!.start);
|
||||
start.character = start.character + tag.offset;
|
||||
const end: Position = {
|
||||
line: start.line,
|
||||
@@ -201,6 +218,53 @@ const tagsPlugin: ParserPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
let sectionStack: Array<{ label: string; level: number; start: Position }> = [];
|
||||
const sectionsPlugin: ParserPlugin = {
|
||||
name: 'section',
|
||||
onWillVisitTree: () => {
|
||||
sectionStack = [];
|
||||
},
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'heading') {
|
||||
const level = (node as any).depth;
|
||||
const label = getTextFromChildren(node);
|
||||
if (!label || !level) {
|
||||
return;
|
||||
}
|
||||
const start = astPositionToFoamRange(node.position!).start;
|
||||
|
||||
// Close all the sections that are not parents of the current section
|
||||
while (
|
||||
sectionStack.length > 0 &&
|
||||
sectionStack[sectionStack.length - 1].level >= level
|
||||
) {
|
||||
const section = sectionStack.pop();
|
||||
note.sections.push({
|
||||
label: section.label,
|
||||
range: Range.createFromPosition(section.start, start),
|
||||
});
|
||||
}
|
||||
|
||||
// Add the new section to the stack
|
||||
sectionStack.push({ label, level, start });
|
||||
}
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
const end = Position.create(note.source.end.line + 1, 0);
|
||||
// Close all the remainig sections
|
||||
while (sectionStack.length > 0) {
|
||||
const section = sectionStack.pop();
|
||||
note.sections.push({
|
||||
label: section.label,
|
||||
range: { start: section.start, end },
|
||||
});
|
||||
}
|
||||
note.sections.sort((a, b) =>
|
||||
Position.compareTo(a.range.start, b.range.start)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const titlePlugin: ParserPlugin = {
|
||||
name: 'title',
|
||||
visit: (node, note) => {
|
||||
@@ -209,8 +273,8 @@ const titlePlugin: ParserPlugin = {
|
||||
node.type === 'heading' &&
|
||||
(node as any).depth === 1
|
||||
) {
|
||||
note.title =
|
||||
((node as Parent)!.children?.[0] as any)?.value || note.title;
|
||||
const title = getTextFromChildren(node);
|
||||
note.title = title.length > 0 ? title : note.title;
|
||||
}
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
@@ -219,7 +283,7 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title === '') {
|
||||
note.title = URI.getBasename(note.uri);
|
||||
note.title = note.uri.getName();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -254,7 +318,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = URI.resolve(targetUri, note.uri);
|
||||
const uri = note.uri.resolve(targetUri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
@@ -295,7 +359,7 @@ const handleError = (
|
||||
const name = plugin.name || '';
|
||||
Logger.warn(
|
||||
`Error while executing [${fnName}] in plugin [${name}]. ${
|
||||
uri ? 'for file [' + URI.toString(uri) : ']'
|
||||
uri ? 'for file [' + uri.toString() : ']'
|
||||
}.`,
|
||||
e
|
||||
);
|
||||
@@ -314,6 +378,7 @@ export function createMarkdownParser(
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
sectionsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
|
||||
@@ -327,7 +392,7 @@ export function createMarkdownParser(
|
||||
|
||||
const foamParser: ResourceParser = {
|
||||
parse: (uri: URI, markdown: string): Resource => {
|
||||
Logger.debug('Parsing:', URI.toString(uri));
|
||||
Logger.debug('Parsing:', uri.toString());
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
try {
|
||||
return plugin.onWillParseMarkdown?.(acc) || acc;
|
||||
@@ -339,11 +404,12 @@ export function createMarkdownParser(
|
||||
const tree = parser.parse(markdown);
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
var note: Resource = {
|
||||
const note: Resource = {
|
||||
uri: uri,
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: '',
|
||||
sections: [],
|
||||
tags: [],
|
||||
links: [],
|
||||
definitions: [],
|
||||
@@ -384,10 +450,7 @@ export function createMarkdownParser(
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn(
|
||||
`Error while parsing YAML for [${URI.toString(uri)}]`,
|
||||
e
|
||||
);
|
||||
Logger.warn(`Error while parsing YAML for [${uri.toString()}]`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +481,7 @@ function getFoamDefinitions(
|
||||
fileEndPoint: Position
|
||||
): NoteLinkDefinition[] {
|
||||
let previousLine = fileEndPoint.line;
|
||||
let foamDefinitions = [];
|
||||
const foamDefinitions = [];
|
||||
|
||||
// walk through each definition in reverse order
|
||||
// (last one first)
|
||||
@@ -441,7 +504,7 @@ function getFoamDefinitions(
|
||||
export function stringifyMarkdownLinkReferenceDefinition(
|
||||
definition: NoteLinkDefinition
|
||||
) {
|
||||
let url =
|
||||
const url =
|
||||
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
|
||||
let text = `[${definition.label}]: ${url}`;
|
||||
if (definition.title) {
|
||||
@@ -459,9 +522,8 @@ export function createMarkdownReferences(
|
||||
// Should never occur since we're already in a file,
|
||||
if (source?.type !== 'note') {
|
||||
console.warn(
|
||||
`Note ${URI.toString(
|
||||
noteUri
|
||||
)} note found in workspace when attempting to generate markdown reference list`
|
||||
`Note ${noteUri.toString()} note found in workspace when attempting \
|
||||
to generate markdown reference list`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -473,9 +535,7 @@ export function createMarkdownReferences(
|
||||
const target = workspace.find(targetUri);
|
||||
if (isNone(target)) {
|
||||
Logger.warn(
|
||||
`Link ${URI.toString(targetUri)} in ${URI.toString(
|
||||
noteUri
|
||||
)} is not valid.`
|
||||
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -484,10 +544,10 @@ export function createMarkdownReferences(
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = URI.relativePath(noteUri, target.uri);
|
||||
const pathToNote = includeExtension
|
||||
? relativePath
|
||||
: dropExtension(relativePath);
|
||||
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
|
||||
if (!includeExtension) {
|
||||
relativeUri = relativeUri.changeExtension('*', '');
|
||||
}
|
||||
|
||||
// [wikilink-text]: path/to/file.md "Page title"
|
||||
return {
|
||||
@@ -495,7 +555,7 @@ export function createMarkdownReferences(
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText || link.label,
|
||||
url: pathToNote,
|
||||
url: relativeUri.path,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -76,9 +76,9 @@ export class FoamGraph implements IDisposable {
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
keepMonitoring = false
|
||||
): FoamGraph {
|
||||
let graph = new FoamGraph(workspace);
|
||||
const graph = new FoamGraph(workspace);
|
||||
|
||||
workspace.list().forEach(resource => graph.resolveResource(resource));
|
||||
if (keepMonitoring) {
|
||||
@@ -99,15 +99,18 @@ export class FoamGraph implements IDisposable {
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
let resourcesToUpdate: Resource[] = [];
|
||||
const resourcesToUpdate: URI[] = [];
|
||||
for (const placeholderId of this.placeholders.keys()) {
|
||||
// quick and dirty check for affected resources
|
||||
if (resource.uri.path.endsWith(placeholderId + '.md')) {
|
||||
resourcesToUpdate.push(resource);
|
||||
resourcesToUpdate.push(
|
||||
...this.backlinks.get(placeholderId).map(c => c.source)
|
||||
);
|
||||
// resourcesToUpdate.push(resource);
|
||||
}
|
||||
}
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res.uri))
|
||||
this.resolveResource(this.workspace.get(res))
|
||||
);
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
@@ -170,7 +173,7 @@ export class FoamGraph implements IDisposable {
|
||||
|
||||
this.backlinks.get(target.path)?.push(connection);
|
||||
|
||||
if (URI.isPlaceholder(target)) {
|
||||
if (target.isPlaceholder()) {
|
||||
this.placeholders.set(uriToPlaceholderId(target), target);
|
||||
}
|
||||
return this;
|
||||
@@ -190,7 +193,7 @@ export class FoamGraph implements IDisposable {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
!source.isEqual(c.source) || !target.isEqual(c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links.set(
|
||||
@@ -206,7 +209,7 @@ export class FoamGraph implements IDisposable {
|
||||
);
|
||||
if (this.backlinks.get(target.path)?.length === 0) {
|
||||
this.backlinks.delete(target.path);
|
||||
if (URI.isPlaceholder(target)) {
|
||||
if (target.isPlaceholder()) {
|
||||
this.placeholders.delete(uriToPlaceholderId(target));
|
||||
}
|
||||
}
|
||||
@@ -232,8 +235,8 @@ export class FoamGraph implements IDisposable {
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
URI.isEqual(a.source, b.source) &&
|
||||
URI.isEqual(a.target, b.target) &&
|
||||
a.source.isEqual(b.source) &&
|
||||
a.target.isEqual(b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
|
||||
@@ -38,12 +38,17 @@ export interface Tag {
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
label: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
uri: URI;
|
||||
type: string;
|
||||
title: string;
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
sections: Section[];
|
||||
tags: Tag[];
|
||||
links: ResourceLink[];
|
||||
|
||||
@@ -66,7 +71,7 @@ export abstract class Resource {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
URI.isUri((thing as Resource).uri) &&
|
||||
(thing as Resource).uri instanceof URI &&
|
||||
typeof (thing as Resource).title === 'string' &&
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
@@ -74,4 +79,11 @@ export abstract class Resource {
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
public static findSection(resource: Resource, label: string): Section | null {
|
||||
if (label) {
|
||||
return resource.sections.find(s => s.label === label) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ export class FoamTags implements IDisposable {
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
keepMonitoring = false
|
||||
): FoamTags {
|
||||
let tags = new FoamTags();
|
||||
const tags = new FoamTags();
|
||||
|
||||
workspace
|
||||
.list()
|
||||
@@ -68,7 +68,7 @@ export class FoamTags implements IDisposable {
|
||||
if (this.tags.has(tag)) {
|
||||
const remainingLocations = this.tags
|
||||
.get(tag)
|
||||
?.filter(uri => !URI.isEqual(uri, resource.uri));
|
||||
?.filter(uri => !uri.isEqual(resource.uri));
|
||||
|
||||
if (remainingLocations && remainingLocations.length > 0) {
|
||||
this.tags.set(tag, remainingLocations);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Logger } from '../utils/log';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
import { URI } from './uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
@@ -11,13 +10,13 @@ describe('Foam URI', () => {
|
||||
['https://www.google.com', URI.parse('https://www.google.com')],
|
||||
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
|
||||
['../relative/file.md', URI.file('/path/relative/file.md')],
|
||||
['#section', URI.create({ ...base, fragment: 'section' })],
|
||||
['#section', base.withFragment('section')],
|
||||
[
|
||||
'../relative/file.md#section',
|
||||
URI.parse('file:/path/relative/file.md#section'),
|
||||
],
|
||||
])('URI Parsing (%s)', (input, exp) => {
|
||||
const result = URI.resolve(input, base);
|
||||
const result = base.resolve(input);
|
||||
expect(result.scheme).toEqual(exp.scheme);
|
||||
expect(result.authority).toEqual(exp.authority);
|
||||
expect(result.path).toEqual(exp.path);
|
||||
@@ -30,8 +29,8 @@ describe('Foam URI', () => {
|
||||
const lowerCase = URI.parse('file:///c:/this/is/a/Path');
|
||||
expect(upperCase.path).toEqual('/C:/this/is/a/Path');
|
||||
expect(lowerCase.path).toEqual('/C:/this/is/a/Path');
|
||||
expect(URI.toFsPath(upperCase)).toEqual('C:\\this\\is\\a\\Path');
|
||||
expect(URI.toFsPath(lowerCase)).toEqual('C:\\this\\is\\a\\Path');
|
||||
expect(upperCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
|
||||
expect(lowerCase.toFsPath()).toEqual('C:\\this\\is\\a\\Path');
|
||||
});
|
||||
|
||||
it('consistently parses file paths', () => {
|
||||
@@ -48,13 +47,13 @@ describe('Foam URI', () => {
|
||||
const winUri = URI.file('c:\\this\\is\\a\\path');
|
||||
const unixUri = URI.file('/this/is/a/path');
|
||||
expect(winUri).toEqual(
|
||||
URI.create({
|
||||
new URI({
|
||||
scheme: 'file',
|
||||
path: '/C:/this/is/a/path',
|
||||
})
|
||||
);
|
||||
expect(unixUri).toEqual(
|
||||
URI.create({
|
||||
new URI({
|
||||
scheme: 'file',
|
||||
path: '/this/is/a/path',
|
||||
})
|
||||
@@ -63,26 +62,14 @@ describe('Foam URI', () => {
|
||||
});
|
||||
|
||||
it('supports computing relative paths', () => {
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.md'), '../hello.md')
|
||||
).toEqual(URI.file('/hello.md'));
|
||||
expect(URI.computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
|
||||
expect(URI.file('/my/file.md').resolve('../hello.md')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
|
||||
).toEqual(URI.file('/hello.markdown'));
|
||||
});
|
||||
|
||||
it('can be slugified', () => {
|
||||
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
|
||||
'no-directory'
|
||||
expect(URI.file('/my/file.md').resolve('../hello')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
|
||||
'manydotsname'
|
||||
expect(URI.file('/my/file.markdown').resolve('../hello')).toEqual(
|
||||
URI.file('/hello.markdown')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// Some code in this file comes from https://github.com/microsoft/vscode/main/src/vs/base/common/uri.ts
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
import { CharCode } from '../common/charCode';
|
||||
import * as pathUtils from '../utils/path';
|
||||
|
||||
/**
|
||||
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
|
||||
@@ -23,248 +23,134 @@ import { CharCode } from '../common/charCode';
|
||||
* urn:example:animal:ferret:nose
|
||||
* ```
|
||||
*/
|
||||
export interface URI {
|
||||
scheme: string;
|
||||
authority: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
const { posix } = paths;
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
export abstract class URI {
|
||||
static create(from: Partial<URI>): URI {
|
||||
// When using this method we assume the path is already posix
|
||||
// so we don't check whether it's a Windows path, nor we do any
|
||||
// conversion
|
||||
return {
|
||||
scheme: from.scheme ?? _empty,
|
||||
authority: from.authority ?? _empty,
|
||||
path: from.path ?? _empty,
|
||||
query: from.query ?? _empty,
|
||||
fragment: from.fragment ?? _empty,
|
||||
};
|
||||
export class URI {
|
||||
readonly scheme: string;
|
||||
readonly authority: string;
|
||||
readonly path: string;
|
||||
readonly query: string;
|
||||
readonly fragment: string;
|
||||
|
||||
constructor(from: Partial<URI> = {}) {
|
||||
this.scheme = from.scheme ?? _empty;
|
||||
this.authority = from.authority ?? _empty;
|
||||
this.path = from.path ?? _empty; // We assume the path is already posix
|
||||
this.query = from.query ?? _empty;
|
||||
this.fragment = from.fragment ?? _empty;
|
||||
}
|
||||
|
||||
static parse(value: string): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return URI.create({});
|
||||
return new URI();
|
||||
}
|
||||
let path = percentDecode(match[5] ?? _empty);
|
||||
if (URI.isWindowsPath(path)) {
|
||||
path = windowsPathToUriPath(path);
|
||||
}
|
||||
return URI.create({
|
||||
return new URI({
|
||||
scheme: match[2] || 'file',
|
||||
authority: percentDecode(match[4] ?? _empty),
|
||||
path: path,
|
||||
path: pathUtils.fromFsPath(percentDecode(match[5] ?? _empty))[0],
|
||||
query: percentDecode(match[7] ?? _empty),
|
||||
fragment: percentDecode(match[9] ?? _empty),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URI from value, taking into consideration possible relative paths.
|
||||
*
|
||||
* @param reference the URI to use as reference in case value is a relative path
|
||||
* @param value the value to parse for a URI
|
||||
* @returns the URI from the given value. In case of a relative path, the URI will take into account
|
||||
* the reference from which it is computed
|
||||
*/
|
||||
static resolve(value: string, reference: URI): URI {
|
||||
let uri = URI.parse(value);
|
||||
if (uri.scheme === 'file' && !value.startsWith('/')) {
|
||||
const [path, fragment] = value.split('#');
|
||||
uri =
|
||||
path.length > 0 ? URI.computeRelativeURI(reference, path) : reference;
|
||||
if (fragment) {
|
||||
uri = URI.create({
|
||||
...uri,
|
||||
fragment: fragment,
|
||||
});
|
||||
static file(value: string): URI {
|
||||
const [path, authority] = pathUtils.fromFsPath(value);
|
||||
return new URI({ scheme: 'file', authority, path });
|
||||
}
|
||||
|
||||
static placeholder(path: string): URI {
|
||||
return new URI({ scheme: 'placeholder', path: path });
|
||||
}
|
||||
|
||||
resolve(value: string | URI, isDirectory = false): URI {
|
||||
const uri = value instanceof URI ? value : URI.parse(value);
|
||||
if (!uri.isAbsolute()) {
|
||||
if (uri.scheme === 'file' || uri.scheme === 'placeholder') {
|
||||
let newUri = this.withFragment(uri.fragment);
|
||||
if (uri.path) {
|
||||
newUri = (isDirectory ? newUri : newUri.getDirectory())
|
||||
.joinPath(uri.path)
|
||||
.changeExtension('', this.getExtension());
|
||||
}
|
||||
return newUri;
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
static computeRelativeURI(reference: URI, relativeSlug: string): URI {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${posix.extname(reference.path)}`;
|
||||
return URI.create({
|
||||
...reference,
|
||||
path: posix.join(posix.dirname(reference.path), slug),
|
||||
});
|
||||
isAbsolute(): boolean {
|
||||
return pathUtils.isAbsolute(this.path);
|
||||
}
|
||||
|
||||
static file(path: string): URI {
|
||||
let authority = _empty;
|
||||
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if (URI.isWindowsPath(path)) {
|
||||
path = windowsPathToUriPath(path);
|
||||
}
|
||||
|
||||
// check for authority as used in UNC shares
|
||||
// or use the path as given
|
||||
if (path[0] === _slash && path[1] === _slash) {
|
||||
const idx = path.indexOf(_slash, 2);
|
||||
if (idx === -1) {
|
||||
authority = path.substring(2);
|
||||
path = _slash;
|
||||
} else {
|
||||
authority = path.substring(2, idx);
|
||||
path = path.substring(idx) || _slash;
|
||||
}
|
||||
}
|
||||
|
||||
return URI.create({ scheme: 'file', authority, path });
|
||||
getDirectory(): URI {
|
||||
const path = pathUtils.getDirectory(this.path);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
static placeholder(key: string): URI {
|
||||
return URI.create({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
getBasename(): string {
|
||||
return pathUtils.getBasename(this.path);
|
||||
}
|
||||
|
||||
static relativePath(source: URI, target: URI): string {
|
||||
const relativePath = posix.relative(
|
||||
posix.dirname(source.path),
|
||||
target.path
|
||||
getName(): string {
|
||||
return pathUtils.getName(this.path);
|
||||
}
|
||||
|
||||
getExtension(): string {
|
||||
return pathUtils.getExtension(this.path);
|
||||
}
|
||||
|
||||
changeExtension(from: string, to: string): URI {
|
||||
const path = pathUtils.changeExtension(this.path, from, to);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
joinPath(...paths: string[]) {
|
||||
const path = pathUtils.joinPath(this.path, ...paths);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
relativeTo(uri: URI) {
|
||||
const path = pathUtils.relativeTo(this.path, uri.path);
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
withFragment(fragment: string): URI {
|
||||
return new URI({ ...this, fragment });
|
||||
}
|
||||
|
||||
isPlaceholder(): boolean {
|
||||
return this.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
toFsPath() {
|
||||
return pathUtils.toFsPath(
|
||||
this.path,
|
||||
this.scheme === 'file' ? this.authority : ''
|
||||
);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
static getBasename(uri: URI) {
|
||||
return posix.parse(uri.path).name;
|
||||
toString(): string {
|
||||
return encode(this, false);
|
||||
}
|
||||
|
||||
static getDir(uri: URI) {
|
||||
return URI.file(posix.dirname(uri.path));
|
||||
isMarkdown(): boolean {
|
||||
const ext = this.getExtension();
|
||||
return ext === '.md' || ext === '.markdown';
|
||||
}
|
||||
|
||||
static getFileNameWithoutExtension(uri: URI) {
|
||||
return URI.getBasename(uri).replace(/\.[^.]+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
static createResourceUriFromPlaceholder(
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param uri The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
static joinPath(uri: URI, ...pathFragment: string[]): URI {
|
||||
if (!uri.path) {
|
||||
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
|
||||
}
|
||||
let newPath: string;
|
||||
if (URI.isWindowsPath(uri.path) && uri.scheme === 'file') {
|
||||
newPath = URI.file(paths.win32.join(URI.toFsPath(uri), ...pathFragment))
|
||||
.path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return URI.create({ ...uri, path: newPath });
|
||||
}
|
||||
|
||||
static toFsPath(uri: URI): string {
|
||||
let value: string;
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`;
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A &&
|
||||
uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a &&
|
||||
uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
// windows drive letter: file:///C:/far/boo
|
||||
value = uri.path[1].toUpperCase() + uri.path.substr(2);
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path;
|
||||
}
|
||||
if (URI.isWindowsPath(value)) {
|
||||
value = value.replace(/\//g, '\\');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static toString(uri: URI): string {
|
||||
return encode(uri, false);
|
||||
}
|
||||
|
||||
// --- utility
|
||||
|
||||
static isWindowsPath(path: string) {
|
||||
isEqual(uri: URI): boolean {
|
||||
return (
|
||||
(path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) ||
|
||||
(path.length >= 3 &&
|
||||
path.charCodeAt(0) === CharCode.Slash &&
|
||||
path.charCodeAt(2) === CharCode.Colon)
|
||||
this.authority === uri.authority &&
|
||||
this.scheme === uri.scheme &&
|
||||
this.path === uri.path &&
|
||||
this.fragment === uri.fragment &&
|
||||
this.query === uri.query
|
||||
);
|
||||
}
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof (thing as URI).authority === 'string' &&
|
||||
typeof (thing as URI).fragment === 'string' &&
|
||||
typeof (thing as URI).path === 'string' &&
|
||||
typeof (thing as URI).query === 'string' &&
|
||||
typeof (thing as URI).scheme === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
static isPlaceholder(uri: URI): boolean {
|
||||
return uri.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
static isEqual(a: URI, b: URI): boolean {
|
||||
return (
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path &&
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query
|
||||
);
|
||||
}
|
||||
static isMarkdownFile(uri: URI): boolean {
|
||||
return uri.path.endsWith('.md');
|
||||
}
|
||||
}
|
||||
|
||||
// --- encode / decode
|
||||
@@ -292,33 +178,6 @@ function percentDecode(str: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a windows-like path to standard URI path
|
||||
* - Normalize the Windows drive letter to upper case
|
||||
* - replace \ with /
|
||||
* - always start with /
|
||||
*
|
||||
* see https://github.com/foambubble/foam/issues/813
|
||||
* see https://github.com/microsoft/vscode/issues/43959
|
||||
* see https://github.com/microsoft/vscode/issues/116298
|
||||
*
|
||||
* @param path the path to convert
|
||||
* @returns the URI compatible path
|
||||
*/
|
||||
function windowsPathToUriPath(path: string): string {
|
||||
path = path.charCodeAt(0) === CharCode.Slash ? path : `/${path}`;
|
||||
path = path.replace(/\\/g, _slash);
|
||||
const code = path.charCodeAt(1);
|
||||
if (
|
||||
path.charCodeAt(2) === CharCode.Colon &&
|
||||
code >= CharCode.a &&
|
||||
code <= CharCode.z
|
||||
) {
|
||||
path = `/${String.fromCharCode(code - 32)}:${path.substr(3)}`; // "/C:".length === 3
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the external version of a uri
|
||||
*/
|
||||
@@ -328,6 +187,7 @@ function encode(uri: URI, skipEncoding: boolean): string {
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FoamWorkspace, getReferenceType } from './workspace';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from './uri';
|
||||
@@ -6,24 +6,6 @@ import { createTestNote, createTestWorkspace } from '../../test/test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('Reference types', () => {
|
||||
it('Detects absolute references', () => {
|
||||
expect(getReferenceType('/hello')).toEqual('absolute-path');
|
||||
expect(getReferenceType('/hello/there')).toEqual('absolute-path');
|
||||
});
|
||||
it('Detects relative references', () => {
|
||||
expect(getReferenceType('../hello')).toEqual('relative-path');
|
||||
expect(getReferenceType('./hello')).toEqual('relative-path');
|
||||
expect(getReferenceType('./hello/there')).toEqual('relative-path');
|
||||
});
|
||||
it('Detects key references', () => {
|
||||
expect(getReferenceType('hello')).toEqual('key');
|
||||
});
|
||||
it('Detects URIs', () => {
|
||||
expect(getReferenceType(URI.file('/path/to/file.md'))).toEqual('uri');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace resources', () => {
|
||||
it('Adds notes to workspace', () => {
|
||||
const ws = createTestWorkspace();
|
||||
@@ -71,6 +53,41 @@ describe('Workspace resources', () => {
|
||||
ws.set(noteA);
|
||||
expect(ws.list()).toEqual([noteA]);
|
||||
});
|
||||
|
||||
it('#851 - listing by ID should not return files with same suffix', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'test-file.md' }))
|
||||
.set(createTestNote({ uri: 'file.md' }));
|
||||
expect(ws.listByIdentifier('file').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('Support dendron-style names', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'note.pdf' }))
|
||||
.set(createTestNote({ uri: 'note.md' }))
|
||||
.set(createTestNote({ uri: 'note.yo.md' }))
|
||||
.set(createTestNote({ uri: 'note2.md' }));
|
||||
for (const [reference, path] of [
|
||||
['note', '/note.md'],
|
||||
['note.md', '/note.md'],
|
||||
['note.yo', '/note.yo.md'],
|
||||
['note.yo.md', '/note.yo.md'],
|
||||
['note.pdf', '/note.pdf'],
|
||||
['note2', '/note2.md'],
|
||||
]) {
|
||||
expect(ws.listByIdentifier(reference)[0].uri.path).toEqual(path);
|
||||
expect(ws.find(reference).uri.path).toEqual(path);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should include fragment when finding resource URI', () => {
|
||||
const ws = createTestWorkspace()
|
||||
.set(createTestNote({ uri: 'test-file.md' }))
|
||||
.set(createTestNote({ uri: 'file.md' }));
|
||||
|
||||
const res = ws.find('test-file#my-section');
|
||||
expect(res.uri.fragment).toEqual('my-section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Graph', () => {
|
||||
@@ -166,6 +183,64 @@ describe('Identifier computation', () => {
|
||||
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
|
||||
expect(ws.getIdentifier(third.uri)).toEqual('path/for/page-a');
|
||||
});
|
||||
|
||||
it('should support sections in identifier computation', () => {
|
||||
const first = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
});
|
||||
const second = createTestNote({
|
||||
uri: '/another/way/for/page-a.md',
|
||||
});
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(first)
|
||||
.set(second)
|
||||
.set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
);
|
||||
});
|
||||
|
||||
const needle = '/project/car/todo';
|
||||
|
||||
test.each([
|
||||
[['/project/home/todo', '/other/todo', '/something/else'], 'car/todo'],
|
||||
[['/family/car/todo', '/other/todo'], 'project/car/todo'],
|
||||
[[], 'todo'],
|
||||
])('Find shortest identifier', (haystack, id) => {
|
||||
expect(FoamWorkspace.getShortestIdentifier(needle, haystack)).toEqual(id);
|
||||
});
|
||||
|
||||
it('should ignore same string in haystack', () => {
|
||||
const haystack = [
|
||||
needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
|
||||
expect(identifier).toEqual('car/todo');
|
||||
});
|
||||
|
||||
it('should return best guess when no solution is possible', () => {
|
||||
/**
|
||||
* In this case there is no way to uniquely identify the element,
|
||||
* our fallback is to just return the "least wrong" result, basically
|
||||
* a full identifier
|
||||
* This is an edge case that should never happen in a real repo
|
||||
*/
|
||||
const haystack = [
|
||||
'/parent/' + needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
const identifier = FoamWorkspace.getShortestIdentifier(needle, haystack);
|
||||
expect(identifier).toEqual('project/car/todo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wikilinks', () => {
|
||||
@@ -383,20 +458,6 @@ describe('Wikilinks', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Allows for dendron-style wikilinks, including a dot', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'dendron.style' }],
|
||||
});
|
||||
const noteB1 = createTestNote({ uri: '/path/to/another/dendron.style.md' });
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB1);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB1.uri]);
|
||||
});
|
||||
|
||||
it('Handles capitalization of files and wikilinks correctly', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
@@ -429,7 +490,7 @@ describe('Wikilinks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown direct links', () => {
|
||||
describe('Markdown direct links', () => {
|
||||
it('Support absolute and relative path', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { isSome, isNone, getShortestIdentifier } from '../utils';
|
||||
import { isAbsolute, getExtension, changeExtension } from '../utils/path';
|
||||
import { isSome } from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
|
||||
export function getReferenceType(
|
||||
reference: URI | string
|
||||
): 'uri' | 'absolute-path' | 'relative-path' | 'key' {
|
||||
if (URI.isUri(reference)) {
|
||||
return 'uri';
|
||||
}
|
||||
if (reference.startsWith('/')) {
|
||||
return 'absolute-path';
|
||||
}
|
||||
if (reference.startsWith('./') || reference.startsWith('../')) {
|
||||
return 'relative-path';
|
||||
}
|
||||
return 'key';
|
||||
}
|
||||
|
||||
function hasExtension(path: string): boolean {
|
||||
const dotIdx = path.lastIndexOf('.');
|
||||
return dotIdx > 0 && path.length - dotIdx <= 4;
|
||||
}
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
private onDidUpdateEmitter = new Emitter<{ old: Resource; new: Resource }>();
|
||||
@@ -63,9 +44,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public exists(uri: URI): boolean {
|
||||
return (
|
||||
!URI.isPlaceholder(uri) && isSome(this.resources.get(normalize(uri.path)))
|
||||
);
|
||||
return isSome(this.find(uri));
|
||||
}
|
||||
|
||||
public list(): Resource[] {
|
||||
@@ -81,18 +60,17 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public listById(resourceId: string): Resource[] {
|
||||
if (!hasExtension(resourceId)) {
|
||||
resourceId = resourceId + '.md';
|
||||
}
|
||||
resourceId = normalize(resourceId);
|
||||
let resources = [];
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
const needle = normalize('/' + identifier);
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
const resources = [];
|
||||
for (const key of this.resources.keys()) {
|
||||
if (key.endsWith(resourceId)) {
|
||||
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
|
||||
resources.push(this.resources.get(normalize(key)));
|
||||
}
|
||||
}
|
||||
return resources;
|
||||
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,60 +80,50 @@ export class FoamWorkspace implements IDisposable {
|
||||
*/
|
||||
public getIdentifier(forResource: URI): string {
|
||||
const amongst = [];
|
||||
const base = forResource.path.split('/').pop();
|
||||
const basename = forResource.getBasename();
|
||||
for (const res of this.resources.values()) {
|
||||
// Just a quick optimization to only add the elements that might match
|
||||
if (res.uri.path.endsWith(base)) {
|
||||
if (!URI.isEqual(res.uri, forResource)) {
|
||||
if (res.uri.path.endsWith(basename)) {
|
||||
if (!res.uri.isEqual(forResource)) {
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
const identifier = getShortestIdentifier(
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
|
||||
return identifier.endsWith('.md') ? identifier.slice(0, -3) : identifier;
|
||||
identifier = changeExtension(identifier, '.md', '');
|
||||
if (forResource.fragment) {
|
||||
identifier += `#${forResource.fragment}`;
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public find(resourceId: URI | string, reference?: URI): Resource | null {
|
||||
const refType = getReferenceType(resourceId);
|
||||
switch (refType) {
|
||||
case 'uri':
|
||||
const uri = resourceId as URI;
|
||||
return this.exists(uri)
|
||||
? this.resources.get(normalize(uri.path)) ?? null
|
||||
: null;
|
||||
|
||||
case 'key':
|
||||
const resources = this.listById(resourceId as string);
|
||||
const sorted = resources.sort((a, b) =>
|
||||
a.uri.path.localeCompare(b.uri.path)
|
||||
);
|
||||
return sorted[0] ?? null;
|
||||
|
||||
case 'absolute-path':
|
||||
if (!hasExtension(resourceId as string)) {
|
||||
resourceId = resourceId + '.md';
|
||||
}
|
||||
const resourceUri = URI.file(resourceId as string);
|
||||
return this.resources.get(normalize(resourceUri.path)) ?? null;
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
return null;
|
||||
}
|
||||
if (!hasExtension(resourceId as string)) {
|
||||
resourceId = resourceId + '.md';
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = URI.computeRelativeURI(reference, relativePath);
|
||||
return this.resources.get(normalize(targetUri.path)) ?? null;
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected reference type: ' + refType);
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this.resources.get(normalize((reference as URI).path)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
const [path, fragment] = (reference as string).split('#');
|
||||
if (FoamWorkspace.isIdentifier(path)) {
|
||||
resource = this.listByIdentifier(path)[0];
|
||||
} else {
|
||||
if (isAbsolute(path) || isSome(baseUri)) {
|
||||
if (getExtension(path) !== '.md') {
|
||||
const uri = baseUri.resolve(path + '.md');
|
||||
resource = uri ? this.resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
if (!resource) {
|
||||
const uri = baseUri.resolve(path);
|
||||
resource = uri ? this.resources.get(normalize(uri.path)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource && fragment) {
|
||||
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
|
||||
}
|
||||
return resource ?? null;
|
||||
}
|
||||
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
@@ -182,6 +150,51 @@ export class FoamWorkspace implements IDisposable {
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
}
|
||||
|
||||
static isIdentifier(path: string): boolean {
|
||||
return !(
|
||||
path.startsWith('/') ||
|
||||
path.startsWith('./') ||
|
||||
path.startsWith('../')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimal identifier for the given string amongst others
|
||||
*
|
||||
* @param forPath the value to compute the identifier for
|
||||
* @param amongst the set of strings within which to find the identifier
|
||||
*/
|
||||
static getShortestIdentifier(forPath: string, amongst: string[]): string {
|
||||
const needleTokens = forPath.split('/').reverse();
|
||||
const haystack = amongst
|
||||
.filter(value => value !== forPath)
|
||||
.map(value => value.split('/').reverse());
|
||||
|
||||
let tokenIndex = 0;
|
||||
let res = needleTokens;
|
||||
while (tokenIndex < needleTokens.length) {
|
||||
for (let j = haystack.length - 1; j >= 0; j--) {
|
||||
if (
|
||||
haystack[j].length < tokenIndex ||
|
||||
needleTokens[tokenIndex] !== haystack[j][tokenIndex]
|
||||
) {
|
||||
haystack.splice(j, 1);
|
||||
}
|
||||
}
|
||||
if (haystack.length === 0) {
|
||||
res = needleTokens.splice(0, tokenIndex + 1);
|
||||
break;
|
||||
}
|
||||
tokenIndex++;
|
||||
}
|
||||
const identifier = res
|
||||
.filter(token => token.trim() !== '')
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
|
||||
const normalize = (v: string) => v.toLocaleLowerCase();
|
||||
|
||||
@@ -5,14 +5,14 @@ import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = URI.joinPath(TEST_DATA_DIR, 'test-datastore');
|
||||
const testFolder = TEST_DATA_DIR.joinPath('test-datastore');
|
||||
|
||||
describe('Matcher', () => {
|
||||
it('generates globs with the base dir provided', () => {
|
||||
const matcher = new Matcher([testFolder], ['*'], []);
|
||||
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '*')),
|
||||
toMatcherPathFormat(testFolder.joinPath('*')),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Matcher', () => {
|
||||
const matcher = new Matcher([testFolder]);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '**', '*')),
|
||||
toMatcherPathFormat(testFolder.joinPath('**', '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,32 +28,32 @@ describe('Matcher', () => {
|
||||
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g1')),
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g2')),
|
||||
toMatcherPathFormat(testFolder.joinPath('g1')),
|
||||
toMatcherPathFormat(testFolder.joinPath('g2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a match method to filter strings', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.match(files)).toEqual([
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a isMatch method to see whether a file is matched or not', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(true);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
@@ -72,10 +72,10 @@ describe('Matcher', () => {
|
||||
it('ignores files in the exclude list', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(false);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
|
||||
@@ -2,11 +2,12 @@ import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import glob from 'glob';
|
||||
import { glob } from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { isWindows } from '../common/platform';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
export interface IMatcher {
|
||||
/**
|
||||
* Filters the given list of URIs, keepin only the ones that
|
||||
@@ -39,8 +40,8 @@ export interface IMatcher {
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => URI.toFsPath(uri).replace(/\\/g, '/')
|
||||
: (uri: URI) => URI.toFsPath(uri);
|
||||
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
|
||||
: (uri: URI) => uri.toFsPath();
|
||||
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
@@ -76,7 +77,7 @@ export class Matcher implements IMatcher {
|
||||
|
||||
match(files: URI[]) {
|
||||
const matches = micromatch(
|
||||
files.map(f => URI.toFsPath(f)),
|
||||
files.map(f => f.toFsPath()),
|
||||
this.include,
|
||||
{
|
||||
ignore: this.exclude,
|
||||
@@ -123,7 +124,7 @@ export class FileDataStore implements IDataStore {
|
||||
|
||||
async read(uri: URI) {
|
||||
try {
|
||||
return (await fs.promises.readFile(URI.toFsPath(uri))).toString();
|
||||
return (await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { getShortestIdentifier } from './core';
|
||||
import { extractHashtags } from './index';
|
||||
import { Logger } from './log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('getShortestIdentifier', () => {
|
||||
const needle = '/project/car/todo';
|
||||
|
||||
test.each([
|
||||
[['/project/home/todo', '/other/todo', '/something/else'], 'car/todo'],
|
||||
[['/family/car/todo', '/other/todo'], 'project/car/todo'],
|
||||
[[], 'todo'],
|
||||
])('Find shortest identifier', (haystack, id) => {
|
||||
expect(getShortestIdentifier(needle, haystack)).toEqual(id);
|
||||
});
|
||||
|
||||
it('should ignore same string in haystack', () => {
|
||||
const haystack = [
|
||||
needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
|
||||
expect(getShortestIdentifier(needle, haystack)).toEqual('car/todo');
|
||||
});
|
||||
|
||||
it('should return best guess when no solution is possible', () => {
|
||||
/**
|
||||
* In this case there is no way to uniquely identify the element,
|
||||
* our fallback is to just return the "least wrong" result, basically
|
||||
* a full identifier
|
||||
* This is an edge case that should never happen in a real repo
|
||||
*/
|
||||
const haystack = [
|
||||
'/parent/' + needle,
|
||||
'/project/home/todo',
|
||||
'/other/todo',
|
||||
'/something/else',
|
||||
];
|
||||
|
||||
expect(getShortestIdentifier(needle, haystack)).toEqual('project/car/todo');
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,19 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null; // eslint-disable-line
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isSome<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is NonNullable<T> {
|
||||
return value != null; // eslint-disable-line
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null; // eslint-disable-line
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export function isNumeric(value: string): boolean {
|
||||
@@ -25,43 +25,3 @@ export const hash = (text: string) =>
|
||||
.createHash('sha1')
|
||||
.update(text)
|
||||
.digest('hex');
|
||||
|
||||
/**
|
||||
* Returns the minimal identifier for the given string amongst others
|
||||
*
|
||||
* @param forValue the value to compute the identifier for
|
||||
* @param amongst the set of strings within which to find the identifier
|
||||
*/
|
||||
export const getShortestIdentifier = (
|
||||
forValue: string,
|
||||
amongst: string[]
|
||||
): string => {
|
||||
const needleTokens = forValue.split('/').reverse();
|
||||
const haystack = amongst
|
||||
.filter(value => value !== forValue)
|
||||
.map(value => value.split('/').reverse());
|
||||
|
||||
let tokenIndex = 0;
|
||||
let res = needleTokens;
|
||||
while (tokenIndex < needleTokens.length) {
|
||||
for (let j = haystack.length - 1; j >= 0; j--) {
|
||||
if (
|
||||
haystack[j].length < tokenIndex ||
|
||||
needleTokens[tokenIndex] !== haystack[j][tokenIndex]
|
||||
) {
|
||||
haystack.splice(j, 1);
|
||||
}
|
||||
}
|
||||
if (haystack.length === 0) {
|
||||
res = needleTokens.splice(0, tokenIndex + 1);
|
||||
break;
|
||||
}
|
||||
tokenIndex++;
|
||||
}
|
||||
const identifier = res
|
||||
.filter(token => token.trim() !== '')
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
return identifier;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,6 @@ import { titleCase } from 'title-case';
|
||||
export { extractHashtags, extractTagsFromProp } from './hashtags';
|
||||
export * from './core';
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
const parts = path.split('.');
|
||||
parts.pop();
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filename
|
||||
|
||||
@@ -58,7 +58,9 @@ export class ConsoleLogger extends BaseLogger {
|
||||
}
|
||||
|
||||
export class NoOpLogger extends BaseLogger {
|
||||
log(_l: LogLevel, _m?: string, ..._p: any[]): void {}
|
||||
log(_l: LogLevel, _m?: string, ..._p: any[]): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
|
||||
192
packages/foam-vscode/src/core/utils/path.ts
Normal file
192
packages/foam-vscode/src/core/utils/path.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { posix } from 'path';
|
||||
import { promises, constants } from 'fs';
|
||||
|
||||
/**
|
||||
* Converts filesystem path to POSIX path. Supported inputs are:
|
||||
* - Windows path starting with a drive letter, e.g. C:\dir\file.ext
|
||||
* - UNC path for a shared file, e.g. \\server\share\path\file.ext
|
||||
* - POSIX path, e.g. /dir/file.ext
|
||||
*
|
||||
* @param path A supported filesystem path.
|
||||
* @returns [path, authority] where path is a POSIX representation for the
|
||||
* given input and authority is undefined except for UNC paths.
|
||||
*/
|
||||
export function fromFsPath(path: string): [string, string] {
|
||||
let authority: string;
|
||||
if (isUNCShare(path)) {
|
||||
[path, authority] = parseUNCShare(path);
|
||||
path = path.replace(/\\/g, '/');
|
||||
} else if (hasDrive(path)) {
|
||||
path = '/' + path[0].toUpperCase() + path.substr(1).replace(/\\/g, '/');
|
||||
} else if (path[0] === '/' && hasDrive(path, 1)) {
|
||||
// POSIX representation of a Windows path: just normalize drive letter case
|
||||
path = '/' + path[1].toUpperCase() + path.substr(2);
|
||||
}
|
||||
return [path, authority];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a POSIX path to a filesystem path.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @param authority An optional authority used to build UNC paths. This only
|
||||
* makes sense for the Windows platform.
|
||||
* @returns A platform-specific representation of the given POSIX path.
|
||||
*/
|
||||
export function toFsPath(path: string, authority?: string): string {
|
||||
if (path[0] === '/' && hasDrive(path, 1)) {
|
||||
path = path.substr(1).replace(/\//g, '\\');
|
||||
if (authority) {
|
||||
path = `\\\\${authority}${path}`;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the containing directory of a POSIX path, e.g.
|
||||
* - /d1/d2/f.ext -> /d1/d2
|
||||
* - /d1/d2 -> /d1
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns true if the path is absolute, false otherwise.
|
||||
*/
|
||||
export function isAbsolute(path: string): boolean {
|
||||
return posix.isAbsolute(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the containing directory of a POSIX path, e.g.
|
||||
* - /d1/d2/f.ext -> /d1/d2
|
||||
* - /d1/d2 -> /d1
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The containing directory of the given path.
|
||||
*/
|
||||
export function getDirectory(path: string): string {
|
||||
return posix.dirname(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the basename of a POSIX path, e.g. /d/f.ext -> f.ext.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The basename of the given path.
|
||||
*/
|
||||
export function getBasename(path: string): string {
|
||||
return posix.basename(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the name of a POSIX path, e.g. /d/f.ext -> f.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The name of the given path.
|
||||
*/
|
||||
export function getName(path: string): string {
|
||||
return changeExtension(getBasename(path), '*', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the extension of a POSIX path, e.g.
|
||||
* - /d/f.ext -> .ext
|
||||
* - /d/f.g.ext -> .ext
|
||||
* - /d/f -> ''
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @returns The extension of the given path.
|
||||
*/
|
||||
export function getExtension(path: string): string {
|
||||
return posix.extname(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a POSIX path matching some extension to have another extension.
|
||||
*
|
||||
* @param path A POSIX path.
|
||||
* @param from The required current extension, or '*' to match any extension.
|
||||
* @param to The target extension.
|
||||
* @returns A POSIX path with its extension possibly changed.
|
||||
*/
|
||||
export function changeExtension(
|
||||
path: string,
|
||||
from: string,
|
||||
to: string
|
||||
): string {
|
||||
const old = getExtension(path);
|
||||
if ((from === '*' && old !== to) || old === from) {
|
||||
path = path.substring(0, path.length - old.length);
|
||||
return to ? path + to : path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a number of POSIX paths into a single POSIX path, e.g.
|
||||
* - /d1, d2, f.ext -> /d1/d2/f.ext
|
||||
* - /d1/d2, .., f.ext -> /d1/f.ext
|
||||
*
|
||||
* @param paths A variable number of POSIX paths.
|
||||
* @returns A POSIX path built from the given POSIX paths.
|
||||
*/
|
||||
export function joinPath(...paths: string[]): string {
|
||||
return posix.join(...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a POSIX path relative to another POSIX path, e.g.
|
||||
* - /d1/d2 relative to /d1 -> d2
|
||||
* - /d1/d2 relative to /d1/d3 -> ../d2
|
||||
*
|
||||
* @param path The POSIX path to be made relative.
|
||||
* @param basePath The POSIX base path.
|
||||
* @returns A POSIX path relative to the base path.
|
||||
*/
|
||||
export function relativeTo(path: string, basePath: string): string {
|
||||
return posix.relative(basePath, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if there is an accessible file for a path.
|
||||
*
|
||||
* @param fsPath A filesystem-specific path.
|
||||
* @returns true if an accesible file exists, false otherwise.
|
||||
*/
|
||||
export async function existsInFs(fsPath: string) {
|
||||
try {
|
||||
await promises.access(fsPath, constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDrive(path: string, idx = 0): boolean {
|
||||
if (path.length <= idx) {
|
||||
return false;
|
||||
}
|
||||
const c = path.charCodeAt(idx);
|
||||
return (
|
||||
((c >= CharCode.A && c <= CharCode.Z) ||
|
||||
(c >= CharCode.a && c <= CharCode.z)) &&
|
||||
path.charCodeAt(idx + 1) === CharCode.Colon
|
||||
);
|
||||
}
|
||||
|
||||
function isUNCShare(fsPath: string): boolean {
|
||||
return (
|
||||
fsPath.length >= 2 &&
|
||||
fsPath.charCodeAt(0) === CharCode.Backslash &&
|
||||
fsPath.charCodeAt(1) === CharCode.Backslash
|
||||
);
|
||||
}
|
||||
|
||||
function parseUNCShare(uncPath: string): [string, string] {
|
||||
const idx = uncPath.indexOf('\\', 2);
|
||||
if (idx === -1) {
|
||||
return [uncPath.substring(2), '\\'];
|
||||
} else {
|
||||
return [uncPath.substring(2, idx), uncPath.substring(idx) || '\\'];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export const uriToSlug = (uri: URI): string =>
|
||||
GithubSlugger.slug(URI.getBasename(uri));
|
||||
@@ -1,7 +1,6 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
|
||||
import { URI } from './core/model/uri';
|
||||
import { isWindows } from './utils';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
@@ -20,11 +19,9 @@ describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = URI.joinPath(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri),
|
||||
config,
|
||||
`${isoDate}.md`
|
||||
);
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
const oldValue = await workspace
|
||||
.getConfiguration('foam')
|
||||
@@ -34,8 +31,8 @@ describe('getDailyNotePath', () => {
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toEqual(
|
||||
URI.toFsPath(expectedPath)
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toEqual(
|
||||
expectedPath.toFsPath()
|
||||
);
|
||||
|
||||
await workspace
|
||||
@@ -60,7 +57,7 @@ describe('getDailyNotePath', () => {
|
||||
.update('openDailyNote.directory', config);
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toMatch(
|
||||
expect(getDailyNotePath(foamConfiguration, date).toFsPath()).toMatch(
|
||||
expectedPath
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import { isAbsolute } from 'path';
|
||||
import { focusNote, pathExists } from './utils';
|
||||
import { existsInFs } from './core/utils/path';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { NoteFactory } from './services/templates';
|
||||
@@ -16,7 +16,7 @@ import { NoteFactory } from './services/templates';
|
||||
*/
|
||||
export async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
const currentDate = date !== undefined ? date : new Date();
|
||||
const currentDate = date instanceof Date ? date : new Date();
|
||||
|
||||
const dailyNotePath = getDailyNotePath(foamConfiguration, currentDate);
|
||||
|
||||
@@ -25,7 +25,12 @@ export async function openDailyNoteFor(date?: Date) {
|
||||
dailyNotePath,
|
||||
currentDate
|
||||
);
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
// if a new file is created, the editor is automatically created
|
||||
// but forcing the focus will block the template placeholders from working
|
||||
// so we only explicitly focus on the note if the file already exists
|
||||
if (!isNew) {
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,16 +50,16 @@ export function getDailyNotePath(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
): URI {
|
||||
const dailyNoteDirectory: string =
|
||||
configuration.get('openDailyNote.directory') ?? '.';
|
||||
const dailyNoteDirectory = URI.file(
|
||||
configuration.get('openDailyNote.directory') ?? '.'
|
||||
);
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
|
||||
if (isAbsolute(dailyNoteDirectory)) {
|
||||
return URI.joinPath(URI.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
if (dailyNoteDirectory.isAbsolute()) {
|
||||
return dailyNoteDirectory.joinPath(dailyNoteFilename);
|
||||
} else {
|
||||
return URI.joinPath(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri),
|
||||
dailyNoteDirectory,
|
||||
return fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
|
||||
dailyNoteDirectory.path,
|
||||
dailyNoteFilename
|
||||
);
|
||||
}
|
||||
@@ -101,7 +106,7 @@ export async function createDailyNoteIfNotExists(
|
||||
dailyNotePath: URI,
|
||||
targetDate: Date
|
||||
) {
|
||||
if (await pathExists(dailyNotePath)) {
|
||||
if (await existsInFs(dailyNotePath.toFsPath())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -109,7 +114,7 @@ export async function createDailyNoteIfNotExists(
|
||||
configuration.get('openDailyNote.titleFormat') ??
|
||||
configuration.get('openDailyNote.filenameFormat');
|
||||
|
||||
const templateFallbackText: string = `---
|
||||
const templateFallbackText = `---
|
||||
foam_template:
|
||||
name: New Daily Note
|
||||
description: Foam's default daily note template
|
||||
|
||||
@@ -63,7 +63,7 @@ export class BacklinksTreeDataProvider
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
URI.isEqual(this.workspace.resolveLink(resource, link), uri)
|
||||
this.workspace.resolveLink(resource, link).isEqual(uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
@@ -72,7 +72,7 @@ export class BacklinksTreeDataProvider
|
||||
).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 start = Math.max(0, link.range.start.character - 15);
|
||||
const ellipsis = start === 0 ? '' : '...';
|
||||
|
||||
item.label = `${link.range.start.line}: ${ellipsis}${line.substr(
|
||||
@@ -93,7 +93,7 @@ export class BacklinksTreeDataProvider
|
||||
}
|
||||
|
||||
const backlinksByResourcePath = groupBy(
|
||||
this.graph.getConnections(uri).filter(c => URI.isEqual(c.target, uri)),
|
||||
this.graph.getConnections(uri).filter(c => c.target.isEqual(uri)),
|
||||
b => b.source.path
|
||||
);
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// import { env, window, Uri, Position, Selection, commands } from 'vscode';
|
||||
// import * as vscode from 'vscode';
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import {
|
||||
createFile,
|
||||
getUriInWorkspace,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
|
||||
describe('copyWithoutBrackets', () => {
|
||||
it('should pass CI', () => {
|
||||
expect(true).toBe(true);
|
||||
it('should get the input from the active editor selection', async () => {
|
||||
const { uri } = await createFile('This is my [[test-content]].');
|
||||
const { editor } = await showInEditor(uri);
|
||||
editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
|
||||
await commands.executeCommand('foam-vscode.copy-without-brackets');
|
||||
const value = await env.clipboard.readText();
|
||||
expect(value).toEqual('This is my Test Content.');
|
||||
});
|
||||
// it('should get the input from the active editor selection', async () => {
|
||||
// const doc = await vscode.workspace.openTextDocument(
|
||||
// Uri.parse('untitled:/hello.md')
|
||||
// );
|
||||
// const editor = await window.showTextDocument(doc);
|
||||
// editor.edit(builder => {
|
||||
// builder.insert(new Position(0, 0), 'This is my [[test-content]].');
|
||||
// });
|
||||
// editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
|
||||
// await commands.executeCommand('foam-vscode.copy-without-brackets');
|
||||
// const value = await env.clipboard.readText();
|
||||
// expect(value).toEqual('This is my Test Content.');
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Uri, commands, window, workspace } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import path from 'path';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { createFile } from '../test/test-utils-vscode';
|
||||
import * as editor from '../services/editor';
|
||||
|
||||
@@ -122,12 +121,12 @@ Template A
|
||||
});
|
||||
|
||||
it('should create a new template', async () => {
|
||||
const template = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'hello-world.md'
|
||||
);
|
||||
).fsPath;
|
||||
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(template);
|
||||
@@ -142,12 +141,12 @@ Template A
|
||||
|
||||
it('can be cancelled', async () => {
|
||||
// This is the default template which would be created.
|
||||
const template = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
const template = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'new-template.md'
|
||||
);
|
||||
).fsPath;
|
||||
window.showInputBox = jest.fn(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import * as path from 'path';
|
||||
import { commands, ExtensionContext, QuickPickItem, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
@@ -60,7 +58,7 @@ async function askUserForTemplate() {
|
||||
await Promise.all(
|
||||
templates.map(async templateUri => {
|
||||
const metadata = await getTemplateMetadata(templateUri);
|
||||
metadata.set('templatePath', path.basename(templateUri.path));
|
||||
metadata.set('templatePath', templateUri.getBasename());
|
||||
return metadata;
|
||||
})
|
||||
)
|
||||
@@ -105,7 +103,7 @@ const feature: FoamFeature = {
|
||||
const templateFilename =
|
||||
(selectedTemplate as QuickPickItem).description ||
|
||||
(selectedTemplate as QuickPickItem).label;
|
||||
const templateUri = URI.joinPath(TEMPLATES_DIR, templateFilename);
|
||||
const templateUri = TEMPLATES_DIR.joinPath(templateFilename);
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../settings';
|
||||
import { isSome } from '../utils';
|
||||
@@ -77,7 +75,7 @@ function generateGraphData(foam: Foam) {
|
||||
|
||||
foam.workspace.list().forEach(n => {
|
||||
const type = n.type === 'note' ? n.properties.type ?? 'note' : n.type;
|
||||
const title = n.type === 'note' ? n.title : path.basename(n.uri.path);
|
||||
const title = n.type === 'note' ? n.title : n.uri.getBasename();
|
||||
graph.nodeInfo[n.uri.path] = {
|
||||
id: n.uri.path,
|
||||
type: type,
|
||||
@@ -92,7 +90,7 @@ function generateGraphData(foam: Foam) {
|
||||
source: c.source.path,
|
||||
target: c.target.path,
|
||||
});
|
||||
if (URI.isPlaceholder(c.target)) {
|
||||
if (c.target.isPlaceholder()) {
|
||||
graph.nodeInfo[c.target.path] = {
|
||||
id: c.target.path,
|
||||
type: 'placeholder',
|
||||
@@ -133,7 +131,7 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async message => {
|
||||
switch (message.type) {
|
||||
case 'webviewDidLoad':
|
||||
case 'webviewDidLoad': {
|
||||
const styles = getGraphStyle();
|
||||
panel.webview.postMessage({
|
||||
type: 'didUpdateStyle',
|
||||
@@ -141,8 +139,8 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
});
|
||||
updateGraph(panel, foam);
|
||||
break;
|
||||
|
||||
case 'webviewDidSelectNode':
|
||||
}
|
||||
case 'webviewDidSelectNode': {
|
||||
const noteUri = vscode.Uri.parse(message.payload);
|
||||
const selectedNote = foam.workspace.get(fromVsCodeUri(noteUri));
|
||||
|
||||
@@ -153,10 +151,11 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
}
|
||||
case 'error': {
|
||||
Logger.error('An error occurred in the graph view', message.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@@ -170,25 +169,27 @@ async function getWebviewContent(
|
||||
context: vscode.ExtensionContext,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const datavizPath = [context.extensionPath, 'static', 'dataviz'];
|
||||
const datavizPath = vscode.Uri.joinPath(
|
||||
vscode.Uri.file(context.extensionPath),
|
||||
'static',
|
||||
'dataviz'
|
||||
);
|
||||
|
||||
const getWebviewUri = (fileName: string) =>
|
||||
panel.webview.asWebviewUri(
|
||||
vscode.Uri.file(path.join(...datavizPath, fileName))
|
||||
);
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizPath, fileName));
|
||||
|
||||
const indexHtml = await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.file(path.join(...datavizPath, 'index.html'))
|
||||
vscode.Uri.joinPath(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="' + getWebviewUri(fileName).toString() + '"';
|
||||
.replace(/data-replace (src|href)="[^"]+"/g, match => {
|
||||
const i = match.indexOf(' ');
|
||||
const j = match.indexOf('=');
|
||||
const uri = getWebviewUri(match.slice(j + 2, -1).trim());
|
||||
return match.slice(i + 1, j) + '="' + uri.toString() + '"';
|
||||
});
|
||||
|
||||
return filled;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
@@ -9,21 +8,15 @@ import {
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Range } from '../core/model/range';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const CONFIG_KEY = 'decorations.links.enable';
|
||||
|
||||
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' },
|
||||
color: { id: 'foam.placeholder' },
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
@@ -32,24 +25,31 @@ const updateDecorations = (
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (!editor || !areDecorationsEnabled()) {
|
||||
if (
|
||||
!editor ||
|
||||
!areDecorationsEnabled() ||
|
||||
editor.document.languageId !== 'markdown'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(editor.document.uri),
|
||||
editor.document.getText()
|
||||
);
|
||||
let linkRanges = [];
|
||||
let placeholderRanges = [];
|
||||
const placeholderRanges = [];
|
||||
note.links.forEach(link => {
|
||||
const linkUri = workspace.resolveLink(note, link);
|
||||
if (URI.isPlaceholder(linkUri)) {
|
||||
placeholderRanges.push(link.range);
|
||||
} else {
|
||||
linkRanges.push(link.range);
|
||||
if (linkUri.isPlaceholder()) {
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + 2,
|
||||
link.range.end.line,
|
||||
link.range.end.character - 2
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
editor.setDecorations(linkDecoration, linkRanges);
|
||||
editor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,6 @@ const feature: FoamFeature = {
|
||||
|
||||
context.subscriptions.push(
|
||||
areDecorationsEnabled,
|
||||
linkDecoration,
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
|
||||
@@ -81,9 +80,9 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
const sources = uniqWith(
|
||||
this.graph
|
||||
.getBacklinks(targetUri)
|
||||
.filter(link => !URI.isEqual(link.source, documentUri))
|
||||
.filter(link => !link.source.isEqual(documentUri))
|
||||
.map(link => link.source),
|
||||
URI.isEqual
|
||||
(u1, u2) => u1.isEqual(u2)
|
||||
);
|
||||
|
||||
const links = sources.slice(0, 10).map(ref => {
|
||||
@@ -101,7 +100,7 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
);
|
||||
|
||||
let mdContent = null;
|
||||
if (!URI.isPlaceholder(targetUri)) {
|
||||
if (!targetUri.isPlaceholder()) {
|
||||
const content: string = await this.workspace.readAsMarkdown(targetUri);
|
||||
|
||||
mdContent = isSome(content)
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
@@ -69,7 +68,7 @@ async function janitor(foam: Foam) {
|
||||
async function runJanitor(foam: Foam) {
|
||||
const notes: Resource[] = foam.workspace
|
||||
.list()
|
||||
.filter(r => URI.isMarkdownFile(r.uri));
|
||||
.filter(r => r.uri.isMarkdown());
|
||||
|
||||
let updatedHeadingCount = 0;
|
||||
let updatedDefinitionListCount = 0;
|
||||
@@ -86,11 +85,11 @@ async function runJanitor(foam: Foam) {
|
||||
);
|
||||
|
||||
const dirtyNotes = notes.filter(note =>
|
||||
dirtyEditorsFileName.includes(URI.toFsPath(note.uri))
|
||||
dirtyEditorsFileName.includes(note.uri.toFsPath())
|
||||
);
|
||||
|
||||
const nonDirtyNotes = notes.filter(
|
||||
note => !dirtyEditorsFileName.includes(URI.toFsPath(note.uri))
|
||||
note => !dirtyEditorsFileName.includes(note.uri.toFsPath())
|
||||
);
|
||||
|
||||
const wikilinkSetting = getWikilinkDefinitionSetting();
|
||||
@@ -98,12 +97,12 @@ async function runJanitor(foam: Foam) {
|
||||
// Apply Text Edits to Non Dirty Notes using fs module just like CLI
|
||||
|
||||
const fileWritePromises = nonDirtyNotes.map(note => {
|
||||
let heading = generateHeading(note);
|
||||
const heading = generateHeading(note);
|
||||
if (heading) {
|
||||
updatedHeadingCount += 1;
|
||||
}
|
||||
|
||||
let definitions =
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
? null
|
||||
: generateLinkReferences(
|
||||
@@ -126,7 +125,7 @@ async function runJanitor(foam: Foam) {
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
|
||||
return fs.promises.writeFile(URI.toFsPath(note.uri), text);
|
||||
return fs.promises.writeFile(note.uri.toFsPath(), text);
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
@@ -136,12 +135,12 @@ async function runJanitor(foam: Foam) {
|
||||
for (const doc of dirtyTextDocuments) {
|
||||
const editor = await window.showTextDocument(doc);
|
||||
const note = dirtyNotes.find(
|
||||
n => URI.toFsPath(n.uri) === editor.document.uri.fsPath
|
||||
n => n.uri.toFsPath() === editor.document.uri.fsPath
|
||||
)!;
|
||||
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
let definitions =
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
? null
|
||||
: generateLinkReferences(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
@@ -9,15 +10,20 @@ import {
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { CompletionProvider } from './link-completion';
|
||||
import {
|
||||
CompletionProvider,
|
||||
SectionCompletionProvider,
|
||||
} from './link-completion';
|
||||
|
||||
describe('Link Completion', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const root = fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri);
|
||||
const ws = new FoamWorkspace();
|
||||
ws.set(
|
||||
createTestNote({
|
||||
root,
|
||||
uri: 'file-name.md',
|
||||
sections: ['Section One', 'Section Two'],
|
||||
})
|
||||
)
|
||||
.set(
|
||||
@@ -82,24 +88,74 @@ describe('Link Completion', () => {
|
||||
});
|
||||
|
||||
it('should return notes with unique identifiers, and placeholders', async () => {
|
||||
const { uri } = await createFile('[[file]] [[');
|
||||
for (const text of ['[[', '[[file]] [[', '[[file]] #tag [[']) {
|
||||
const { uri } = await createFile(text);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, text.length)
|
||||
);
|
||||
|
||||
expect(links.items.length).toEqual(5);
|
||||
expect(new Set(links.items.map(i => i.insertText))).toEqual(
|
||||
new Set([
|
||||
'to/file',
|
||||
'another/file',
|
||||
'File name with spaces',
|
||||
'file-name',
|
||||
'placeholder text',
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return sections for other notes', async () => {
|
||||
for (const text of [
|
||||
'[[file-name#',
|
||||
'[[file]] [[file-name#',
|
||||
'[[file]] #tag [[file-name#',
|
||||
]) {
|
||||
const { uri } = await createFile(text);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, text.length)
|
||||
);
|
||||
|
||||
expect(new Set(links.items.map(i => i.label))).toEqual(
|
||||
new Set(['Section One', 'Section Two'])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return sections within the note', async () => {
|
||||
const { uri, content } = await createFile(`
|
||||
# Section 1
|
||||
|
||||
Content of section 1
|
||||
|
||||
# Section 2
|
||||
|
||||
Content of section 2
|
||||
|
||||
[[#
|
||||
`);
|
||||
ws.set(parser.parse(uri, content));
|
||||
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
const provider = new SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 11)
|
||||
new vscode.Position(9, 3)
|
||||
);
|
||||
|
||||
expect(links.items.length).toEqual(5);
|
||||
expect(new Set(links.items.map(i => i.insertText))).toEqual(
|
||||
new Set([
|
||||
'to/file',
|
||||
'another/file',
|
||||
'File name with spaces',
|
||||
'file-name',
|
||||
'placeholder text',
|
||||
])
|
||||
expect(new Set(links.items.map(i => i.label))).toEqual(
|
||||
new Set(['Section 1', 'Section 2'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const WIKILINK_REGEX = /\[\[[^[\]]*(?!.*\]\])/;
|
||||
export const SECTION_REGEX = /\[\[([^[\]]*#(?!.*\]\]))/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -19,11 +21,74 @@ const feature: FoamFeature = {
|
||||
mdDocSelector,
|
||||
new CompletionProvider(foam.workspace, foam.graph),
|
||||
'['
|
||||
),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new SectionCompletionProvider(foam.workspace),
|
||||
'#'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace) {}
|
||||
|
||||
provideCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.CompletionList<vscode.CompletionItem>> {
|
||||
const cursorPrefix = document
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
|
||||
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
|
||||
const match = cursorPrefix.match(SECTION_REGEX);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resourceId =
|
||||
match[1] === '#' ? fromVsCodeUri(document.uri) : match[1].slice(0, -1);
|
||||
|
||||
const resource = this.ws.find(resourceId);
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
cursorPrefix.lastIndexOf('#') + 1,
|
||||
position.line,
|
||||
position.character
|
||||
);
|
||||
if (resource) {
|
||||
const items = resource.sections.map(b => {
|
||||
const item = new ResourceCompletionItem(
|
||||
b.label,
|
||||
vscode.CompletionItemKind.Text,
|
||||
resource.uri.withFragment(b.label)
|
||||
);
|
||||
item.sortText = String(b.range.start.line).padStart(5, '0');
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
});
|
||||
return new vscode.CompletionList(items);
|
||||
}
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
item: ResourceCompletionItem | vscode.CompletionItem
|
||||
): vscode.ProviderResult<vscode.CompletionItem> {
|
||||
if (item instanceof ResourceCompletionItem) {
|
||||
return this.ws.readAsMarkdown(item.resourceUri).then(text => {
|
||||
item.documentation = getNoteTooltip(text);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
export class CompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
|
||||
@@ -38,22 +103,30 @@ export class CompletionProvider
|
||||
|
||||
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
|
||||
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const requiresAutocomplete = cursorPrefix.match(/\[\[[^\[\]]*(?!.*\]\])/);
|
||||
const requiresAutocomplete = cursorPrefix.match(WIKILINK_REGEX);
|
||||
|
||||
if (!requiresAutocomplete) {
|
||||
if (!requiresAutocomplete || requiresAutocomplete[0].indexOf('#') >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = requiresAutocomplete[0];
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
position.character - (text.length - 2),
|
||||
position.line,
|
||||
position.character
|
||||
);
|
||||
const resources = this.ws.list().map(resource => {
|
||||
const label = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
|
||||
const item = new ResourceCompletionItem(
|
||||
label,
|
||||
vscode.CompletionItemKind.File,
|
||||
resource
|
||||
resource.uri
|
||||
);
|
||||
item.filterText = URI.getBasename(resource.uri);
|
||||
item.filterText = resource.uri.getName();
|
||||
item.insertText = this.ws.getIdentifier(resource.uri);
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = ['#'];
|
||||
return item;
|
||||
});
|
||||
const placeholders = Array.from(this.graph.placeholders.values()).map(
|
||||
@@ -63,6 +136,7 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.Interface
|
||||
);
|
||||
item.insertText = uri.path;
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
}
|
||||
);
|
||||
@@ -74,7 +148,10 @@ export class CompletionProvider
|
||||
item: ResourceCompletionItem | vscode.CompletionItem
|
||||
): vscode.ProviderResult<vscode.CompletionItem> {
|
||||
if (item instanceof ResourceCompletionItem) {
|
||||
item.documentation = getNoteTooltip(item.resource.source.text);
|
||||
return this.ws.readAsMarkdown(item.resourceUri).then(text => {
|
||||
item.documentation = getNoteTooltip(text);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -87,7 +164,7 @@ class ResourceCompletionItem extends vscode.CompletionItem {
|
||||
constructor(
|
||||
label: string,
|
||||
type: vscode.CompletionItemKind,
|
||||
public resource: Resource
|
||||
public resourceUri: URI
|
||||
) {
|
||||
super(label, type);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Document navigation', () => {
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileA.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 28));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 26));
|
||||
});
|
||||
|
||||
it('should create links for placeholders', async () => {
|
||||
@@ -87,7 +87,7 @@ describe('Document navigation', () => {
|
||||
expect(links[0].target).toEqual(
|
||||
OPEN_COMMAND.asURI(URI.placeholder('a placeholder'))
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 35));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -232,6 +232,6 @@ describe('Document navigation', () => {
|
||||
range: new vscode.Range(0, 23, 0, 23 + 9),
|
||||
});
|
||||
});
|
||||
it('should provide references for placeholders', async () => {});
|
||||
// it('should provide references for placeholders', async () => {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { OPEN_COMMAND } from './utility-commands';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { ResourceLink, ResourceParser } from '../core/model/note';
|
||||
import { Resource, ResourceLink, ResourceParser } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
@@ -70,7 +70,10 @@ export class NavigationProvider
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.Location[]> {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
const targetLink: ResourceLink | undefined = resource.links.find(link =>
|
||||
Range.containsPosition(link.range, position)
|
||||
);
|
||||
@@ -95,7 +98,10 @@ export class NavigationProvider
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): vscode.LocationLink[] {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
const targetLink: ResourceLink | undefined = resource.links.find(link =>
|
||||
Range.containsPosition(link.range, position)
|
||||
);
|
||||
@@ -104,26 +110,26 @@ export class NavigationProvider
|
||||
}
|
||||
|
||||
const uri = this.workspace.resolveLink(resource, targetLink);
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetResource = this.workspace.get(uri);
|
||||
|
||||
let targetRange = Range.createFromPosition(
|
||||
targetResource.source.contentStart,
|
||||
targetResource.source.end
|
||||
);
|
||||
const section = Resource.findSection(targetResource, uri.fragment);
|
||||
if (section) {
|
||||
targetRange = section.range;
|
||||
}
|
||||
const result: vscode.LocationLink = {
|
||||
originSelectionRange: toVsCodeRange(targetLink.range),
|
||||
targetUri: toVsCodeUri(uri),
|
||||
targetRange: toVsCodeRange(
|
||||
Range.createFromPosition(
|
||||
targetResource.source.contentStart,
|
||||
targetResource.source.end
|
||||
)
|
||||
),
|
||||
targetRange: toVsCodeRange(targetRange),
|
||||
targetSelectionRange: toVsCodeRange(
|
||||
Range.createFromPosition(
|
||||
targetResource.source.contentStart,
|
||||
targetResource.source.contentStart
|
||||
)
|
||||
Range.createFromPosition(targetRange.start, targetRange.start)
|
||||
),
|
||||
};
|
||||
return [result];
|
||||
@@ -135,7 +141,10 @@ export class NavigationProvider
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument
|
||||
): vscode.DocumentLink[] {
|
||||
const resource = this.parser.parse(document.uri, document.getText());
|
||||
const resource = this.parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
document.getText()
|
||||
);
|
||||
|
||||
const targets: { link: ResourceLink; target: URI }[] = resource.links.map(
|
||||
link => ({
|
||||
@@ -145,14 +154,19 @@ export class NavigationProvider
|
||||
);
|
||||
|
||||
return targets.map(o => {
|
||||
const command = OPEN_COMMAND.asURI(toVsCodeUri(o.target));
|
||||
const command = OPEN_COMMAND.asURI(o.target);
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
toVsCodeRange(o.link.range),
|
||||
new vscode.Range(
|
||||
o.link.range.start.line,
|
||||
o.link.range.start.character + 2,
|
||||
o.link.range.end.line,
|
||||
o.link.range.end.character - 2
|
||||
),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = URI.isPlaceholder(o.target)
|
||||
documentLink.tooltip = o.target.isPlaceholder()
|
||||
? `Create note for '${o.target.path}'`
|
||||
: `Go to ${URI.toFsPath(o.target)}`;
|
||||
: `Go to ${o.target.toFsPath()}`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { focusNote } from '../utils';
|
||||
@@ -10,9 +9,7 @@ const feature: FoamFeature = {
|
||||
commands.registerCommand('foam-vscode.open-random-note', async () => {
|
||||
const foam = await foamPromise;
|
||||
const currentFile = window.activeTextEditor?.document.uri.path;
|
||||
const notes = foam.workspace
|
||||
.list()
|
||||
.filter(r => URI.isMarkdownFile(r.uri));
|
||||
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
|
||||
if (notes.length <= 1) {
|
||||
window.showInformationMessage(
|
||||
'Could not find another note to open. If you believe this is a bug, please file an issue.'
|
||||
|
||||
@@ -29,7 +29,7 @@ const feature: FoamFeature = {
|
||||
workspacesURIs,
|
||||
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
|
||||
uri => {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
|
||||
@@ -30,7 +30,7 @@ const feature: FoamFeature = {
|
||||
.getAllNodes()
|
||||
.filter(uri => isPlaceholderResource(uri, foam.workspace)),
|
||||
uri => {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
@@ -54,7 +54,7 @@ const feature: FoamFeature = {
|
||||
export default feature;
|
||||
|
||||
export function isPlaceholderResource(uri: URI, workspace: FoamWorkspace) {
|
||||
if (URI.isPlaceholder(uri)) {
|
||||
if (uri.isPlaceholder()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { createMarkdownParser } from '../core/markdown-provider';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { getUriInWorkspace } from '../test/test-utils-vscode';
|
||||
import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
} from '../test/test-utils-vscode';
|
||||
import {
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithFoamTags,
|
||||
@@ -39,12 +44,7 @@ describe('Link generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Stylable tag generation in preview', () => {
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
title: 'Note B',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteB);
|
||||
const md = markdownItWithFoamTags(MarkdownIt(), ws);
|
||||
const md = markdownItWithFoamTags(MarkdownIt(), new FoamWorkspace());
|
||||
|
||||
it('transforms a string containing multiple tags to a stylable html element', () => {
|
||||
expect(md.render(`Lorem #ipsum dolor #sit`)).toMatch(
|
||||
@@ -60,25 +60,14 @@ describe('Stylable tag generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const noteC = createTestNote({
|
||||
uri: 'note-c.md',
|
||||
text: 'This is the text of note C which includes ![[note-d]]',
|
||||
});
|
||||
const noteD = createTestNote({
|
||||
uri: 'note-d.md',
|
||||
text: 'This is the text of note D which includes ![[note-c]]',
|
||||
});
|
||||
const ws = new FoamWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteC)
|
||||
.set(noteD);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
it('should render an included note', () => {
|
||||
const note = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(note);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
it('renders an included note', () => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
@@ -90,20 +79,62 @@ describe('Displaying included notes in preview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the syntax when a note is not found', () => {
|
||||
it('should render an included section', async () => {
|
||||
// here we use createFile as the test note doesn't fill in
|
||||
// all the metadata we need
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note D
|
||||
|
||||
# Section 2
|
||||
This is the second section of note D
|
||||
|
||||
# Section 3
|
||||
This is the third section of note D
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
![[note-b]]`)
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 2]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.
|
||||
![[note-b]]</p>
|
||||
`
|
||||
`<p>This is the root node.</p>
|
||||
<p><h1>Section 2</h1>
|
||||
<p>This is the second section of note D</p>
|
||||
</p>`
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), new FoamWorkspace());
|
||||
|
||||
expect(md.render(`This is the root node. ![[non-existing-note]]`)).toMatch(
|
||||
`<p>This is the root node. ![[non-existing-note]]</p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a warning in case of cyclical inclusions', () => {
|
||||
expect(md.render(noteD.source.text)).toMatch(
|
||||
`<p>This is the text of note D which includes <p>This is the text of note C which includes <p>This is the text of note D which includes <div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: note-c</div></p>
|
||||
it('should display a warning in case of cyclical inclusions', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A which includes ![[note-b]]',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
text: 'This is the text of note B which includes ![[note-a]]',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(md.render(noteB.source.text)).toMatch(
|
||||
`<p>This is the text of note B which includes <p>This is the text of note A which includes <p>This is the text of note B which includes <div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: note-a</div></p>
|
||||
</p>
|
||||
</p>
|
||||
`
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNone } from '../utils';
|
||||
import { isNone, isSome } from '../utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
const refsStack: string[] = [];
|
||||
@@ -45,21 +46,32 @@ export const markdownItWithNoteInclusion = (
|
||||
return `![[${wikilink}]]`;
|
||||
}
|
||||
|
||||
const cyclicLinkDetected = refsStack.includes(wikilink);
|
||||
const cyclicLinkDetected = refsStack.includes(
|
||||
includedNote.uri.path.toLocaleLowerCase()
|
||||
);
|
||||
|
||||
if (!cyclicLinkDetected) {
|
||||
refsStack.push(wikilink.toLowerCase());
|
||||
refsStack.push(includedNote.uri.path.toLocaleLowerCase());
|
||||
}
|
||||
|
||||
const html = cyclicLinkDetected
|
||||
? `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`
|
||||
: md.render(includedNote.source.text);
|
||||
|
||||
if (!cyclicLinkDetected) {
|
||||
if (cyclicLinkDetected) {
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
} else {
|
||||
let content = includedNote.source.text;
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
}
|
||||
|
||||
return html;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
|
||||
@@ -79,4 +79,18 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a wikilink', async () => {
|
||||
const { uri } = await createFile('[[#prim');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 7)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { SECTION_REGEX } from './link-completion';
|
||||
|
||||
export const TAG_REGEX = /#(.*)/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -32,7 +35,8 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(/#(.*)/);
|
||||
const requiresAutocomplete =
|
||||
cursorPrefix.match(TAG_REGEX) && !cursorPrefix.match(SECTION_REGEX);
|
||||
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
|
||||
@@ -62,6 +62,7 @@ describe('Tags tree panel', () => {
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).toEqual('child');
|
||||
}
|
||||
});
|
||||
@@ -94,7 +95,9 @@ describe('Tags tree panel', () => {
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(['child', 'subchild']).toContain(child.title);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).not.toEqual('parent');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,54 +1,66 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { NoteFactory } from '../services/templates';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
export const OPEN_COMMAND = {
|
||||
command: 'foam-vscode.open-resource',
|
||||
title: 'Foam: Open Resource',
|
||||
|
||||
execute: async (params: { uri: URI }) => {
|
||||
const { uri } = params;
|
||||
switch (uri.scheme) {
|
||||
case 'file':
|
||||
return vscode.commands.executeCommand('vscode.open', toVsCodeUri(uri));
|
||||
|
||||
case 'placeholder':
|
||||
const title = uri.path.split('/').slice(-1)[0];
|
||||
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri)
|
||||
: fromVsCodeUri(vscode.window.activeTextEditor?.document.uri)
|
||||
? URI.getDir(
|
||||
fromVsCodeUri(vscode.window.activeTextEditor!.document.uri)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = URI.createResourceUriFromPlaceholder(basedir, uri);
|
||||
|
||||
await NoteFactory.createForPlaceholderWikilink(title, target);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
asURI: (uri: URI) =>
|
||||
vscode.Uri.parse(`command:${OPEN_COMMAND.command}`).with({
|
||||
query: encodeURIComponent(JSON.stringify({ uri: URI.create(uri) })),
|
||||
query: encodeURIComponent(JSON.stringify({ uri })),
|
||||
}),
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext) => {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
OPEN_COMMAND.command,
|
||||
OPEN_COMMAND.execute
|
||||
async (params: { uri: URI }) => {
|
||||
const uri = new URI(params.uri);
|
||||
switch (uri.scheme) {
|
||||
case 'file': {
|
||||
let selection = new vscode.Range(1, 0, 1, 0);
|
||||
if (uri.fragment) {
|
||||
const foam = await foamPromise;
|
||||
const resource = foam.workspace.get(uri);
|
||||
const section = Resource.findSection(resource, uri.fragment);
|
||||
if (section) {
|
||||
selection = toVsCodeRange(section.range);
|
||||
}
|
||||
}
|
||||
const targetUri =
|
||||
uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(uri);
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri, {
|
||||
selection: selection,
|
||||
});
|
||||
}
|
||||
case 'placeholder': {
|
||||
const basedir =
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
? vscode.workspace.workspaceFolders[0].uri
|
||||
: vscode.window.activeTextEditor?.document.uri
|
||||
? vscode.window.activeTextEditor!.document.uri
|
||||
: undefined;
|
||||
if (basedir === undefined) {
|
||||
return;
|
||||
}
|
||||
const title = uri.getName();
|
||||
const target = fromVsCodeUri(basedir)
|
||||
.resolve(uri, true)
|
||||
.changeExtension('', '.md');
|
||||
await NoteFactory.createForPlaceholderWikilink(title, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { updateDiagnostics } from './wikilink-diagnostics';
|
||||
|
||||
describe('Wikilink diagnostics', () => {
|
||||
@@ -15,12 +16,8 @@ describe('Wikilink diagnostics', () => {
|
||||
await closeEditors();
|
||||
});
|
||||
it('should show no warnings when there are no conflicts', async () => {
|
||||
const fileA = await createFile('This is the todo file', [
|
||||
'project',
|
||||
'car',
|
||||
'todo.md',
|
||||
]);
|
||||
const fileB = await createFile('This is linked to [[todo]]');
|
||||
const fileA = await createFile('This is the todo file');
|
||||
const fileB = await createFile(`This is linked to [[${fileA.name}]]`);
|
||||
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace()
|
||||
@@ -106,14 +103,95 @@ describe('Wikilink diagnostics', () => {
|
||||
expect(items[0].severity).toEqual(vscode.DiagnosticSeverity.Warning);
|
||||
expect(
|
||||
items[0].relatedInformation.map(info => info.location.uri.path)
|
||||
).toEqual([fileA.uri.path, fileB.uri.path]);
|
||||
).toEqual([fileB.uri.path, fileA.uri.path]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Section diagnostics', () => {
|
||||
it('should show nothing on placeholders', async () => {
|
||||
const file = await createFile('Link to [[placeholder]]');
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(file.uri, file.content));
|
||||
|
||||
await showInEditor(file.uri);
|
||||
|
||||
const collection = vscode.languages.createDiagnosticCollection('foam-test');
|
||||
updateDiagnostics(
|
||||
ws,
|
||||
parser,
|
||||
vscode.window.activeTextEditor.document,
|
||||
collection
|
||||
);
|
||||
expect(countEntries(collection)).toEqual(0);
|
||||
});
|
||||
it('should show nothing when the section is correct', async () => {
|
||||
const fileA = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
Content of section 1
|
||||
|
||||
# Section 2
|
||||
Content of section 2
|
||||
`,
|
||||
['my-file.md']
|
||||
);
|
||||
const fileB = await createFile('Link to [[my-file#Section 1]]');
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
|
||||
await showInEditor(fileB.uri);
|
||||
|
||||
const collection = vscode.languages.createDiagnosticCollection('foam-test');
|
||||
updateDiagnostics(
|
||||
ws,
|
||||
parser,
|
||||
vscode.window.activeTextEditor.document,
|
||||
collection
|
||||
);
|
||||
expect(countEntries(collection)).toEqual(0);
|
||||
});
|
||||
it('should show a warning when the section name is incorrect', async () => {
|
||||
const fileA = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
Content of section 1
|
||||
|
||||
# Section 2
|
||||
Content of section 2
|
||||
`
|
||||
);
|
||||
const fileB = await createFile(`Link to [[${fileA.name}#Section 10]]`);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(fileA.uri, fileA.content))
|
||||
.set(parser.parse(fileB.uri, fileB.content));
|
||||
|
||||
await showInEditor(fileB.uri);
|
||||
|
||||
const collection = vscode.languages.createDiagnosticCollection('foam-test');
|
||||
updateDiagnostics(
|
||||
ws,
|
||||
parser,
|
||||
vscode.window.activeTextEditor.document,
|
||||
collection
|
||||
);
|
||||
expect(countEntries(collection)).toEqual(1);
|
||||
const items = collection.get(toVsCodeUri(fileB.uri));
|
||||
expect(items[0].range).toEqual(new vscode.Range(0, 15, 0, 28));
|
||||
expect(items[0].severity).toEqual(vscode.DiagnosticSeverity.Warning);
|
||||
expect(items[0].relatedInformation.map(info => info.message)).toEqual([
|
||||
'Section 1',
|
||||
'Section 2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const countEntries = (collection: vscode.DiagnosticCollection): number => {
|
||||
let count = 0;
|
||||
collection.forEach(i => {
|
||||
count++;
|
||||
collection.forEach((i, diagnostics) => {
|
||||
count += diagnostics.length;
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
import { Resource, ResourceParser } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getShortestIdentifier } from '../core/utils';
|
||||
import { FoamFeature } from '../types';
|
||||
import { fromVsCodeUri, toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { isNone } from '../utils';
|
||||
import {
|
||||
fromVsCodeUri,
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../utils/vsc-utils';
|
||||
|
||||
const AMBIGUOUS_IDENTIFIER_CODE = 'ambiguous-identifier';
|
||||
const UNKNOWN_SECTION_CODE = 'unknown-section';
|
||||
|
||||
interface FoamCommand<T> {
|
||||
name: string;
|
||||
@@ -24,7 +31,7 @@ const FIND_IDENTIFER_COMMAND: FoamCommand<FindIdentifierCommandArgs> = {
|
||||
name: 'foam:compute-identifier',
|
||||
execute: async ({ target, amongst, range }) => {
|
||||
if (vscode.window.activeTextEditor) {
|
||||
let identifier = getShortestIdentifier(
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
target.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
@@ -33,13 +40,27 @@ const FIND_IDENTIFER_COMMAND: FoamCommand<FindIdentifierCommandArgs> = {
|
||||
? identifier.slice(0, -3)
|
||||
: identifier;
|
||||
|
||||
vscode.window.activeTextEditor.edit(builder => {
|
||||
await vscode.window.activeTextEditor.edit(builder => {
|
||||
builder.replace(range, identifier);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
interface ReplaceTextCommandArgs {
|
||||
range: vscode.Range;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const REPLACE_TEXT_COMMAND: FoamCommand<ReplaceTextCommandArgs> = {
|
||||
name: 'foam:replace-text',
|
||||
execute: async ({ range, value }) => {
|
||||
await vscode.window.activeTextEditor.edit(builder => {
|
||||
builder.replace(range, value);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -85,6 +106,10 @@ const feature: FoamFeature = {
|
||||
vscode.commands.registerCommand(
|
||||
FIND_IDENTIFER_COMMAND.name,
|
||||
FIND_IDENTIFER_COMMAND.execute
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
REPLACE_TEXT_COMMAND.name,
|
||||
REPLACE_TEXT_COMMAND.execute
|
||||
)
|
||||
);
|
||||
},
|
||||
@@ -97,6 +122,7 @@ export function updateDiagnostics(
|
||||
collection: vscode.DiagnosticCollection
|
||||
): void {
|
||||
collection.clear();
|
||||
const result = [];
|
||||
if (document && document.languageId === 'markdown') {
|
||||
const resource = parser.parse(
|
||||
fromVsCodeUri(document.uri),
|
||||
@@ -105,32 +131,62 @@ export function updateDiagnostics(
|
||||
|
||||
for (const link of resource.links) {
|
||||
if (link.type === 'wikilink') {
|
||||
const targets = workspace.listById(link.target);
|
||||
const [target, section] = link.target.split('#');
|
||||
const targets = workspace.listByIdentifier(target);
|
||||
if (targets.length > 1) {
|
||||
collection.set(document.uri, [
|
||||
{
|
||||
code: AMBIGUOUS_IDENTIFIER_CODE,
|
||||
message: 'Resource identifier is ambiguous',
|
||||
range: toVsCodeRange(link.range),
|
||||
result.push({
|
||||
code: AMBIGUOUS_IDENTIFIER_CODE,
|
||||
message: 'Resource identifier is ambiguous',
|
||||
range: toVsCodeRange(link.range),
|
||||
severity: vscode.DiagnosticSeverity.Warning,
|
||||
source: 'Foam',
|
||||
relatedInformation: targets.map(
|
||||
t =>
|
||||
new vscode.DiagnosticRelatedInformation(
|
||||
new vscode.Location(
|
||||
toVsCodeUri(t.uri),
|
||||
new vscode.Position(0, 0)
|
||||
),
|
||||
`Possible target: ${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(t.uri)
|
||||
)}`
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
if (section && targets.length === 1) {
|
||||
const resource = targets[0];
|
||||
if (isNone(Resource.findSection(resource, section))) {
|
||||
const range = Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + target.length + 2,
|
||||
link.range.end.line,
|
||||
link.range.end.character
|
||||
);
|
||||
result.push({
|
||||
code: UNKNOWN_SECTION_CODE,
|
||||
message: `Cannot find section "${section}" in document, available sections are:`,
|
||||
range: toVsCodeRange(range),
|
||||
severity: vscode.DiagnosticSeverity.Warning,
|
||||
source: 'Foam',
|
||||
relatedInformation: targets.map(
|
||||
t =>
|
||||
relatedInformation: resource.sections.map(
|
||||
b =>
|
||||
new vscode.DiagnosticRelatedInformation(
|
||||
new vscode.Location(
|
||||
toVsCodeUri(t.uri),
|
||||
new vscode.Position(0, 0)
|
||||
toVsCodeUri(resource.uri),
|
||||
toVsCodePosition(b.range.start)
|
||||
),
|
||||
`Possible target: ${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(t.uri)
|
||||
)}`
|
||||
b.label
|
||||
)
|
||||
),
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.length > 0) {
|
||||
collection.set(document.uri, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,50 +201,88 @@ export class IdentifierResolver implements vscode.CodeActionProvider {
|
||||
context: vscode.CodeActionContext,
|
||||
token: vscode.CancellationToken
|
||||
): vscode.CodeAction[] {
|
||||
return context.diagnostics
|
||||
.filter(diagnostic => diagnostic.code === AMBIGUOUS_IDENTIFIER_CODE)
|
||||
.reduce((acc, diagnostic) => {
|
||||
return context.diagnostics.reduce((acc, diagnostic) => {
|
||||
if (diagnostic.code === AMBIGUOUS_IDENTIFIER_CODE) {
|
||||
const res: vscode.CodeAction[] = [];
|
||||
const uris = diagnostic.relatedInformation.map(
|
||||
info => info.location.uri
|
||||
);
|
||||
for (const item of diagnostic.relatedInformation) {
|
||||
res.push(
|
||||
this.createCommandCodeAction(diagnostic, item.location.uri, uris)
|
||||
createFindIdentifierCommand(diagnostic, item.location.uri, uris)
|
||||
);
|
||||
}
|
||||
return [...acc, ...res];
|
||||
}, [] as vscode.CodeAction[]);
|
||||
}
|
||||
|
||||
private createCommandCodeAction(
|
||||
diagnostic: vscode.Diagnostic,
|
||||
target: vscode.Uri,
|
||||
possibleTargets: vscode.Uri[]
|
||||
): vscode.CodeAction {
|
||||
const action = new vscode.CodeAction(
|
||||
`Use ${vscode.workspace.asRelativePath(target)}`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
command: FIND_IDENTIFER_COMMAND.name,
|
||||
title: 'Link to this resource',
|
||||
arguments: [
|
||||
{
|
||||
target: target,
|
||||
amongst: possibleTargets,
|
||||
range: new vscode.Range(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character + 2,
|
||||
diagnostic.range.end.line,
|
||||
diagnostic.range.end.character - 2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
action.diagnostics = [diagnostic];
|
||||
return action;
|
||||
}
|
||||
if (diagnostic.code === UNKNOWN_SECTION_CODE) {
|
||||
const res: vscode.CodeAction[] = [];
|
||||
const sections = diagnostic.relatedInformation.map(
|
||||
info => info.message
|
||||
);
|
||||
for (const section of sections) {
|
||||
res.push(createReplaceSectionCommand(diagnostic, section));
|
||||
}
|
||||
return [...acc, ...res];
|
||||
}
|
||||
return acc;
|
||||
}, [] as vscode.CodeAction[]);
|
||||
}
|
||||
}
|
||||
|
||||
const createReplaceSectionCommand = (
|
||||
diagnostic: vscode.Diagnostic,
|
||||
section: string
|
||||
): vscode.CodeAction => {
|
||||
const action = new vscode.CodeAction(
|
||||
`Use ${section}`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
command: REPLACE_TEXT_COMMAND.name,
|
||||
title: `Use section ${section}`,
|
||||
arguments: [
|
||||
{
|
||||
value: section,
|
||||
range: new vscode.Range(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character + 1,
|
||||
diagnostic.range.end.line,
|
||||
diagnostic.range.end.character - 2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
action.diagnostics = [diagnostic];
|
||||
return action;
|
||||
};
|
||||
|
||||
const createFindIdentifierCommand = (
|
||||
diagnostic: vscode.Diagnostic,
|
||||
target: vscode.Uri,
|
||||
possibleTargets: vscode.Uri[]
|
||||
): vscode.CodeAction => {
|
||||
const action = new vscode.CodeAction(
|
||||
`Use ${vscode.workspace.asRelativePath(target)}`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
command: FIND_IDENTIFER_COMMAND.name,
|
||||
title: 'Link to this resource',
|
||||
arguments: [
|
||||
{
|
||||
target: target,
|
||||
amongst: possibleTargets,
|
||||
range: new vscode.Range(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character + 2,
|
||||
diagnostic.range.end.line,
|
||||
diagnostic.range.end.character - 2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
action.diagnostics = [diagnostic];
|
||||
return action;
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -61,7 +61,7 @@ const feature: FoamFeature = {
|
||||
// when a file is created as a result of peekDefinition
|
||||
// action on a wikilink, add definition update references
|
||||
foam.workspace.onDidAdd(_ => {
|
||||
let editor = window.activeTextEditor;
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
}
|
||||
@@ -79,13 +79,13 @@ function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
|
||||
}
|
||||
|
||||
async function createReferenceList(foam: FoamWorkspace) {
|
||||
let editor = window.activeTextEditor;
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let refs = await generateReferenceList(foam, editor.document);
|
||||
const refs = await generateReferenceList(foam, editor.document);
|
||||
if (refs && refs.length) {
|
||||
await editor.edit(function(editBuilder) {
|
||||
if (editor) {
|
||||
@@ -213,7 +213,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
|
||||
): CodeLens[] | Thenable<CodeLens[]> {
|
||||
loadDocConfig();
|
||||
|
||||
let range = detectReferenceListRange(document);
|
||||
const range = detectReferenceListRange(document);
|
||||
if (!range) {
|
||||
return [];
|
||||
}
|
||||
@@ -222,7 +222,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
|
||||
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
|
||||
const newRefs = refs.join(docConfig.eol);
|
||||
|
||||
let status = oldRefs === newRefs ? 'up to date' : 'out of date';
|
||||
const status = oldRefs === newRefs ? 'up to date' : 'out of date';
|
||||
|
||||
return [
|
||||
new CodeLens(range, {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Selection, workspace } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import {
|
||||
closeEditors,
|
||||
@@ -17,7 +16,7 @@ describe('Editor utils', () => {
|
||||
const file = await createFile('this is the file content.');
|
||||
await showInEditor(file.uri);
|
||||
|
||||
expect(getCurrentEditorDirectory()).toEqual(URI.getDir(file.uri));
|
||||
expect(getCurrentEditorDirectory()).toEqual(file.uri.getDirectory());
|
||||
});
|
||||
|
||||
it('should return the directory of the workspace folder if no editor is open', async () => {
|
||||
|
||||
@@ -71,11 +71,11 @@ export async function replaceSelection(
|
||||
* @returns URI
|
||||
* @throws Error if no file is open in editor AND no workspace folder defined
|
||||
*/
|
||||
export function getCurrentEditorDirectory() {
|
||||
export function getCurrentEditorDirectory(): URI {
|
||||
const uri = window.activeTextEditor?.document?.uri;
|
||||
|
||||
if (isSome(uri)) {
|
||||
return URI.getDir(fromVsCodeUri(uri));
|
||||
return fromVsCodeUri(uri).getDirectory();
|
||||
}
|
||||
|
||||
if (workspace.workspaceFolders.length > 0) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Selection, ViewColumn, window, workspace } from 'vscode';
|
||||
import path from 'path';
|
||||
import { isWindows } from '../utils';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { isWindows } from '../core/common/platform';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { determineNewNoteFilepath, NoteFactory } from '../services/templates';
|
||||
import {
|
||||
@@ -43,7 +41,8 @@ describe('Create note from template', () => {
|
||||
})
|
||||
);
|
||||
|
||||
await deleteFile(fileA.uri);
|
||||
await deleteFile(fileA);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should not ask a user for path if defined in template', async () => {
|
||||
@@ -51,7 +50,7 @@ describe('Create note from template', () => {
|
||||
const templateA = await createFile(
|
||||
`---
|
||||
foam_template: # foam template metadata
|
||||
filepath: "${URI.toFsPath(uri)}"
|
||||
filepath: "${uri.toFsPath()}"
|
||||
---
|
||||
`,
|
||||
['.foam', 'templates', 'template-with-path.md']
|
||||
@@ -65,6 +64,9 @@ foam_template: # foam template metadata
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await deleteFile(uri);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should focus the editor on the newly created note', async () => {
|
||||
@@ -84,6 +86,7 @@ foam_template: # foam template metadata
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,8 +107,9 @@ foam_template: # foam template metadata
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
`${new Date().getFullYear()}`
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(template.uri);
|
||||
await deleteFile(template);
|
||||
});
|
||||
|
||||
describe('Creation with active text selection', () => {
|
||||
@@ -121,9 +125,13 @@ foam_template: # foam template metadata
|
||||
const target = getUriInWorkspace();
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
await NoteFactory.createFromTemplate(templateA.uri, resolver, target);
|
||||
expect(await resolver.resolve('FOAM_SELECTED_TEXT')).toEqual(
|
||||
expect(await resolver.resolveFromName('FOAM_SELECTED_TEXT')).toEqual(
|
||||
'first file'
|
||||
);
|
||||
|
||||
await deleteFile(templateA);
|
||||
await deleteFile(target);
|
||||
await deleteFile(file);
|
||||
});
|
||||
|
||||
it('should open created note in a new column if there was a selection', async () => {
|
||||
@@ -148,7 +156,9 @@ foam_template: # foam template metadata
|
||||
expect(fromVsCodeUri(window.visibleTextEditors[1].document.uri)).toEqual(
|
||||
target
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(templateA);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
@@ -171,7 +181,7 @@ foam_template: # foam template metadata
|
||||
'Hello World World'
|
||||
);
|
||||
expect(window.visibleTextEditors[0].document.getText()).toEqual(
|
||||
`This is my first file: [[${URI.getBasename(target)}]]`
|
||||
`This is my first file: [[${target.getName()}]]`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -190,13 +200,13 @@ describe('determineNewNoteFilepath', () => {
|
||||
undefined,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(URI.toFsPath(winResult)).toMatch(winAbsolutePath);
|
||||
expect(winResult.toFsPath()).toMatch(winAbsolutePath);
|
||||
const linuxResult = await determineNewNoteFilepath(
|
||||
linuxAbsolutePath,
|
||||
undefined,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(URI.toFsPath(linuxResult)).toMatch(linuxAbsolutePath);
|
||||
expect(linuxResult.toFsPath()).toMatch(linuxAbsolutePath);
|
||||
});
|
||||
|
||||
it('should compute the relative template filepath from the current directory', async () => {
|
||||
@@ -208,11 +218,10 @@ describe('determineNewNoteFilepath', () => {
|
||||
undefined,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
const expectedPath = path.join(
|
||||
URI.toFsPath(fromVsCodeUri(workspace.workspaceFolders[0].uri)),
|
||||
relativePath
|
||||
);
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(expectedPath);
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(relativePath);
|
||||
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
|
||||
});
|
||||
|
||||
it('should use the note title if nothing else is available', async () => {
|
||||
@@ -222,11 +231,10 @@ describe('determineNewNoteFilepath', () => {
|
||||
undefined,
|
||||
new Resolver(new Map().set('FOAM_TITLE', noteTitle), new Date())
|
||||
);
|
||||
const expectedPath = path.join(
|
||||
URI.toFsPath(fromVsCodeUri(workspace.workspaceFolders[0].uri)),
|
||||
`${noteTitle}.md`
|
||||
);
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(expectedPath);
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(`${noteTitle}.md`);
|
||||
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
|
||||
});
|
||||
|
||||
it('should ask the user for a note title if nothing else is available', async () => {
|
||||
@@ -239,11 +247,10 @@ describe('determineNewNoteFilepath', () => {
|
||||
undefined,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
const expectedPath = path.join(
|
||||
URI.toFsPath(fromVsCodeUri(workspace.workspaceFolders[0].uri)),
|
||||
`${noteTitle}.md`
|
||||
);
|
||||
const expectedPath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(`${noteTitle}.md`);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(expectedPath);
|
||||
expect(resultFilepath.toFsPath()).toMatch(expectedPath.toFsPath());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { existsSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isAbsolute } from 'path';
|
||||
import { TextEncoder } from 'util';
|
||||
import { SnippetString, ViewColumn, window, workspace } from 'vscode';
|
||||
import { focusNote } from '../utils';
|
||||
@@ -19,24 +17,19 @@ import { Resolver } from './variable-resolver';
|
||||
/**
|
||||
* The templates directory
|
||||
*/
|
||||
export const TEMPLATES_DIR = URI.joinPath(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri),
|
||||
'.foam',
|
||||
'templates'
|
||||
);
|
||||
export const TEMPLATES_DIR = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath('.foam', 'templates');
|
||||
|
||||
/**
|
||||
* The URI of the default template
|
||||
*/
|
||||
export const DEFAULT_TEMPLATE_URI = URI.joinPath(TEMPLATES_DIR, 'new-note.md');
|
||||
export const DEFAULT_TEMPLATE_URI = TEMPLATES_DIR.joinPath('new-note.md');
|
||||
|
||||
/**
|
||||
* The URI of the template for daily notes
|
||||
*/
|
||||
export const DAILY_NOTE_TEMPLATE_URI = URI.joinPath(
|
||||
TEMPLATES_DIR,
|
||||
'daily-note.md'
|
||||
);
|
||||
export const DAILY_NOTE_TEMPLATE_URI = TEMPLATES_DIR.joinPath('daily-note.md');
|
||||
|
||||
const WIKILINK_DEFAULT_TEMPLATE_TEXT = `# $\{1:$FOAM_TITLE}\n\n$0`;
|
||||
|
||||
@@ -79,9 +72,8 @@ export async function getTemplates(): Promise<URI[]> {
|
||||
export const NoteFactory = {
|
||||
/**
|
||||
* Creates a new note using a template.
|
||||
* @param givenValues already resolved values of Foam template variables. These are used instead of resolving the Foam template variables.
|
||||
* @param extraVariablesToResolve Foam template variables to resolve, in addition to those mentioned in the template.
|
||||
* @param templateUri the URI of the template to use.
|
||||
* @param resolver the Resolver to use.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use if the template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
@@ -89,9 +81,9 @@ export const NoteFactory = {
|
||||
templateUri: URI,
|
||||
resolver: Resolver,
|
||||
filepathFallbackURI?: URI,
|
||||
templateFallbackText: string = ''
|
||||
templateFallbackText = ''
|
||||
): Promise<void> => {
|
||||
const templateText = existsSync(URI.toFsPath(templateUri))
|
||||
const templateText = existsSync(templateUri.toFsPath())
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
@@ -99,12 +91,13 @@ export const NoteFactory = {
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content ?? '');
|
||||
if (selectedContent?.content) {
|
||||
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content);
|
||||
}
|
||||
|
||||
let templateWithResolvedVariables: string;
|
||||
try {
|
||||
[, templateWithResolvedVariables] = await resolver.resolveText(
|
||||
templateText
|
||||
);
|
||||
templateWithResolvedVariables = await resolver.resolveText(templateText);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
@@ -126,8 +119,8 @@ export const NoteFactory = {
|
||||
resolver
|
||||
);
|
||||
|
||||
if (existsSync(URI.toFsPath(filepath))) {
|
||||
const filename = path.basename(filepath.path);
|
||||
if (existsSync(filepath.toFsPath())) {
|
||||
const filename = filepath.getBasename();
|
||||
const newFilepath = await askUserForFilepathConfirmation(
|
||||
filepath,
|
||||
filename
|
||||
@@ -146,7 +139,7 @@ export const NoteFactory = {
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
const newNoteTitle = URI.getFileNameWithoutExtension(filepath);
|
||||
const newNoteTitle = filepath.getName();
|
||||
|
||||
await replaceSelection(
|
||||
selectedContent.document,
|
||||
@@ -166,11 +159,7 @@ export const NoteFactory = {
|
||||
templateFallbackText: string,
|
||||
targetDate: Date
|
||||
): Promise<void> => {
|
||||
const resolver = new Resolver(
|
||||
new Map(),
|
||||
targetDate,
|
||||
new Set(['FOAM_SELECTED_TEXT'])
|
||||
);
|
||||
const resolver = new Resolver(new Map(), targetDate);
|
||||
return NoteFactory.createFromTemplate(
|
||||
DAILY_NOTE_TEMPLATE_URI,
|
||||
resolver,
|
||||
@@ -190,8 +179,7 @@ export const NoteFactory = {
|
||||
): Promise<void> => {
|
||||
const resolver = new Resolver(
|
||||
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
|
||||
new Date()
|
||||
);
|
||||
return NoteFactory.createFromTemplate(
|
||||
DEFAULT_TEMPLATE_URI,
|
||||
@@ -204,8 +192,8 @@ export const NoteFactory = {
|
||||
|
||||
export const createTemplate = async (): Promise<void> => {
|
||||
const defaultFilename = 'new-template.md';
|
||||
const defaultTemplate = URI.joinPath(TEMPLATES_DIR, defaultFilename);
|
||||
const fsPath = URI.toFsPath(defaultTemplate);
|
||||
const defaultTemplate = TEMPLATES_DIR.joinPath(defaultFilename);
|
||||
const fsPath = defaultTemplate.toFsPath();
|
||||
const filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: fsPath,
|
||||
@@ -233,7 +221,7 @@ async function askUserForFilepathConfirmation(
|
||||
defaultFilepath: URI,
|
||||
defaultFilename: string
|
||||
) {
|
||||
const fsPath = URI.toFsPath(defaultFilepath);
|
||||
const fsPath = defaultFilepath.toFsPath();
|
||||
return await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: fsPath,
|
||||
@@ -253,12 +241,12 @@ export async function determineNewNoteFilepath(
|
||||
resolver: Resolver
|
||||
): Promise<URI> {
|
||||
if (templateFilepathAttribute) {
|
||||
const defaultFilepath = isAbsolute(templateFilepathAttribute)
|
||||
? URI.file(templateFilepathAttribute)
|
||||
: URI.joinPath(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri),
|
||||
templateFilepathAttribute
|
||||
);
|
||||
let defaultFilepath = URI.file(templateFilepathAttribute);
|
||||
if (!defaultFilepath.isAbsolute()) {
|
||||
defaultFilepath = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(templateFilepathAttribute);
|
||||
}
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
@@ -266,9 +254,8 @@ export async function determineNewNoteFilepath(
|
||||
return fallbackURI;
|
||||
}
|
||||
|
||||
const defaultName = await resolver.resolve('FOAM_TITLE');
|
||||
const defaultFilepath = URI.joinPath(
|
||||
getCurrentEditorDirectory(),
|
||||
const defaultName = await resolver.resolveFromName('FOAM_TITLE');
|
||||
const defaultFilepath = getCurrentEditorDirectory().joinPath(
|
||||
`${defaultName}.md`
|
||||
);
|
||||
return defaultFilepath;
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import { window } from 'vscode';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { Variable } from '../core/common/snippetParser';
|
||||
|
||||
describe('substituteFoamVariables', () => {
|
||||
test('Does nothing if no Foam-specific variables are used', async () => {
|
||||
const input = `
|
||||
# \${AnotherVariable} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
|
||||
# $AnotherVariable} <-- Unrelated to foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
|
||||
# \${AnotherVariable} <-- Unrelated to Foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to Foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to Foam
|
||||
# $AnotherVariable} <-- Unrelated to Foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to Foam
|
||||
`;
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', 'My note title');
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect((await resolver.resolveText(input))[1]).toEqual(input);
|
||||
expect(await resolver.resolveText(input)).toEqual(input);
|
||||
});
|
||||
|
||||
test('Ignores variable-looking text values', async () => {
|
||||
// Related to https://github.com/foambubble/foam/issues/602
|
||||
const input = `
|
||||
# \${CURRENT_DATE/.*/\${FOAM_TITLE}/} <-- FOAM_TITLE is not a variable here, but a text in a transform
|
||||
# \${1|one,two,\${FOAM_TITLE}|} <-- FOAM_TITLE is not a variable here, but a text in a choice
|
||||
`;
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', 'My note title');
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(input);
|
||||
});
|
||||
|
||||
test('Correctly substitutes variables that are substrings of one another', async () => {
|
||||
@@ -22,32 +36,31 @@ describe('substituteFoamVariables', () => {
|
||||
// If we're not careful with how we substitute the values
|
||||
// we can end up putting the FOAM_TITLE in place FOAM_TITLE_NON_EXISTENT_VARIABLE should be.
|
||||
const input = `
|
||||
# \${FOAM_TITLE}
|
||||
# $FOAM_TITLE
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
# \${FOAM_TITLE}
|
||||
# $FOAM_TITLE
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
# My note title
|
||||
# My note title
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
# My note title
|
||||
# My note title
|
||||
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
|
||||
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
|
||||
`;
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', 'My note title');
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect((await resolver.resolveText(input))[1]).toEqual(expected);
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFoamVariables', () => {
|
||||
test('Does nothing for unknown Foam-specific variables', async () => {
|
||||
const variables = ['FOAM_FOO'];
|
||||
const variables = [new Variable('FOAM_FOO')];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_FOO', 'FOAM_FOO');
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
@@ -56,7 +69,7 @@ describe('resolveFoamVariables', () => {
|
||||
|
||||
test('Resolves FOAM_TITLE', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
const variables = [new Variable('FOAM_TITLE')];
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
@@ -72,7 +85,7 @@ describe('resolveFoamVariables', () => {
|
||||
|
||||
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
const variables = [new Variable('FOAM_TITLE')];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
@@ -85,18 +98,18 @@ describe('resolveFoamVariables', () => {
|
||||
|
||||
test('Resolves FOAM_DATE_* properties with current day by default', async () => {
|
||||
const variables = [
|
||||
'FOAM_DATE_YEAR',
|
||||
'FOAM_DATE_YEAR_SHORT',
|
||||
'FOAM_DATE_MONTH',
|
||||
'FOAM_DATE_MONTH_NAME',
|
||||
'FOAM_DATE_MONTH_NAME_SHORT',
|
||||
'FOAM_DATE_DATE',
|
||||
'FOAM_DATE_DAY_NAME',
|
||||
'FOAM_DATE_DAY_NAME_SHORT',
|
||||
'FOAM_DATE_HOUR',
|
||||
'FOAM_DATE_MINUTE',
|
||||
'FOAM_DATE_SECOND',
|
||||
'FOAM_DATE_SECONDS_UNIX',
|
||||
new Variable('FOAM_DATE_YEAR'),
|
||||
new Variable('FOAM_DATE_YEAR_SHORT'),
|
||||
new Variable('FOAM_DATE_MONTH'),
|
||||
new Variable('FOAM_DATE_MONTH_NAME'),
|
||||
new Variable('FOAM_DATE_MONTH_NAME_SHORT'),
|
||||
new Variable('FOAM_DATE_DATE'),
|
||||
new Variable('FOAM_DATE_DAY_NAME'),
|
||||
new Variable('FOAM_DATE_DAY_NAME_SHORT'),
|
||||
new Variable('FOAM_DATE_HOUR'),
|
||||
new Variable('FOAM_DATE_MINUTE'),
|
||||
new Variable('FOAM_DATE_SECOND'),
|
||||
new Variable('FOAM_DATE_SECONDS_UNIX'),
|
||||
];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
@@ -123,18 +136,18 @@ describe('resolveFoamVariables', () => {
|
||||
test('Resolves FOAM_DATE_* properties with given date', async () => {
|
||||
const targetDate = new Date(2021, 9, 12, 1, 2, 3);
|
||||
const variables = [
|
||||
'FOAM_DATE_YEAR',
|
||||
'FOAM_DATE_YEAR_SHORT',
|
||||
'FOAM_DATE_MONTH',
|
||||
'FOAM_DATE_MONTH_NAME',
|
||||
'FOAM_DATE_MONTH_NAME_SHORT',
|
||||
'FOAM_DATE_DATE',
|
||||
'FOAM_DATE_DAY_NAME',
|
||||
'FOAM_DATE_DAY_NAME_SHORT',
|
||||
'FOAM_DATE_HOUR',
|
||||
'FOAM_DATE_MINUTE',
|
||||
'FOAM_DATE_SECOND',
|
||||
'FOAM_DATE_SECONDS_UNIX',
|
||||
new Variable('FOAM_DATE_YEAR'),
|
||||
new Variable('FOAM_DATE_YEAR_SHORT'),
|
||||
new Variable('FOAM_DATE_MONTH'),
|
||||
new Variable('FOAM_DATE_MONTH_NAME'),
|
||||
new Variable('FOAM_DATE_MONTH_NAME_SHORT'),
|
||||
new Variable('FOAM_DATE_DATE'),
|
||||
new Variable('FOAM_DATE_DAY_NAME'),
|
||||
new Variable('FOAM_DATE_DAY_NAME_SHORT'),
|
||||
new Variable('FOAM_DATE_HOUR'),
|
||||
new Variable('FOAM_DATE_MINUTE'),
|
||||
new Variable('FOAM_DATE_SECOND'),
|
||||
new Variable('FOAM_DATE_SECONDS_UNIX'),
|
||||
];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
@@ -164,16 +177,14 @@ describe('resolveFoamVariables', () => {
|
||||
describe('resolveFoamTemplateVariables', () => {
|
||||
test('Does nothing for template without Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# \${AnotherVariable} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
|
||||
# $AnotherVariable} <-- Unrelated to foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
|
||||
`;
|
||||
# \${AnotherVariable} <-- Unrelated to Foam
|
||||
# \${AnotherVariable:default_value} <-- Unrelated to Foam
|
||||
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to Foam
|
||||
# $AnotherVariable} <-- Unrelated to Foam
|
||||
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to Foam
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
const expected = input;
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
@@ -181,48 +192,17 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
|
||||
test('Does nothing for unknown Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# $FOAM_FOO
|
||||
# \${FOAM_FOO}
|
||||
# \${FOAM_FOO:default_value}
|
||||
# \${FOAM_FOO:default_value/(.*)/\${1:/upcase}/}}
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
# $FOAM_FOO
|
||||
# \${FOAM_FOO}
|
||||
# \${FOAM_FOO:default_value}
|
||||
# \${FOAM_FOO:default_value/(.*)/\${1:/upcase}/}}
|
||||
`;
|
||||
|
||||
const expected = input;
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Allows extra variables to be provided; only resolves the unique set', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
# My note title
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
|
||||
const resolver = new Resolver(
|
||||
new Map(),
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE'])
|
||||
);
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in a newline', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
@@ -232,20 +212,11 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
|
||||
const input = `# \${FOAM_TITLE}\n`;
|
||||
|
||||
const expectedOutput = `# My note title\nSelected text\n`;
|
||||
const expected = `# My note title\nSelected text\n`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
const resolver = new Resolver(
|
||||
givenValues,
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
|
||||
);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -258,20 +229,11 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
|
||||
const input = `# \${FOAM_TITLE}\n\n`;
|
||||
|
||||
const expectedOutput = `# My note title\n\nSelected text\n`;
|
||||
const expected = `# My note title\n\nSelected text\n`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
const resolver = new Resolver(
|
||||
givenValues,
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
|
||||
);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -284,20 +246,11 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
|
||||
const input = `# \${FOAM_TITLE}`;
|
||||
|
||||
const expectedOutput = '# My note title\nSelected text';
|
||||
const expected = '# My note title\nSelected text';
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
const resolver = new Resolver(
|
||||
givenValues,
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
|
||||
);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -309,25 +262,15 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `
|
||||
# \${FOAM_TITLE}
|
||||
`;
|
||||
# \${FOAM_TITLE}
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
# My note title
|
||||
`;
|
||||
const expected = `
|
||||
# My note title
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', '');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', '');
|
||||
const resolver = new Resolver(
|
||||
givenValues,
|
||||
new Date(),
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
|
||||
);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { findSelectionContent } from './editor';
|
||||
import { window } from 'vscode';
|
||||
import { UserCancelledOperation } from './errors';
|
||||
import {
|
||||
SnippetParser,
|
||||
Variable,
|
||||
VariableResolver,
|
||||
} from '../core/common/snippetParser';
|
||||
|
||||
const knownFoamVariables = new Set([
|
||||
'FOAM_TITLE',
|
||||
@@ -19,54 +24,18 @@ const knownFoamVariables = new Set([
|
||||
'FOAM_DATE_SECONDS_UNIX',
|
||||
]);
|
||||
|
||||
export function substituteVariables(
|
||||
text: string,
|
||||
variables: Map<string, string>
|
||||
) {
|
||||
variables.forEach((value, variable) => {
|
||||
const regex = new RegExp(
|
||||
// Matches a limited subset of the the TextMate variable syntax:
|
||||
// ${VARIABLE} OR $VARIABLE
|
||||
`\\\${${variable}}|\\$${variable}(\\W|$)`,
|
||||
// The latter is more complicated, since it needs to avoid replacing
|
||||
// longer variable names with the values of variables that are
|
||||
// substrings of the longer ones (e.g. `$FOO` and `$FOOBAR`. If you
|
||||
// replace $FOO first, and aren't careful, you replace the first
|
||||
// characters of `$FOOBAR`)
|
||||
'g' // 'g' => Global replacement (i.e. not just the first instance)
|
||||
);
|
||||
text = text.replace(regex, `${value}$1`);
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function findFoamVariables(templateText: string): string[] {
|
||||
const regex = /\$(FOAM_[_a-zA-Z0-9]*)|\${(FOAM_[[_a-zA-Z0-9]*)}/g;
|
||||
var matches = [];
|
||||
const output: string[] = [];
|
||||
while ((matches = regex.exec(templateText))) {
|
||||
output.push(matches[1] || matches[2]);
|
||||
}
|
||||
const uniqVariables = [...new Set(output)];
|
||||
const knownVariables = uniqVariables.filter(x => knownFoamVariables.has(x));
|
||||
return knownVariables;
|
||||
}
|
||||
|
||||
export class Resolver {
|
||||
promises = new Map<string, Thenable<string>>();
|
||||
export class Resolver implements VariableResolver {
|
||||
private promises = new Map<string, Promise<string | undefined>>();
|
||||
|
||||
/**
|
||||
* Create a resolver
|
||||
*
|
||||
* @param givenValues the map of variable name to value
|
||||
* @param foamDate the date used to fill FOAM_DATE_* variables
|
||||
* @param extraVariablesToResolve other variables to always resolve, even if not present in text
|
||||
*/
|
||||
constructor(
|
||||
private givenValues: Map<string, string>,
|
||||
private foamDate: Date,
|
||||
private extraVariablesToResolve: Set<string> = new Set()
|
||||
private foamDate: Date
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -86,31 +55,38 @@ export class Resolver {
|
||||
* @returns an array, where the first element is the resolution map,
|
||||
* and the second is the processed text
|
||||
*/
|
||||
async resolveText(text: string): Promise<[Map<string, string>, string]> {
|
||||
const variablesInTemplate = findFoamVariables(text.toString());
|
||||
const variables = variablesInTemplate.concat(
|
||||
...this.extraVariablesToResolve
|
||||
async resolveText(text: string): Promise<string> {
|
||||
let snippet = new SnippetParser().parse(text, false, false);
|
||||
let foamVariablesInTemplate = new Set(
|
||||
snippet
|
||||
.variables()
|
||||
.map(v => v.name)
|
||||
.filter(name => knownFoamVariables.has(name))
|
||||
);
|
||||
const uniqVariables = [...new Set(variables)];
|
||||
|
||||
const resolvedValues = await this.resolveAll(uniqVariables);
|
||||
|
||||
// Add FOAM_SELECTED_TEXT to the template text if required
|
||||
// and re-parse the template text.
|
||||
if (
|
||||
resolvedValues.get('FOAM_SELECTED_TEXT') &&
|
||||
!variablesInTemplate.includes('FOAM_SELECTED_TEXT')
|
||||
this.givenValues.has('FOAM_SELECTED_TEXT') &&
|
||||
!foamVariablesInTemplate.has('FOAM_SELECTED_TEXT')
|
||||
) {
|
||||
text = text.endsWith('\n')
|
||||
? `${text}\${FOAM_SELECTED_TEXT}\n`
|
||||
: `${text}\n\${FOAM_SELECTED_TEXT}`;
|
||||
|
||||
variablesInTemplate.push('FOAM_SELECTED_TEXT');
|
||||
variables.push('FOAM_SELECTED_TEXT');
|
||||
uniqVariables.push('FOAM_SELECTED_TEXT');
|
||||
const token = '$FOAM_SELECTED_TEXT';
|
||||
if (text.endsWith('\n')) {
|
||||
text = `${text}${token}\n`;
|
||||
} else {
|
||||
text = `${text}\n${token}`;
|
||||
}
|
||||
snippet = new SnippetParser().parse(text, false, false);
|
||||
foamVariablesInTemplate = new Set(
|
||||
snippet
|
||||
.variables()
|
||||
.map(v => v.name)
|
||||
.filter(name => knownFoamVariables.has(name))
|
||||
);
|
||||
}
|
||||
|
||||
const subbedText = substituteVariables(text.toString(), resolvedValues);
|
||||
|
||||
return [resolvedValues, subbedText];
|
||||
await snippet.resolveVariables(this, foamVariablesInTemplate);
|
||||
return snippet.snippetTextWithVariablesSubstituted(foamVariablesInTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,19 +95,16 @@ export class Resolver {
|
||||
* @param variables a list of variables to resolve
|
||||
* @returns a Map of variable name to its value
|
||||
*/
|
||||
async resolveAll(variables: string[]): Promise<Map<string, string>> {
|
||||
const promises = variables.map(async variable =>
|
||||
Promise.resolve([variable, await this.resolve(variable)])
|
||||
);
|
||||
async resolveAll(variables: Variable[]): Promise<Map<string, string>> {
|
||||
await Promise.all(variables.map(variable => variable.resolve(this)));
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const valueByName = new Map<string, string>();
|
||||
results.forEach(([variable, value]) => {
|
||||
valueByName.set(variable, value);
|
||||
const resolvedValues = new Map<string, string>();
|
||||
variables.forEach(variable => {
|
||||
if (variable.children.length > 0) {
|
||||
resolvedValues.set(variable.name, variable.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return valueByName;
|
||||
return resolvedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +113,15 @@ export class Resolver {
|
||||
* @param name the variable name
|
||||
* @returns the resolved value, or the name of the variable if nothing is found
|
||||
*/
|
||||
resolve(name: string): Thenable<string> {
|
||||
async resolveFromName(name: string): Promise<string> {
|
||||
const variable = new Variable(name);
|
||||
await variable.resolve(this);
|
||||
|
||||
return (variable.children[0] ?? name).toString();
|
||||
}
|
||||
|
||||
resolve(variable: Variable): Promise<string | undefined> {
|
||||
const name = variable.name;
|
||||
if (this.givenValues.has(name)) {
|
||||
this.promises.set(name, Promise.resolve(this.givenValues.get(name)));
|
||||
} else if (!this.promises.has(name)) {
|
||||
@@ -263,7 +244,7 @@ export class Resolver {
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this.promises.set(name, Promise.resolve(name));
|
||||
this.promises.set(name, Promise.resolve(undefined));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { runTests } from 'vscode-test';
|
||||
import { runUnit } from './suite-unit';
|
||||
|
||||
@@ -31,27 +29,21 @@ async function main() {
|
||||
console.log('Running e2e tests');
|
||||
// The folder containing the Extension Manifest package.json
|
||||
// Passed to `--extensionDevelopmentPath`
|
||||
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||
const extensionDevelopmentPath = path.join(__dirname, '..', '..');
|
||||
// The path to the extension test script
|
||||
// Passed to --extensionTestsPath
|
||||
const extensionTestsPath = path.resolve(__dirname, './suite');
|
||||
const tmpWorkspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foam-'));
|
||||
const extensionTestsPath = path.join(__dirname, 'suite');
|
||||
|
||||
// Download VS Code, unzip it and run the integration test
|
||||
await runTests({
|
||||
extensionDevelopmentPath,
|
||||
extensionTestsPath,
|
||||
launchArgs: [
|
||||
tmpWorkspaceDir,
|
||||
path.join(extensionDevelopmentPath, '.test-workspace'),
|
||||
'--disable-extensions',
|
||||
'--disable-workspace-trust',
|
||||
],
|
||||
// Running the tests with vscode 1.53.0 is causing issues in the output/error stream management,
|
||||
// which is causing a stack overflow, possibly due to a recursive callback.
|
||||
// Also see https://github.com/foambubble/foam/pull/479#issuecomment-774167127
|
||||
// Forcing the version to 1.52.0 solves the problem.
|
||||
// TODO: to review, further investigate, and roll back this workaround.
|
||||
version: '1.52.0',
|
||||
version: '1.60.0',
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error occurred while running Foam e2e tests:', err);
|
||||
|
||||
@@ -8,32 +8,30 @@
|
||||
* they will make direct use of the vscode API to be invoked as commands, create editors,
|
||||
* and so on..
|
||||
*/
|
||||
import path from 'path';
|
||||
import { runCLI } from '@jest/core';
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
/* eslint-disable import/first */
|
||||
|
||||
// Set before imports, see https://github.com/facebook/jest/issues/12162
|
||||
process.env.FORCE_COLOR = '1';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { runCLI } from '@jest/core';
|
||||
import path from 'path';
|
||||
|
||||
const rootDir = path.join(__dirname, '..', '..');
|
||||
|
||||
export function runUnit(): Promise<void> {
|
||||
process.env.FORCE_COLOR = '1';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.BABEL_ENV = 'test';
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { results } = await runCLI(
|
||||
{
|
||||
rootDir,
|
||||
roots: ['<rootDir>/src'],
|
||||
transform: JSON.stringify({ '^.+\\.ts$': 'ts-jest' }),
|
||||
runInBand: true,
|
||||
testRegex: '\\.(test)\\.ts$',
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
globals: JSON.stringify({
|
||||
'ts-jest': {
|
||||
tsconfig: path.resolve(rootDir, './tsconfig.json'),
|
||||
},
|
||||
}),
|
||||
testTimeout: 20000,
|
||||
verbose: false,
|
||||
silent: false,
|
||||
|
||||
@@ -9,42 +9,48 @@
|
||||
* and so on..
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { runCLI } from '@jest/core';
|
||||
/* eslint-disable import/first */
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
// Set before imports, see https://github.com/facebook/jest/issues/12162
|
||||
process.env.FORCE_COLOR = '1';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { runCLI } from '@jest/core';
|
||||
import { cleanWorkspace } from './test-utils-vscode';
|
||||
import path from 'path';
|
||||
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
|
||||
export function run(): Promise<void> {
|
||||
const errWrite = process.stderr.write;
|
||||
|
||||
let remaining = '';
|
||||
process.stderr.write = (buffer: string) => {
|
||||
console.log(buffer);
|
||||
const lines = (remaining + buffer).split('\n');
|
||||
remaining = lines.pop() as string;
|
||||
// Trim long lines because some uninformative code dumps will flood the
|
||||
// console or, worse, be suppressed altogether because of their size.
|
||||
lines.forEach(l => console.log(l.substr(0, 300)));
|
||||
return true;
|
||||
};
|
||||
|
||||
// process.on('unhandledRejection', err => {
|
||||
// throw err;
|
||||
// });
|
||||
process.env.FORCE_COLOR = '1';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.BABEL_ENV = 'test';
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await cleanWorkspace();
|
||||
try {
|
||||
const { results } = await runCLI(
|
||||
{
|
||||
rootDir,
|
||||
roots: ['<rootDir>/src'],
|
||||
transform: JSON.stringify({ '^.+\\.ts$': 'ts-jest' }),
|
||||
runInBand: true,
|
||||
testRegex: '\\.(test|spec)\\.ts$',
|
||||
testEnvironment:
|
||||
'<rootDir>/src/test/support/extended-vscode-environment.js',
|
||||
testEnvironment: '<rootDir>/src/test/support/vscode-environment.js',
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
globals: JSON.stringify({
|
||||
'ts-jest': {
|
||||
tsconfig: path.resolve(rootDir, './tsconfig.json'),
|
||||
},
|
||||
}),
|
||||
testTimeout: 30000,
|
||||
useStderr: true,
|
||||
verbose: true,
|
||||
@@ -71,6 +77,7 @@ export function run(): Promise<void> {
|
||||
return reject(error);
|
||||
} finally {
|
||||
process.stderr.write = errWrite.bind(process.stderr);
|
||||
await cleanWorkspace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/env/ExtendedVscodeEnvironment.js
|
||||
const VscodeEnvironment = require('jest-environment-vscode');
|
||||
const NodeEnvironment = require('jest-environment-node');
|
||||
const vscode = require('vscode');
|
||||
|
||||
const initialVscode = vscode;
|
||||
class ExtendedVscodeEnvironment extends VscodeEnvironment {
|
||||
class VscodeEnvironment extends NodeEnvironment {
|
||||
async setup() {
|
||||
await super.setup();
|
||||
this.global.vscode = vscode;
|
||||
|
||||
// Expose RegExp otherwise document.getWordRangeAtPosition won't work as supposed.
|
||||
// Implementation of getWordRangeAtPosition uses "instanceof RegExp" which returns false
|
||||
// due to Jest running tests in the different vm context.
|
||||
// See https://github.com/nodejs/node-v0.x-archive/issues/1277.
|
||||
// And also https://github.com/microsoft/vscode-test/issues/37#issuecomment-700167820
|
||||
this.global.RegExp = RegExp;
|
||||
this.global.vscode = vscode;
|
||||
|
||||
vscode.workspace
|
||||
.getConfiguration()
|
||||
.update('foam.edit.linkReferenceDefinitions', 'off');
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
this.global.vscode = initialVscode;
|
||||
this.global.vscode = {};
|
||||
await super.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExtendedVscodeEnvironment;
|
||||
module.exports = VscodeEnvironment;
|
||||
@@ -13,7 +13,7 @@ import { randomString, wait } from './test-utils';
|
||||
Logger.setLevel('error');
|
||||
|
||||
export const cleanWorkspace = async () => {
|
||||
const files = await vscode.workspace.findFiles('**', '.vscode');
|
||||
const files = await vscode.workspace.findFiles('**', '{.vscode,.keep}');
|
||||
await Promise.all(files.map(f => vscode.workspace.fs.delete(f)));
|
||||
};
|
||||
|
||||
@@ -28,7 +28,8 @@ export const closeEditors = async () => {
|
||||
await wait(100);
|
||||
};
|
||||
|
||||
export const deleteFile = (uri: URI) => {
|
||||
export const deleteFile = (file: URI | { uri: URI }) => {
|
||||
const uri = 'uri' in file ? file.uri : file;
|
||||
return vscode.workspace.fs.delete(toVsCodeUri(uri), { recursive: true });
|
||||
};
|
||||
|
||||
@@ -42,7 +43,7 @@ export const deleteFile = (uri: URI) => {
|
||||
export const getUriInWorkspace = (...filepath: string[]) => {
|
||||
const rootUri = fromVsCodeUri(vscode.workspace.workspaceFolders[0].uri);
|
||||
filepath = filepath.length > 0 ? filepath : [randomString() + '.md'];
|
||||
const uri = URI.joinPath(rootUri, ...filepath);
|
||||
const uri = rootUri.joinPath(...filepath);
|
||||
return uri;
|
||||
};
|
||||
|
||||
@@ -55,7 +56,7 @@ export const getUriInWorkspace = (...filepath: string[]) => {
|
||||
*/
|
||||
export const createFile = async (content: string, filepath: string[] = []) => {
|
||||
const uri = getUriInWorkspace(...filepath);
|
||||
const filenameComponents = path.parse(URI.toFsPath(uri));
|
||||
const filenameComponents = path.parse(uri.toFsPath());
|
||||
await vscode.workspace.fs.writeFile(
|
||||
toVsCodeUri(uri),
|
||||
new TextEncoder().encode(content)
|
||||
@@ -64,7 +65,7 @@ export const createFile = async (content: string, filepath: string[] = []) => {
|
||||
};
|
||||
|
||||
export const createNote = (r: Resource) => {
|
||||
let content = `# ${r.title}
|
||||
const content = `# ${r.title}
|
||||
|
||||
some content and ${r.links
|
||||
.map(l =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* This file should not depend on VS Code as it's used for unit tests
|
||||
*/
|
||||
import path from 'path';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { Range } from '../core/model/range';
|
||||
import { URI } from '../core/model/uri';
|
||||
@@ -12,8 +11,7 @@ import { NoteLinkDefinition, Resource } from '../core/model/note';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
export const TEST_DATA_DIR = URI.joinPath(
|
||||
URI.file(__dirname),
|
||||
export const TEST_DATA_DIR = URI.file(__dirname).joinPath(
|
||||
'..',
|
||||
'..',
|
||||
'test-data'
|
||||
@@ -50,15 +48,20 @@ export const createTestNote = (params: {
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
tags?: string[];
|
||||
text?: string;
|
||||
sections?: string[];
|
||||
root?: URI;
|
||||
}): Resource => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: URI.resolve(params.uri, root),
|
||||
uri: root.resolve(params.uri),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
title: params.title ?? strToUri(params.uri).getBasename(),
|
||||
definitions: params.definitions ?? [],
|
||||
sections: params.sections?.map(label => ({
|
||||
label,
|
||||
range: Range.create(0, 0, 1, 0),
|
||||
})),
|
||||
tags:
|
||||
params.tags?.map(t => ({
|
||||
label: t,
|
||||
@@ -106,3 +109,6 @@ export const randomString = (len = 5) =>
|
||||
.fill('')
|
||||
.map(() => chars.charAt(Math.floor(Math.random() * chars.length)))
|
||||
.join('');
|
||||
|
||||
export const getRandomURI = () =>
|
||||
URI.file('/random-uri-root/' + randomString() + '.md');
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { dropExtension, removeBrackets, toTitleCase } from './utils';
|
||||
|
||||
describe('dropExtension', () => {
|
||||
test('returns file name without extension', () => {
|
||||
expect(dropExtension('file.md')).toEqual('file');
|
||||
});
|
||||
});
|
||||
import { removeBrackets, toTitleCase } from './utils';
|
||||
|
||||
describe('removeBrackets', () => {
|
||||
it('removes the brackets', () => {
|
||||
|
||||
@@ -11,16 +11,12 @@ import {
|
||||
version,
|
||||
ViewColumn,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
import os from 'os';
|
||||
import { toVsCodeUri } from './utils/vsc-utils';
|
||||
import { Logger } from './core/utils/log';
|
||||
import { URI } from './core/model/uri';
|
||||
|
||||
export const isWindows = os.platform() === 'win32';
|
||||
|
||||
export const docConfig = { tab: ' ', eol: '\r\n' };
|
||||
|
||||
export const mdDocSelector = [
|
||||
@@ -30,7 +26,7 @@ export const mdDocSelector = [
|
||||
|
||||
export function loadDocConfig() {
|
||||
// Load workspace config
|
||||
let activeEditor = window.activeTextEditor;
|
||||
const activeEditor = window.activeTextEditor;
|
||||
if (!activeEditor) {
|
||||
Logger.debug('Failed to load config, no active editor');
|
||||
return;
|
||||
@@ -38,8 +34,8 @@ export function loadDocConfig() {
|
||||
|
||||
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? '\r\n' : '\n';
|
||||
|
||||
let tabSize = Number(activeEditor.options.tabSize);
|
||||
let insertSpaces = activeEditor.options.insertSpaces;
|
||||
const tabSize = Number(activeEditor.options.tabSize);
|
||||
const insertSpaces = activeEditor.options.insertSpaces;
|
||||
if (insertSpaces) {
|
||||
docConfig.tab = ' '.repeat(tabSize);
|
||||
} else {
|
||||
@@ -85,12 +81,6 @@ export function getText(range: Range): string {
|
||||
return window.activeTextEditor.document.getText(range);
|
||||
}
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
const parts = path.split('.');
|
||||
parts.pop();
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the "Copy to Clipboard Without Brackets" command
|
||||
*
|
||||
@@ -134,18 +124,6 @@ export function toTitleCase(word: string): string {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given path exists in the file system
|
||||
*
|
||||
* @param path The path to verify
|
||||
*/
|
||||
export function pathExists(path: URI) {
|
||||
return fs.promises
|
||||
.access(URI.toFsPath(path), fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given object is defined
|
||||
*
|
||||
@@ -155,7 +133,7 @@ export function isSome<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is NonNullable<T> {
|
||||
//
|
||||
return value != null; // eslint-disable-line
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +144,7 @@ export function isSome<T>(
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null; // eslint-disable-line
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export async function focusNote(
|
||||
|
||||
@@ -16,7 +16,7 @@ import { FoamWorkspace } from '../core/model/workspace';
|
||||
* 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
|
||||
@@ -168,7 +168,7 @@ export class GroupedResourcesTreeDataProvider
|
||||
}
|
||||
|
||||
private isMatch(uri: URI) {
|
||||
return micromatch.isMatch(URI.toFsPath(uri), this.exclude);
|
||||
return micromatch.isMatch(uri.toFsPath(), this.exclude);
|
||||
}
|
||||
|
||||
private getGlobs(fsURI: URI[], globs: string[]): string[] {
|
||||
@@ -216,7 +216,7 @@ export class UriTreeItem extends vscode.TreeItem {
|
||||
title?: string;
|
||||
} = {}
|
||||
) {
|
||||
super(options?.title ?? URI.getBasename(uri), options.collapsibleState);
|
||||
super(options?.title ?? uri.getName(), options.collapsibleState);
|
||||
this.description = uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(uri))?.uri.path,
|
||||
''
|
||||
|
||||
@@ -9,6 +9,6 @@ export const toVsCodePosition = (p: FoamPosition): Position =>
|
||||
export const toVsCodeRange = (r: FoamRange): Range =>
|
||||
new Range(r.start.line, r.start.character, r.end.line, r.end.character);
|
||||
|
||||
export const toVsCodeUri = (u: FoamURI): Uri => Uri.parse(FoamURI.toString(u));
|
||||
export const toVsCodeUri = (u: FoamURI): Uri => Uri.parse(u.toString());
|
||||
|
||||
export const fromVsCodeUri = (u: Uri): FoamURI => FoamURI.parse(u.toString());
|
||||
|
||||
74
packages/foam-vscode/static/dataviz/graph.css
Normal file
74
packages/foam-vscode/static/dataviz/graph.css
Normal file
@@ -0,0 +1,74 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dg .c {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.dg .property-name {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.vscode-light .dg.main.taller-than-window .close-button {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.vscode-light .dg.main .close-button {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.vscode-light .dg.main .close-button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.vscode-light .dg {
|
||||
color: #555;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.vscode-light .dg.main::-webkit-scrollbar {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.vscode-light .dg.main::-webkit-scrollbar-thumb {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.vscode-light .dg li:not(.folder) {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.vscode-light .dg li.save-row .button {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.vscode-light .dg li.title {
|
||||
background: #e8e8e8 url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAACVJREFUGFdjZMACGLEKRkfH/0eXAKtElli6dCEjXDtIAiQAUgQAF7UKGmOy49cAAAAASUVORK5CYII=) 6px 10px no-repeat;
|
||||
}
|
||||
|
||||
.vscode-light .dg .cr.function:hover,.dg .cr.boolean:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vscode-light .dg .c input[type=text] {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.vscode-light .dg .c input[type=text]:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.vscode-light .dg .c input[type=text]:focus {
|
||||
background: #eee;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.vscode-light .dg .c .slider {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.vscode-light .dg .c .slider:hover {
|
||||
background: #eee;
|
||||
}
|
||||
@@ -299,7 +299,7 @@ function updateForceGraphDataFromModel(m) {
|
||||
});
|
||||
// apply the delta
|
||||
nodeIdsToRemove.forEach(id => {
|
||||
const index = m.data.nodes.findIndex(n => n.id == id);
|
||||
const index = m.data.nodes.findIndex(n => n.id === id);
|
||||
m.data.nodes.splice(index, 1); // delete the element
|
||||
});
|
||||
nodeIdsToAdd.forEach(nodeId => {
|
||||
@@ -312,10 +312,10 @@ function updateForceGraphDataFromModel(m) {
|
||||
m.data.links = m.graph.links
|
||||
.filter(link => {
|
||||
const isSource = Object.values(m.data.nodes).some(
|
||||
node => node.id == link.source
|
||||
node => node.id === link.source
|
||||
);
|
||||
const isTarget = Object.values(m.data.nodes).some(
|
||||
node => node.id == link.target
|
||||
node => node.id === link.target
|
||||
);
|
||||
return isSource && isTarget;
|
||||
})
|
||||
@@ -399,7 +399,7 @@ function getLinkState(link, model) {
|
||||
? 'regular'
|
||||
: Array.from(model.focusLinks).some(
|
||||
fLink =>
|
||||
fLink.source == link.source.id && fLink.target == link.target.id
|
||||
fLink.source === link.source.id && fLink.target === link.target.id
|
||||
)
|
||||
? 'highlighted'
|
||||
: 'lessened';
|
||||
@@ -473,6 +473,7 @@ class Painter {
|
||||
try {
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.model = model;
|
||||
window.graphUpdated = false;
|
||||
|
||||
window.onload = () => {
|
||||
initDataviz(vscode);
|
||||
@@ -501,6 +502,15 @@ try {
|
||||
case 'didUpdateGraphData':
|
||||
graphData = augmentGraphInfo(message.payload);
|
||||
Actions.refreshWorkspaceData(graphData);
|
||||
if (!graphUpdated) {
|
||||
window.graphUpdated = true;
|
||||
graph.zoom(graph.zoom() * 1.5);
|
||||
graph.cooldownTicks(100);
|
||||
graph.onEngineStop(() => {
|
||||
graph.onEngineStop(() => {});
|
||||
graph.zoomToFit(500);
|
||||
});
|
||||
}
|
||||
console.log('didUpdateGraphData', graphData);
|
||||
break;
|
||||
case 'didSelectNote':
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
<script data-replace src="./d3.v6.min.js"></script>
|
||||
<script data-replace src="./force-graph.1.40.5.min.js"></script>
|
||||
<script data-replace src="./dat.gui.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<link data-replace href="./graph.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
|
||||
11
packages/foam-vscode/syntaxes/injection.json
Normal file
11
packages/foam-vscode/syntaxes/injection.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"scopeName": "foam.wikilink.injection",
|
||||
"injectionSelector": "L:meta.paragraph.markdown",
|
||||
"patterns": [
|
||||
{
|
||||
"contentName": "string.other.link.title.markdown.foam",
|
||||
"begin": "\\[\\[",
|
||||
"end": "\\]\\]"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
readme.md
18
readme.md
@@ -5,7 +5,7 @@
|
||||
👀*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)
|
||||
@@ -61,6 +61,11 @@ Embed the content from other notes.
|
||||
|
||||

|
||||
|
||||
### Support for sections
|
||||
|
||||
Foam supports autocompletion, navigation, embedding and diagnostics for note sections.
|
||||
Just use the standard wiki syntax of `[[resource#Section Title]]`.
|
||||
|
||||
### Link Alias
|
||||
|
||||
Foam supports link aliasing, so you can have a `[[wikilink]]`, or a `[[wikilink|alias]]`.
|
||||
@@ -87,9 +92,9 @@ Foam also supports hierarchical tags.
|
||||
|
||||
### Orphans and Placeholder Panels
|
||||
|
||||
Orphans are note that have no inbound nor outbound links.
|
||||
Orphans are notes that have no inbound nor outbound links.
|
||||
Placeholders are dangling links, or notes without content.
|
||||
Keep them under control, and your knowledge base in better state, by using this panel.
|
||||
Keep them under control, and your knowledge base in a better state, by using this panel.
|
||||
|
||||

|
||||
|
||||
@@ -232,7 +237,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://mlaws.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
|
||||
@@ -295,6 +300,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
280
yarn.lock
280
yarn.lock
@@ -14,7 +14,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.11.tgz#9c8fe523c206979c9a81b1e12fe50c1254f1aa35"
|
||||
integrity sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==
|
||||
|
||||
"@babel/core@^7.1.0", "@babel/core@^7.11.0", "@babel/core@^7.4.4", "@babel/core@^7.7.5":
|
||||
"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.7.5":
|
||||
version "7.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
|
||||
integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
|
||||
@@ -465,13 +465,6 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-syntax-typescript@^7.12.13":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474"
|
||||
integrity sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-transform-arrow-functions@^7.13.0":
|
||||
version "7.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae"
|
||||
@@ -669,7 +662,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-transform-runtime@^7.10.4", "@babel/plugin-transform-runtime@^7.6.0":
|
||||
"@babel/plugin-transform-runtime@^7.6.0":
|
||||
version "7.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1"
|
||||
integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==
|
||||
@@ -717,15 +710,6 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-transform-typescript@^7.13.0":
|
||||
version "7.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.13.0.tgz#4a498e1f3600342d2a9e61f60131018f55774853"
|
||||
integrity sha512-elQEwluzaU8R8dbVuW2Q2Y8Nznf7hnjM7+DSCd14Lo5fF63C9qNLbwZYbmZrtV9/ySpSUpkRpQXvJb6xyu4hCQ==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.13.0"
|
||||
"@babel/helper-plugin-utils" "^7.13.0"
|
||||
"@babel/plugin-syntax-typescript" "^7.12.13"
|
||||
|
||||
"@babel/plugin-transform-unicode-escapes@^7.12.13":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz#840ced3b816d3b5127dd1d12dcedc5dead1a5e74"
|
||||
@@ -749,7 +733,7 @@
|
||||
core-js "^2.6.5"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.4.4":
|
||||
"@babel/preset-env@^7.4.4":
|
||||
version "7.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252"
|
||||
integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ==
|
||||
@@ -834,15 +818,6 @@
|
||||
"@babel/types" "^7.4.4"
|
||||
esutils "^2.0.2"
|
||||
|
||||
"@babel/preset-typescript@^7.10.4":
|
||||
version "7.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz#ab107e5f050609d806fbb039bec553b33462c60a"
|
||||
integrity sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.13.0"
|
||||
"@babel/helper-validator-option" "^7.12.17"
|
||||
"@babel/plugin-transform-typescript" "^7.13.0"
|
||||
|
||||
"@babel/runtime-corejs3@^7.10.2":
|
||||
version "7.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86"
|
||||
@@ -2006,11 +1981,32 @@
|
||||
call-me-maybe "^1.0.1"
|
||||
glob-to-regexp "^0.3.0"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.stat@^1.1.2":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
dependencies:
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@octokit/auth-token@^2.4.0":
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
|
||||
@@ -2250,11 +2246,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/github-slugger@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/github-slugger/-/github-slugger-1.3.0.tgz#16ab393b30d8ae2a111ac748a015ac05a1fc5524"
|
||||
integrity sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==
|
||||
|
||||
"@types/glob@^7.1.1":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
|
||||
@@ -2314,6 +2305,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
|
||||
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
@@ -2461,6 +2457,18 @@
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@^5.0.0":
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.0.tgz#0916ffe98d34b3c95e3652efa0cace61a7b25728"
|
||||
integrity sha512-KN5FvNH71bhZ8fKtL+lhW7bjm7cxs1nt+hrDZWIqb6ViCffQcWyLunGrgvISgkRojIDcXIsH+xlFfI4RCDA0xA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.8.0"
|
||||
"@typescript-eslint/types" "5.8.0"
|
||||
"@typescript-eslint/typescript-estree" "5.8.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/parser@^2.12.0", "@typescript-eslint/parser@^2.30.0":
|
||||
version "2.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8"
|
||||
@@ -2471,6 +2479,19 @@
|
||||
"@typescript-eslint/typescript-estree" "2.34.0"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.8.0":
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.8.0.tgz#2371095b4fa4c7be6a80b380f4e1b49c715e16f4"
|
||||
integrity sha512-x82CYJsLOjPCDuFFEbS6e7K1QEWj7u5Wk1alw8A+gnJiYwNnDJk0ib6PCegbaPMjrfBvFKa7SxE3EOnnIQz2Gg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.8.0"
|
||||
"@typescript-eslint/visitor-keys" "5.8.0"
|
||||
|
||||
"@typescript-eslint/types@5.8.0":
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.8.0.tgz#e7fa74ec35d9dbe3560d039d3d8734986c3971e0"
|
||||
integrity sha512-LdCYOqeqZWqCMOmwFnum6YfW9F3nKuxJiR84CdIRN5nfHJ7gyvGpXWqL/AaW0k3Po0+wm93ARAsOdzlZDPCcXg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@2.34.0":
|
||||
version "2.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5"
|
||||
@@ -2484,6 +2505,27 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.8.0":
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.0.tgz#900469ba9d5a37f4482b014ecce4a5dbb86cb4dd"
|
||||
integrity sha512-srfeZ3URdEcUsSLbkOFqS7WoxOqn8JNil2NSLO9O+I2/Uyc85+UlfpEvQHIpj5dVts7KKOZnftoJD/Fdv0L7nQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.8.0"
|
||||
"@typescript-eslint/visitor-keys" "5.8.0"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.8.0":
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.0.tgz#22d4ed96fe2451135299239feedb9fe1dcec780c"
|
||||
integrity sha512-+HDIGOEMnqbxdAHegxvnOqESUH6RWFRR2b8qxP1W9CZnnYh4Usz6MBL+2KMAgPk/P0o9c1HqnYtwzVH6GTIqug==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.8.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@zkochan/cmd-shim@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e"
|
||||
@@ -2795,6 +2837,11 @@ array-union@^1.0.2:
|
||||
dependencies:
|
||||
array-uniq "^1.0.1"
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
array-uniq@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
||||
@@ -2954,7 +3001,7 @@ babel-jest@^24.9.0:
|
||||
chalk "^2.4.2"
|
||||
slash "^2.0.0"
|
||||
|
||||
babel-jest@^26.2.2, babel-jest@^26.6.3:
|
||||
babel-jest@^26.6.3:
|
||||
version "26.6.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056"
|
||||
integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==
|
||||
@@ -4054,6 +4101,13 @@ debug@^3.1.0, debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.3.1, debug@^4.3.2:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debuglog@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||
@@ -4193,6 +4247,13 @@ dir-glob@^2.2.2:
|
||||
dependencies:
|
||||
path-type "^3.0.0"
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
doctrine@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
|
||||
@@ -4283,11 +4344,6 @@ emittery@^0.7.1:
|
||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
|
||||
integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==
|
||||
|
||||
"emoji-regex@>=6.0.0 <=6.1.1":
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"
|
||||
integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
@@ -4489,6 +4545,17 @@ eslint-import-resolver-node@^0.3.6:
|
||||
debug "^3.2.7"
|
||||
resolve "^1.20.0"
|
||||
|
||||
eslint-import-resolver-typescript@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a"
|
||||
integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
glob "^7.1.7"
|
||||
is-glob "^4.0.1"
|
||||
resolve "^1.20.0"
|
||||
tsconfig-paths "^3.9.0"
|
||||
|
||||
eslint-module-utils@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
|
||||
@@ -4552,6 +4619,13 @@ eslint-plugin-import@^2.24.2:
|
||||
resolve "^1.20.0"
|
||||
tsconfig-paths "^3.11.0"
|
||||
|
||||
eslint-plugin-jest@^25.3.0:
|
||||
version "25.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.3.0.tgz#6c04bbf13624a75684a05391a825b58e2e291950"
|
||||
integrity sha512-79WQtuBsTN1S8Y9+7euBYwxIOia/k7ykkl9OCBHL3xuww5ecursHy/D8GCIlvzHVWv85gOkS5Kv6Sh7RxOgK1Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "^5.0.0"
|
||||
|
||||
eslint-plugin-jsx-a11y@^6.2.3:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
|
||||
@@ -4598,7 +4672,7 @@ eslint-plugin-react@^7.14.3:
|
||||
resolve "^1.18.1"
|
||||
string.prototype.matchall "^4.0.2"
|
||||
|
||||
eslint-scope@^5.0.0:
|
||||
eslint-scope@^5.0.0, eslint-scope@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
|
||||
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
|
||||
@@ -4620,11 +4694,28 @@ eslint-utils@^2.0.0:
|
||||
dependencies:
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
eslint-utils@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
|
||||
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
|
||||
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
|
||||
|
||||
eslint-visitor-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
|
||||
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
|
||||
|
||||
eslint-visitor-keys@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2"
|
||||
integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
|
||||
|
||||
eslint@^6.1.0, eslint@^6.8.0:
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
|
||||
@@ -4897,6 +4988,17 @@ fast-glob@^2.2.6:
|
||||
merge2 "^1.2.3"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
fast-glob@^3.1.1:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
||||
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
@@ -4907,6 +5009,13 @@ fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
||||
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
fault@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
|
||||
@@ -5312,13 +5421,6 @@ gitconfiglocal@^1.0.0:
|
||||
dependencies:
|
||||
ini "^1.3.2"
|
||||
|
||||
github-slugger@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9"
|
||||
integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==
|
||||
dependencies:
|
||||
emoji-regex ">=6.0.0 <=6.1.1"
|
||||
|
||||
glob-parent@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
|
||||
@@ -5327,7 +5429,7 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-parent@^5.0.0:
|
||||
glob-parent@^5.0.0, glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@@ -5351,6 +5453,18 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.7:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
|
||||
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
@@ -5373,6 +5487,18 @@ globalyzer@0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
|
||||
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
|
||||
|
||||
globby@^11.0.4:
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
|
||||
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
|
||||
dependencies:
|
||||
array-union "^2.1.0"
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.1.1"
|
||||
ignore "^5.1.4"
|
||||
merge2 "^1.3.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
globby@^9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
|
||||
@@ -5661,6 +5787,11 @@ ignore@^4.0.3, ignore@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
ignore@^5.1.4:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
|
||||
|
||||
import-fresh@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
||||
@@ -6011,6 +6142,13 @@ is-glob@^4.0.0, is-glob@^4.0.1:
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-hexadecimal@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
||||
@@ -6515,11 +6653,6 @@ jest-environment-node@^26.6.2:
|
||||
jest-mock "^26.6.2"
|
||||
jest-util "^26.6.2"
|
||||
|
||||
jest-environment-vscode@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-environment-vscode/-/jest-environment-vscode-1.0.0.tgz#96367fe8531047e64359e0682deafc973bfaea91"
|
||||
integrity sha512-VKlj5j5pNurFEwWPaDiX1kBgmhWqcJTAZsvEX1x5lh0/+5myjk+qipEs/dPJVRbBPb3XFxiR48XzGn+wOU7SSQ==
|
||||
|
||||
jest-extended@^0.11.5:
|
||||
version "0.11.5"
|
||||
resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-0.11.5.tgz#f063b3f1eaadad8d7c13a01f0dfe0f538d498ccf"
|
||||
@@ -7717,7 +7850,7 @@ merge-stream@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||
|
||||
merge2@^1.2.3:
|
||||
merge2@^1.2.3, merge2@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
@@ -7749,6 +7882,14 @@ micromatch@^4.0.2:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.0.5"
|
||||
|
||||
micromatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
||||
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
|
||||
dependencies:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
mime-db@1.46.0:
|
||||
version "1.46.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
|
||||
@@ -8665,6 +8806,11 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
||||
|
||||
picomatch@^2.2.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
||||
|
||||
pify@^2.0.0, pify@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
@@ -8935,6 +9081,11 @@ query-string@^6.13.8:
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
quick-lru@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
|
||||
@@ -9423,6 +9574,11 @@ retry@^0.10.0:
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
@@ -9508,6 +9664,13 @@ run-async@^2.2.0, run-async@^2.4.0:
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
|
||||
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
|
||||
@@ -9618,6 +9781,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
serialize-javascript@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
|
||||
@@ -10617,7 +10787,7 @@ tslib@^2.0.0, tslib@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
tsutils@^3.17.1, tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
|
||||
Reference in New Issue
Block a user