mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
34 Commits
v0.16.0
...
fix-link-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aff88827f | ||
|
|
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,24 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -24,9 +24,9 @@
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"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...
|
||||
@@ -218,6 +218,8 @@ 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>
|
||||
</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 |
|
||||
|
||||
|
||||
@@ -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.2"
|
||||
}
|
||||
|
||||
0
packages/foam-vscode/.test-workspace/.keep
Normal file
0
packages/foam-vscode/.test-workspace/.keep
Normal file
@@ -4,6 +4,43 @@ 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.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.2",
|
||||
"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",
|
||||
@@ -377,7 +399,6 @@
|
||||
"@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 +409,10 @@
|
||||
"@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-plugin-import": "^2.24.2",
|
||||
"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 +426,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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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', () => {
|
||||
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) => {
|
||||
|
||||
@@ -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) {
|
||||
@@ -128,21 +133,32 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
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':
|
||||
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 +177,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;
|
||||
@@ -201,6 +217,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 +272,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 +282,7 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title === '') {
|
||||
note.title = URI.getBasename(note.uri);
|
||||
note.title = note.uri.getName();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -254,7 +317,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 +358,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 +377,7 @@ export function createMarkdownParser(
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
sectionsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
|
||||
@@ -327,7 +391,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;
|
||||
@@ -344,6 +408,7 @@ export function createMarkdownParser(
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: '',
|
||||
sections: [],
|
||||
tags: [],
|
||||
links: [],
|
||||
definitions: [],
|
||||
@@ -384,10 +449,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +521,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 +534,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 +543,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 +554,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,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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[] = [];
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
let [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
|
||||
*/
|
||||
|
||||
@@ -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,16 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public listById(resourceId: string): Resource[] {
|
||||
if (!hasExtension(resourceId)) {
|
||||
resourceId = resourceId + '.md';
|
||||
}
|
||||
resourceId = normalize(resourceId);
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
let needle = normalize('/' + identifier);
|
||||
let mdNeedle = getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
let 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 +79,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;
|
||||
let [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 +149,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);
|
||||
|
||||
@@ -39,8 +39,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 +76,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 +123,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');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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,5 +1,5 @@
|
||||
import { Uri } 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';
|
||||
@@ -122,12 +122,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 +142,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',
|
||||
@@ -170,25 +168,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 = [];
|
||||
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();
|
||||
@@ -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,7 +135,7 @@ 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
|
||||
|
||||
@@ -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(
|
||||
@@ -102,4 +108,46 @@ describe('Link Completion', () => {
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sections for other notes', async () => {
|
||||
const { uri } = await createFile('[[file-name#');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 12)
|
||||
);
|
||||
|
||||
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 SectionCompletionProvider(ws);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(9, 3)
|
||||
);
|
||||
|
||||
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,75 @@ 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.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
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) {}
|
||||
@@ -39,21 +105,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) {
|
||||
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 +138,7 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.Interface
|
||||
);
|
||||
item.insertText = uri.path;
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
}
|
||||
);
|
||||
@@ -74,7 +150,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 +166,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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
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;
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -124,6 +128,10 @@ foam_template: # foam template metadata
|
||||
expect(await resolver.resolve('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`;
|
||||
|
||||
@@ -91,7 +84,7 @@ export const NoteFactory = {
|
||||
filepathFallbackURI?: URI,
|
||||
templateFallbackText: string = ''
|
||||
): Promise<void> => {
|
||||
const templateText = existsSync(URI.toFsPath(templateUri))
|
||||
const templateText = existsSync(templateUri.toFsPath())
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
@@ -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,
|
||||
@@ -204,8 +197,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 +226,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 +246,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;
|
||||
}
|
||||
|
||||
@@ -267,8 +260,7 @@ export async function determineNewNoteFilepath(
|
||||
}
|
||||
|
||||
const defaultName = await resolver.resolve('FOAM_TITLE');
|
||||
const defaultFilepath = URI.joinPath(
|
||||
getCurrentEditorDirectory(),
|
||||
const defaultFilepath = getCurrentEditorDirectory().joinPath(
|
||||
`${defaultName}.md`
|
||||
);
|
||||
return defaultFilepath;
|
||||
|
||||
@@ -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,28 @@
|
||||
* they will make direct use of the vscode API to be invoked as commands, create editors,
|
||||
* and so on..
|
||||
*/
|
||||
|
||||
/* 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';
|
||||
|
||||
import path from 'path';
|
||||
import { runCLI } from '@jest/core';
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
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';
|
||||
|
||||
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,46 @@
|
||||
* and so on..
|
||||
*/
|
||||
|
||||
/* 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';
|
||||
|
||||
import path from 'path';
|
||||
import { runCLI } from '@jest/core';
|
||||
import { cleanWorkspace } from './test-utils-vscode';
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
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';
|
||||
|
||||
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 +75,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)
|
||||
|
||||
@@ -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 = [
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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": "\\]\\]"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
readme.md
15
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,8 @@ 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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -2250,11 +2250,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"
|
||||
@@ -2954,7 +2949,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==
|
||||
@@ -4283,11 +4278,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"
|
||||
@@ -5312,13 +5302,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"
|
||||
@@ -6515,11 +6498,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"
|
||||
|
||||
Reference in New Issue
Block a user