Compare commits

...

8 Commits

Author SHA1 Message Date
Riccardo Ferretti
b1f76bb653 v0.21.0 2023-02-16 14:20:49 -06:00
Riccardo Ferretti
d4bc16b9bd Preparation for next release 2023-02-16 14:20:35 -06:00
Riccardo
882b0b6012 Use filter in open-resource command (#1161)
* Added title param in create-note command

* Added utility functions for commands

* Use create-note command when dealing with placeholders

* Updated open-resource command to new pattern

* Pass workspace.isTrusted to createFilter

* Fixed bug when finding absolute paths in workspace without providing basedir

* open-resource command can also receive `uri` param to skip filtering step

* added tests

* added docs
2023-02-15 22:53:32 +01:00
Riccardo Ferretti
048623d910 v0.20.8 2023-02-10 17:10:38 -06:00
Riccardo Ferretti
f2fbe927ae Preparation for next release 2023-02-10 17:10:22 -06:00
Riccardo
d0ee71be1b Updated a bunch of dependencies (#1160)
* Updated typescript and vscode engine version to support workspace trust

* updates tsdx to dts, and updated a other deps too

* updated eslint configuration

* Updated node version

* Update lerna

* Updated github action configuration

* removed glob library
2023-02-11 00:08:23 +01:00
Riccardo
2a14dc0c57 Added resource filters (#1158)
* Added note filter and a few tests

* Added expression for filters and trusted workspace support

* Consolidate `include` and `exclude` into `path` parameter

* Added documentation
2023-02-10 12:17:42 +01:00
Riccardo Ferretti
745acbabd3 Fixed VS Code installs badge 2023-02-01 13:33:33 +01:00
56 changed files with 6474 additions and 7782 deletions

View File

@@ -6,21 +6,22 @@
"sourceType": "module"
},
"env": { "node": true, "es6": true },
"plugins": ["@typescript-eslint", "import", "jest"],
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jest/recommended"
],
"rules": {
"no-redeclare": "off",
"no-unused-vars": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/no-unused-vars": "warn",
"import/no-extraneous-dependencies": [
"error",
{

View File

@@ -11,16 +11,16 @@ on:
jobs:
lint:
name: Lint
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v1
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '12'
node-version: '18'
- name: Restore Dependencies and VS Code test instance
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@@ -36,7 +36,7 @@ jobs:
name: Build and Test
strategy:
matrix:
os: [macos-10.15, ubuntu-18.04, windows-2019]
os: [macos-12, ubuntu-22.04, windows-2022]
runs-on: ${{ matrix.os }}
env:
OS: ${{ matrix.os }}
@@ -44,11 +44,11 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '12'
node-version: '18'
- name: Restore Dependencies and VS Code test instance
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules

View File

@@ -9,16 +9,19 @@ In particular, some commands can be very customizible and can help with custom w
This command creates a note.
Although it works fine on its own, it can be customized to achieve various use cases.
Here are the settings available for the command:
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
- templatePath: The path of the template to use. If relative it will be resolved against the workspace root.
- title: The title of the note (that is, the `FOAM_TITLE` variable)
- text: The text to use for the note. If also a template is provided, the template has precedence
- variables: Variables to use in the text or template (e.g. `FOAM_TITLE`)
- date: The date used to resolve the FOAM_DATE_* variables. in `YYYY-MM-DD` format
- variables: Variables to use in the text or template
- date: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
- onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel': What to do in case the target file already exists
To customize a command and associate a key binding to it, open the key binding settings and add the appropriate configuration, here are some examples:
- Create a note called `test note.md` with some text. If the note already exists, ask for a new name
```
{
"key": "alt+f",
@@ -32,6 +35,7 @@ To customize a command and associate a key binding to it, open the key binding s
```
- Create a note following the `weekly-note.md` template. If the note already exists, open it
```
{
"key": "alt+g",
@@ -43,3 +47,27 @@ To customize a command and associate a key binding to it, open the key binding s
}
```
## foam-vscode.open-resource command
This command opens a resource.
Normally it receives a `URI`, which identifies the resource to open.
It is also possible to pass in a filter, which will be run against the workspace resources to find one or more matches.
- If there is one match, it will be opened
- If there is more than one match, a quick pick will show up allowing the user to select the desired resource
Examples:
```
{
"key": "alt+f",
"command": "foam-vscode.open-resource",
"args": {
"filter": {
"title": "Weekly Note*"
}
}
}
```

View File

@@ -0,0 +1,42 @@
# Resource Filters
Resource filters can be passed to some Foam commands to limit their scope.
A filter supports the following parameters:
- `tag`: include a resource if it has the given tag (e.g. `{"tag": "#research"}`)
- `type`: include a resource if it is of the given type (e.g. `{"type": "daily-note"}`)
- `path`: include a resource if its path matches the given regex (e.g. `{"path": "/projects/*"}`). **Note that this parameter supports regex and not globs.**
- `expression`: include a resource if it makes the given expression `true`, where `resource` represents the resource being evaluated (e.g. `{"expression": "resource.type ==='weekly-note'"}`)
- `title`: include a resource if the title matches the given regex (e.g. `{"title": "Team meeting:*"}`)
A filter also supports some logical operators:
- `and`: include a resource if it matches all the sub-parameters (e.g `{"and": [{"tag": "#research"}, {"title": "Paper *"}]}`)
- `or`: include a resource if it matches any of the sub-parameters (e.g `{"or": [{"tag": "#research"}, {"title": "Paper *"}]}`)
- `not`: invert the result of the nested filter (e.g. `{"not": {"type": "daily-note"}}`)
Here is an example of a complex filter, for example to show the Foam graph only of a subset of the workspace:
```
{
"key": "alt+f",
"command": "foam-vscode.show-graph",
"args": {
"filter": {
"and": [
{
"or": [
{ "type": 'daily-note' },
{ "type": 'weekly-note' },
{ "path": '/projects/*' },
],
"not": {
{ "tag": '#b' },
},
},
],
}
}
}
```

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.20.7"
"version": "0.21.0"
}

View File

@@ -22,10 +22,10 @@
},
"devDependencies": {
"all-contributors-cli": "^6.16.1",
"lerna": "^3.22.1"
"lerna": "^6.4.1"
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"husky": {
"hooks": {
@@ -33,6 +33,7 @@
}
},
"prettier": {
"arrowParens": "avoid",
"printWidth": 80,
"semi": true,
"singleQuote": true,

View File

@@ -4,6 +4,18 @@ 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.21.0] - 2023-02-16
Features:
- Added support for filters for the `foam-vscode.open-resource` command (#1161)
## [0.20.8] - 2023-02-10
Internal:
- Updated most dependencies (#1160)
## [0.20.7] - 2023-01-31
Fixes and Improvements:
@@ -58,7 +70,7 @@ Fixes and Improvements:
## [0.20.0] - 2022-09-30
New Features:
Features:
- Added `foam-vscode.create-note` command, which can be very customized for several use cases (#1076)
@@ -114,7 +126,7 @@ Internal:
## [0.19.0] - 2022-07-07
New Features:
Features:
- Support for attachments (PDF) and images (#1027)
- Support for opening day notes for other days as well (#1026, thanks @alper)
@@ -594,7 +606,7 @@ Fixes and Improvements:
## [0.7.1] - 2020-11-27
New Feature:
Features:
- Foam logging can now be inspected in VsCode Output panel (#377)
@@ -606,7 +618,7 @@ Fixes and Improvements:
## [0.7.0] - 2020-11-25
New Features:
Features:
- Foam stays in sync with changes in notes
- Dataviz: Added multiple selection in graph (shift+click on node)
@@ -618,7 +630,7 @@ Fixes and Improvements:
## [0.6.0] - 2020-11-19
New features:
Features:
- Added command to create notes from templates (#115 - Thanks @ingalless)
@@ -633,7 +645,7 @@ Fixes and Improvements:
## [0.5.0] - 2020-11-09
New features:
Features:
- Added tags panel (#311)
@@ -647,7 +659,7 @@ Fixes and Improvements:
## [0.4.0] - 2020-10-28
New features:
Features:
- Added `Foam: Show Graph` command
- Added date snippets (/+1d, ...) to create wikilinks to dates in daily note format
@@ -675,7 +687,7 @@ Fixes and improvements:
## [0.3.0] - 2020-07-25
New features:
Features:
- [Daily Notes](https://foambubble.github.io/foam/daily-notes)
- [Janitor](https://foambubble.github.io/foam/workspace-janitor) for updating headings and link references across your workspace

View File

@@ -2,7 +2,7 @@
[![Version](https://img.shields.io/visual-studio-marketplace/v/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Downloads](https://img.shields.io/visual-studio-marketplace/d/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode?label=VS%20Code%20Installs)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.20.7",
"version": "0.21.0",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -22,6 +22,12 @@
"workspaceContains:.vscode/foam.json"
],
"main": "./out/extension.js",
"capabilities": {
"untrustedWorkspaces": {
"supported": "limited",
"description": "No expressions are allowed in filters."
}
},
"contributes": {
"markdown.markdownItPlugins": true,
"markdown.previewStyles": [
@@ -457,7 +463,7 @@
"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",
"lint": "dts lint src",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
@@ -471,32 +477,31 @@
},
"devDependencies": {
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.157",
"@types/markdown-it": "^12.0.1",
"@types/micromatch": "^4.0.1",
"@types/node": "^13.11.0",
"@types/picomatch": "^2.2.1",
"@types/remove-markdown": "^0.1.1",
"@types/vscode": "^1.47.1",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"esbuild": "^0.14.45",
"eslint": "^6.8.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^25.3.0",
"glob": "^7.1.6",
"@types/vscode": "^1.70.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"dts-cli": "^1.6.3",
"esbuild": "^0.17.7",
"eslint": "^8.33.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"husky": "^4.2.5",
"jest": "^26.2.2",
"jest-extended": "^0.11.5",
"jest": "^27.5.1",
"jest-extended": "^3.2.3",
"markdown-it": "^12.0.4",
"micromatch": "^4.0.2",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"tsdx": "^0.13.2",
"ts-jest": "^27.1.5",
"tslib": "^2.0.0",
"typescript": "^3.9.5",
"typescript": "^4.9.5",
"vscode-test": "^1.3.0",
"wait-for-expect": "^3.0.2"
},
@@ -506,7 +511,7 @@
"github-slugger": "^1.4.0",
"gray-matter": "^4.0.2",
"lodash": "^4.17.21",
"lru-cache": "^7.12.0",
"lru-cache": "^7.14.1",
"markdown-it-regex": "^0.2.0",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.2",

View File

@@ -22,12 +22,7 @@ describe('Graph', () => {
const noteD = createTestNote({ uri: '/Page D.md' });
const noteE = createTestNote({ uri: '/page e.md' });
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE);
workspace.set(noteA).set(noteB).set(noteC).set(noteD).set(noteE);
const graph = FoamGraph.fromWorkspace(workspace);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
@@ -69,9 +64,7 @@ describe('Graph', () => {
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteA.uri)).toEqual([
{
@@ -95,9 +88,7 @@ describe('Graph', () => {
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
@@ -165,9 +156,7 @@ describe('Graph', () => {
uri: '/path/to/more/attachment-b.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentB);
ws.set(noteA).set(attachmentA).set(attachmentB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
@@ -189,9 +178,7 @@ describe('Graph', () => {
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentABis);
ws.set(noteA).set(attachmentA).set(attachmentABis);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
@@ -211,9 +198,7 @@ describe('Graph', () => {
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentABis)
.set(attachmentA);
ws.set(noteA).set(attachmentABis).set(attachmentA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
@@ -323,9 +308,7 @@ describe('Regenerating graph after workspace changes', () => {
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
ws.set(noteA).set(noteB).set(noteC);
let graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
@@ -512,9 +495,7 @@ describe('Updating graph on workspace state', () => {
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
ws.set(noteA).set(noteB).set(noteC);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);

View File

@@ -26,7 +26,8 @@ import * as pathUtils from '../utils/path';
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
const _regexp =
/^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
export class URI {
readonly scheme: string;

View File

@@ -87,6 +87,13 @@ describe('Workspace resources', () => {
const res = ws.find('test-file#my-section');
expect(res.uri.fragment).toEqual('my-section');
});
it('should find absolute files even when no basedir is provided', () => {
const noteA = createTestNote({ uri: '/a/path/to/file.md' });
const ws = createTestWorkspace().set(noteA);
expect(ws.find('/a/path/to/file.md').uri.path).toEqual(noteA.uri.path);
});
});
describe('Identifier computation', () => {
@@ -100,10 +107,7 @@ describe('Identifier computation', () => {
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
const ws = new FoamWorkspace().set(first).set(second).set(third);
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
@@ -120,10 +124,7 @@ describe('Identifier computation', () => {
const third = createTestNote({
uri: '/another/path/for/page-a.md',
});
const ws = new FoamWorkspace()
.set(first)
.set(second)
.set(third);
const ws = new FoamWorkspace().set(first).set(second).set(third);
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
'to/page-a#section name'
@@ -176,11 +177,7 @@ describe('Identifier computation', () => {
const noteD = createTestNote({ uri: '/path/to/note-d.md' });
const noteABis = createTestNote({ uri: '/path/to/another/note-a.md' });
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD);
workspace.set(noteA).set(noteB).set(noteC).set(noteD);
expect(workspace.getIdentifier(noteABis.uri)).toEqual('another/note-a');
expect(
workspace.getIdentifier(noteABis.uri, [noteB.uri, noteA.uri])

View File

@@ -121,14 +121,16 @@ export class FoamWorkspace implements IDisposable {
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;
const candidates = [path, path + '.md'];
for (const candidate of candidates) {
const searchKey = isAbsolute(candidate)
? candidate
: isSome(baseUri)
? baseUri.resolve(candidate).path
: null;
resource = this._resources.get(normalize(searchKey));
if (resource) {
break;
}
}
}

View File

@@ -51,10 +51,7 @@ describe('Link resolution', () => {
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b 2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
});
@@ -63,10 +60,7 @@ describe('Link resolution', () => {
const noteA = createNoteFromMarkdown('Link to [[page b]]', '/page-a.md');
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
@@ -80,10 +74,7 @@ describe('Link resolution', () => {
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.set(noteB3);
ws.set(noteA).set(noteB1).set(noteB2).set(noteB3);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteB3.uri);
@@ -97,10 +88,7 @@ describe('Link resolution', () => {
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
workspace.set(noteA).set(noteB).set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(workspace.resolveLink(noteA, noteA.links[1])).toEqual(noteB.uri);
});
@@ -157,9 +145,7 @@ describe('Link resolution', () => {
],
});
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const ws = createTestWorkspace().set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
noteB.uri.withFragment('section')
@@ -258,10 +244,7 @@ describe('Link resolution', () => {
);
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD);
ws.set(noteA).set(noteB).set(noteC).set(noteD);
expect(ws.resolveLink(noteB, noteB.links[0])).toEqual(noteA.uri);
expect(ws.resolveLink(noteC, noteC.links[0])).toEqual(noteA.uri);

View File

@@ -0,0 +1,116 @@
import { Logger } from '../utils/log';
import { createTestNote } from '../../test/test-utils';
import { createFilter } from './resource-filter';
Logger.setLevel('error');
describe('Resource Filter', () => {
describe('Filter parameters', () => {
it('should support expressions when code execution is enabled', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
expression: 'resource.type === "type-1"',
},
true
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeFalsy();
});
it('should not allow expressions when code execution is not enabled', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
expression: 'resource.type === "type-1"',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
});
it('should support resource type', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
type: 'type-1',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeFalsy();
});
it('should support resource title', () => {
const noteA = createTestNote({
uri: 'note-a.md',
title: 'title-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
title: 'title-2',
});
const noteC = createTestNote({
uri: 'note-c.md',
title: 'another title',
});
const filter = createFilter(
{
title: '^title',
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
expect(filter(noteC)).toBeFalsy();
});
});
describe('Filter operators', () => {
it('should support the OR operator', () => {
const noteA = createTestNote({
uri: 'note-a.md',
type: 'type-1',
});
const noteB = createTestNote({
uri: 'note-b.md',
type: 'type-2',
});
const filter = createFilter(
{
or: [{ type: 'type-1' }, { type: 'type-2' }],
},
false
);
expect(filter(noteA)).toBeTruthy();
expect(filter(noteB)).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,77 @@
import { negate } from 'lodash';
import { Resource } from '../model/note';
export interface FilterDescriptor
extends FilterDescriptorOp,
FilterDescriptorParam {}
interface FilterDescriptorOp {
and?: FilterDescriptor[];
or?: FilterDescriptor[];
not?: FilterDescriptor;
}
interface FilterDescriptorParam {
/**
* A regex of the path to include
*/
path?: string;
/**
* A tag
*/
tag?: string;
/**
* A note type
*/
type?: string;
/**
* The title of the note
*/
title?: string;
/**
* An expression to evaluate to JS, use `resource` to reference the resource object
*/
expression?: string;
}
type ResourceFilter = (r: Resource) => boolean;
export function createFilter(
filter: FilterDescriptor,
enableCode: boolean
): ResourceFilter {
filter = filter ?? {};
const expressionFn =
enableCode && filter.expression
? resource => eval(filter.expression) // eslint-disable-line no-eval
: undefined;
return resource => {
if (expressionFn && !expressionFn(resource)) {
return false;
}
if (filter.type && resource.type !== filter.type) {
return false;
}
if (filter.title && !resource.title.match(filter.title)) {
return false;
}
if (filter.and) {
return filter.and
.map(pred => createFilter(pred, enableCode))
.every(fn => fn(resource));
}
if (filter.or) {
return filter.or
.map(pred => createFilter(pred, enableCode))
.some(fn => fn(resource));
}
if (filter.not) {
return negate(createFilter(filter.not, enableCode))(resource);
}
return true;
};
}

View File

@@ -21,7 +21,4 @@ export function isNumeric(value: string): boolean {
}
export const hash = (text: string) =>
crypto
.createHash('sha1')
.update(text)
.digest('hex');
crypto.createHash('sha1').update(text).digest('hex');

View File

@@ -1,6 +1,8 @@
import { isSome } from './core';
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
const HASHTAG_REGEX =
/(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
const WORD_REGEX =
/(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
export const extractHashtags = (
text: string

View File

@@ -1,72 +1,72 @@
import { workspace } from 'vscode';
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
import { isWindows } from './core/common/platform';
import {
cleanWorkspace,
closeEditors,
createFile,
deleteFile,
showInEditor,
withModifiedFoamConfiguration,
} from './test/test-utils-vscode';
import { fromVsCodeUri } from './utils/vsc-utils';
describe('getDailyNotePath', () => {
const date = new Date('2021-02-07T00:00:00Z');
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const isoDate = `${year}-0${month}-0${day}`;
test('Adds the root directory to relative directories', async () => {
const config = 'journal';
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(config, `${isoDate}.md`);
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
);
});
test('Uses absolute directories without modification', async () => {
const config = isWindows
? 'C:\\absolute_path\\journal'
: '/absolute_path/journal';
const expectedPath = isWindows
? `${config}\\${isoDate}.md`
: `${config}/${isoDate}.md`;
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
);
});
});
describe('Daily note template', () => {
it('Uses the daily note variables in the template', async () => {
const targetDate = new Date(2021, 8, 12);
const template = await createFile(
// eslint-disable-next-line no-template-curly-in-string
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
['.foam', 'templates', 'daily-note.md']
);
const uri = getDailyNotePath(targetDate);
await createDailyNoteIfNotExists(targetDate);
const doc = await showInEditor(uri);
const content = doc.editor.document.getText();
expect(content).toEqual('hello September 12 hello');
await deleteFile(template.uri);
});
afterAll(async () => {
await cleanWorkspace();
await closeEditors();
});
});
import { workspace } from 'vscode';
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
import { isWindows } from './core/common/platform';
import {
cleanWorkspace,
closeEditors,
createFile,
deleteFile,
showInEditor,
withModifiedFoamConfiguration,
} from './test/test-utils-vscode';
import { fromVsCodeUri } from './utils/vsc-utils';
describe('getDailyNotePath', () => {
const date = new Date('2021-02-07T00:00:00Z');
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const isoDate = `${year}-0${month}-0${day}`;
test('Adds the root directory to relative directories', async () => {
const config = 'journal';
const expectedPath = fromVsCodeUri(
workspace.workspaceFolders[0].uri
).joinPath(config, `${isoDate}.md`);
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
);
});
test('Uses absolute directories without modification', async () => {
const config = isWindows
? 'C:\\absolute_path\\journal'
: '/absolute_path/journal';
const expectedPath = isWindows
? `${config}\\${isoDate}.md`
: `${config}/${isoDate}.md`;
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
);
});
});
describe('Daily note template', () => {
it('Uses the daily note variables in the template', async () => {
const targetDate = new Date(2021, 8, 12);
const template = await createFile(
// eslint-disable-next-line no-template-curly-in-string
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
['.foam', 'templates', 'daily-note.md']
);
const uri = getDailyNotePath(targetDate);
await createDailyNoteIfNotExists(targetDate);
const doc = await showInEditor(uri);
const content = doc.editor.document.getText();
expect(content).toEqual('hello September 12 hello');
await deleteFile(template.uri);
});
afterAll(async () => {
await cleanWorkspace();
await closeEditors();
});
});

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import { workspace, ExtensionContext, window, commands } from 'vscode';
import { MarkdownResourceProvider } from './core/services/markdown-provider';
import { bootstrap } from './core/model/foam';
@@ -27,11 +29,8 @@ export async function activate(context: ExtensionContext) {
// Prepare Foam
const excludes = getIgnoredFilesSetting().map(g => g.toString());
const {
matcher,
dataStore,
excludePatterns,
} = await createMatcherAndDataStore(excludes);
const { matcher, dataStore, excludePatterns } =
await createMatcherAndDataStore(excludes);
Logger.info('Loading from directories:');
for (const folder of workspace.workspaceFolders) {

View File

@@ -17,7 +17,7 @@ describe('create-note-from-default-template command', () => {
'foam-vscode.create-note-from-default-template'
);
expect(spy).toBeCalledWith({
expect(spy).toHaveBeenCalledWith({
prompt: `Enter a title for the new note`,
value: 'Title of my New Note',
validateInput: expect.anything(),

View File

@@ -17,7 +17,7 @@ describe('create-note-from-default-template command', () => {
'foam-vscode.create-note-from-default-template'
);
expect(spy).toBeCalledWith({
expect(spy).toHaveBeenCalledWith({
prompt: `Enter a title for the new note`,
value: 'Title of my New Note',
validateInput: expect.anything(),

View File

@@ -14,7 +14,7 @@ describe('create-note-from-template command', () => {
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(['Yes', 'No'], {
expect(spy).toHaveBeenCalledWith(['Yes', 'No'], {
placeHolder:
'No templates available. Would you like to create one instead?',
});
@@ -38,7 +38,7 @@ describe('create-note-from-template command', () => {
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
[
expect.objectContaining({ label: 'template-a.md' }),
expect.objectContaining({ label: 'template-b.md' }),
@@ -71,7 +71,7 @@ Template A
await commands.executeCommand('foam-vscode.create-note-from-template');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
[
expect.objectContaining({
label: 'My Template',

View File

@@ -22,7 +22,7 @@ describe('create-note command', () => {
.mockImplementationOnce(jest.fn(() => Promise.resolve('Test note')));
await commands.executeCommand('foam-vscode.create-note');
expect(spy).toBeCalled();
expect(spy).toHaveBeenCalled();
const target = asAbsoluteWorkspaceUri(URI.file('Test note.md'));
expectSameUri(target, window.activeTextEditor?.document.uri);
await deleteFile(target);
@@ -124,7 +124,7 @@ describe('create-note command', () => {
text: 'test ask',
onFileExists: 'ask',
});
expect(spy).toBeCalled();
expect(spy).toHaveBeenCalled();
await deleteFile(target);
});
@@ -183,7 +183,7 @@ describe('create-note command', () => {
text: 'test asking',
onRelativeNotePath: 'ask',
});
expect(spy).toBeCalled();
expect(spy).toHaveBeenCalled();
await deleteFile(base);
});

View File

@@ -11,6 +11,7 @@ import { Foam } from '../../core/model/foam';
import { Resolver } from '../../services/variable-resolver';
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
import { isSome } from '../../core/utils';
import { CommandDescriptor } from '../../utils/commands';
interface CreateNoteArgs {
/**
@@ -34,11 +35,15 @@ interface CreateNoteArgs {
/**
* Variables to use in the text or template
*/
variables?: Map<string, string>;
variables?: { [key: string]: string };
/**
* The date used to resolve the FOAM_DATE_* variables. in YYYY-MM-DD format
*/
date?: string;
/**
* The title of the note (translates into the FOAM_TITLE variable)
*/
title?: string;
/**
* What to do in case the target file already exists
*/
@@ -64,6 +69,9 @@ async function createNote(args: CreateNoteArgs) {
new Map(Object.entries(args.variables ?? {})),
date
);
if (args.title) {
resolver.define('FOAM_TITLE', args.title);
}
const text = args.text ?? DEFAULT_NEW_NOTE_TEXT;
const noteUri = args.notePath && URI.file(args.notePath);
let templateUri: URI;
@@ -101,12 +109,20 @@ async function createNote(args: CreateNoteArgs) {
export const CREATE_NOTE_COMMAND = {
command: 'foam-vscode.create-note',
title: 'Foam: Create Note',
asURI: (args: CreateNoteArgs) =>
vscode.Uri.parse(`command:${CREATE_NOTE_COMMAND.command}`).with({
query: encodeURIComponent(JSON.stringify(args)),
}),
forPlaceholder: (
placeholder: string,
extra: Partial<CreateNoteArgs> = {}
): CommandDescriptor<CreateNoteArgs> => {
return {
name: CREATE_NOTE_COMMAND.command,
params: {
title: placeholder,
notePath: placeholder,
...extra,
},
};
},
};
const feature: FoamFeature = {

View File

@@ -9,7 +9,7 @@ describe('open-daily-note-for-date command', () => {
await commands.executeCommand('foam-vscode.open-daily-note-for-date');
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
expect.objectContaining([
expect.objectContaining({
label: expect.stringContaining(

View File

@@ -0,0 +1,100 @@
import dateFormat from 'dateformat';
import { commands, window } from 'vscode';
import { CommandDescriptor } from '../../utils/commands';
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
import * as filter from '../../core/services/resource-filter';
import { URI } from '../../core/model/uri';
import { closeEditors, createFile } from '../../test/test-utils-vscode';
import { deleteFile } from '../../services/editor';
import waitForExpect from 'wait-for-expect';
describe('open-resource command', () => {
beforeEach(async () => {
await jest.resetAllMocks();
await closeEditors();
});
it('URI param has precedence over filter', async () => {
const spy = jest.spyOn(filter, 'createFilter');
const noteA = await createFile('Note A for open command');
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: noteA.uri,
filter: { title: 'note 1' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
expect(spy).not.toHaveBeenCalled();
await deleteFile(noteA.uri);
});
it('URI param accept URI object, or path', async () => {
const noteA = await createFile('Note A for open command');
const uriCommand: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: URI.file('path/to/file.md'),
},
};
await commands.executeCommand(uriCommand.name, uriCommand.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
await closeEditors();
const pathCommand: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
uri: URI.file('path/to/file.md'),
},
};
await commands.executeCommand(pathCommand.name, pathCommand.params);
waitForExpect(() => {
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
});
await deleteFile(noteA.uri);
});
it('User is notified if no resource is found', async () => {
const spy = jest.spyOn(window, 'showInformationMessage');
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
filter: { title: 'note 1 with no existing title' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(spy).toHaveBeenCalled();
});
});
it('filter with multiple results will show a quick pick', async () => {
const spy = jest
.spyOn(window, 'showQuickPick')
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
const command: CommandDescriptor<OpenResourceArgs> = {
name: OPEN_COMMAND.command,
params: {
filter: { title: '.*' },
},
};
await commands.executeCommand(command.name, command.params);
waitForExpect(() => {
expect(spy).toHaveBeenCalled();
});
});
});

View File

@@ -1,68 +1,127 @@
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { URI } from '../../core/model/uri';
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
import { NoteFactory } from '../../services/templates';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { Foam } from '../../core/model/foam';
import {
createFilter,
FilterDescriptor,
} from '../../core/services/resource-filter';
import { CommandDescriptor } from '../../utils/commands';
import { FoamWorkspace } from '../../core/model/workspace';
import { Resource } from '../../core/model/note';
import { isSome, isNone } from '../../core/utils';
import { Logger } from '../../core/utils/log';
export interface OpenResourceArgs {
/**
* The URI of the resource to open.
* If present the `filter` param is ignored
*/
uri?: URI | string | vscode.Uri;
/**
* The filter object that describes which notes to consider
* for opening
*/
filter?: FilterDescriptor;
}
export const OPEN_COMMAND = {
command: 'foam-vscode.open-resource',
title: 'Foam: Open Resource',
asURI: (uri: URI) =>
vscode.Uri.parse(`command:${OPEN_COMMAND.command}`).with({
query: encodeURIComponent(JSON.stringify({ uri })),
}),
forURI: (uri: URI): CommandDescriptor<OpenResourceArgs> => {
return {
name: OPEN_COMMAND.command,
params: {
uri: uri,
},
};
},
};
async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
args = args ?? {};
let item: { uri: URI } | null = null;
if (args.uri) {
const path = typeof args.uri === 'string' ? args.uri : args.uri.path;
item = workspace.find(path);
}
if (isNone(item) && args.filter) {
const resources = workspace.list();
const candidates = resources.filter(
createFilter(args.filter, vscode.workspace.isTrusted)
);
if (candidates.length === 0) {
vscode.window.showInformationMessage(
'Foam: No note matches given filters.'
);
return;
}
item =
candidates.length === 1
? candidates[0]
: await vscode.window.showQuickPick(
candidates.map(createQuickPickItemForResource)
);
}
if (isSome(item)) {
const targetUri =
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
? vscode.window.activeTextEditor?.document.uri
: toVsCodeUri(item.uri.asPlain());
return vscode.commands.executeCommand('vscode.open', targetUri);
}
Logger.info(
`${OPEN_COMMAND.command}: No resource matches given args`,
JSON.stringify(args)
);
vscode.window.showInformationMessage(
`${OPEN_COMMAND.command}: No resource matches given args`
);
}
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
context.subscriptions.push(
vscode.commands.registerCommand(
OPEN_COMMAND.command,
async (params: { uri: URI }) => {
const uri = new URI(params.uri);
switch (uri.scheme) {
case 'file': {
const targetUri =
uri.path === vscode.window.activeTextEditor?.document.uri.path
? vscode.window.activeTextEditor?.document.uri
: toVsCodeUri(uri.asPlain());
// if the doc is already open, reuse the same colunm
const targetEditor = vscode.window.visibleTextEditors.find(
ed => targetUri.path === ed.document.uri.path
);
const column = targetEditor?.viewColumn;
return vscode.commands.executeCommand('vscode.open', targetUri);
}
case 'placeholder': {
const title = uri.getName();
if (uri.isAbsolute()) {
return NoteFactory.createForPlaceholderWikilink(
title,
URI.file(uri.path)
);
}
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 target = fromVsCodeUri(basedir)
.resolve(uri, true)
.changeExtension('', '.md');
await NoteFactory.createForPlaceholderWikilink(title, target);
return;
}
}
}
)
vscode.commands.registerCommand(OPEN_COMMAND.command, args => {
return openResource(foam.workspace, args);
})
);
},
};
interface ResourceItem extends vscode.QuickPickItem {
label: string;
description: string;
uri: URI;
detail?: string;
}
const createQuickPickItemForResource = (resource: Resource): ResourceItem => {
const icon = 'file';
const sections = resource.sections
.map(s => s.label)
.filter(l => l !== resource.title);
const detail = sections.length > 0 ? 'Sections: ' + sections.join(', ') : '';
return {
label: `$(${icon}) ${resource.title}`,
description: vscode.workspace.asRelativePath(resource.uri.toFsPath()),
uri: resource.uri,
detail: detail,
};
};
export default feature;

View File

@@ -70,7 +70,7 @@ async function createReferenceList(foam: FoamWorkspace) {
const refs = await generateReferenceList(foam, editor.document);
if (refs && refs.length) {
await editor.edit(function(editBuilder) {
await editor.edit(function (editBuilder) {
if (editor) {
const spacing = hasEmptyTrailing(editor.document)
? docConfig.eol
@@ -193,7 +193,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
public provideCodeLenses(
document: TextDocument,
_: CancellationToken
): CodeLens[] | Thenable<CodeLens[]> {
): CodeLens[] | Promise<CodeLens[]> {
loadDocConfig();
const range = detectReferenceListRange(document);

View File

@@ -14,33 +14,32 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
cursor: 'pointer',
});
const updateDecorations = (
parser: ResourceParser,
workspace: FoamWorkspace
) => (editor: vscode.TextEditor) => {
if (!editor || editor.document.languageId !== 'markdown') {
return;
}
const note = parser.parse(
fromVsCodeUri(editor.document.uri),
editor.document.getText()
);
const placeholderRanges = [];
note.links.forEach(link => {
const linkUri = workspace.resolveLink(note, link);
if (linkUri.isPlaceholder()) {
placeholderRanges.push(
Range.create(
link.range.start.line,
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
link.range.end.line,
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
)
);
const updateDecorations =
(parser: ResourceParser, workspace: FoamWorkspace) =>
(editor: vscode.TextEditor) => {
if (!editor || editor.document.languageId !== 'markdown') {
return;
}
});
editor.setDecorations(placeholderDecoration, placeholderRanges);
};
const note = parser.parse(
fromVsCodeUri(editor.document.uri),
editor.document.getText()
);
const placeholderRanges = [];
note.links.forEach(link => {
const linkUri = workspace.resolveLink(note, link);
if (linkUri.isPlaceholder()) {
placeholderRanges.push(
Range.create(
link.range.start.line,
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
link.range.end.line,
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
)
);
}
});
editor.setDecorations(placeholderDecoration, placeholderRanges);
};
const feature: FoamFeature = {
activate: async (

View File

@@ -92,9 +92,7 @@ describe('Hover provider', () => {
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const provider = new HoverProvider(hoverEnabled, ws, graph, parser);
@@ -133,9 +131,7 @@ describe('Hover provider', () => {
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -164,9 +160,7 @@ describe('Hover provider', () => {
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -190,9 +184,7 @@ describe('Hover provider', () => {
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);
@@ -220,9 +212,7 @@ The content of file B`);
);
const noteA = parser.parse(fileA.uri, fileA.content);
const noteB = parser.parse(fileB.uri, fileB.content);
const ws = createWorkspace()
.set(noteA)
.set(noteB);
const ws = createWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(noteA.uri);

View File

@@ -14,6 +14,7 @@ import { Range } from '../core/model/range';
import { FoamGraph } from '../core/model/graph';
import { OPEN_COMMAND } from './commands/open-resource';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
import { commandAsURI } from '../utils/commands';
export const CONFIG_KEY = 'links.hover.enable';
@@ -22,9 +23,8 @@ const feature: FoamFeature = {
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const isHoverEnabled: ConfigurationMonitor<boolean> = monitorFoamVsCodeConfig(
CONFIG_KEY
);
const isHoverEnabled: ConfigurationMonitor<boolean> =
monitorFoamVsCodeConfig(CONFIG_KEY);
const foam = await foamPromise;
@@ -87,7 +87,7 @@ export class HoverProvider implements vscode.HoverProvider {
);
const links = sources.slice(0, 10).map(ref => {
const command = OPEN_COMMAND.asURI(ref);
const command = commandAsURI(OPEN_COMMAND.forURI(ref));
return `- [${this.workspace.get(ref).title}](${command.toString()})`;
});
@@ -109,27 +109,14 @@ export class HoverProvider implements vscode.HoverProvider {
: this.workspace.get(targetUri).title;
}
// If placeholder, offer to create a new note from template (compared to default link provider - not from template)
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 target = fromVsCodeUri(basedir)
.resolve(targetUri, true)
.changeExtension('', '.md');
const args = {
text: target.getName(),
notePath: target.path,
const command = CREATE_NOTE_COMMAND.forPlaceholder(targetUri.path, {
askForTemplate: true,
};
const command = CREATE_NOTE_COMMAND.asURI(args);
onFileExists: 'open',
});
const newNoteFromTemplate = new vscode.MarkdownString(
`[Create note from template for '${targetUri.getName()}'](${command})`
`[Create note from template for '${targetUri.getName()}'](${commandAsURI(
command
).toString()})`
);
newNoteFromTemplate.isTrusted = true;

View File

@@ -64,15 +64,11 @@ export const completionCursorMove: FoamFeature = {
.lineAt(changedPosition.line)
.text.charAt(changedPosition.character - 1);
const {
character: selectionChar,
line: selectionLine,
} = e.selections[0].active;
const { character: selectionChar, line: selectionLine } =
e.selections[0].active;
const {
line: completionLine,
character: completionChar,
} = currentPosition;
const { line: completionLine, character: completionChar } =
currentPosition;
const inCompleteBySectionDivider =
linkCommitCharacters.includes(preChar) &&
@@ -102,7 +98,8 @@ export const completionCursorMove: FoamFeature = {
};
export class SectionCompletionProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
implements vscode.CompletionItemProvider<vscode.CompletionItem>
{
constructor(private ws: FoamWorkspace) {}
provideCompletionItems(
@@ -162,7 +159,8 @@ export class SectionCompletionProvider
}
export class WikilinkCompletionProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
implements vscode.CompletionItemProvider<vscode.CompletionItem>
{
constructor(private ws: FoamWorkspace, private graph: FoamGraph) {}
provideCompletionItems(
@@ -293,9 +291,8 @@ class ResourceCompletionItem extends vscode.CompletionItem {
}
function getCompletionLabelSetting() {
const labelStyle: 'path' | 'title' | 'identifier' = getFoamVsCodeConfig(
'completion.label'
);
const labelStyle: 'path' | 'title' | 'identifier' =
getFoamVsCodeConfig('completion.label');
return labelStyle;
}

View File

@@ -12,6 +12,8 @@ import { OPEN_COMMAND } from './commands/open-resource';
import { toVsCodeUri } from '../utils/vsc-utils';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamGraph } from '../core/model/graph';
import { commandAsURI } from '../utils/commands';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
describe('Document navigation', () => {
const parser = createMarkdownParser([]);
@@ -82,7 +84,11 @@ describe('Document navigation', () => {
expect(links.length).toEqual(1);
expect(links[0].target).toEqual(
OPEN_COMMAND.asURI(URI.placeholder('a placeholder'))
commandAsURI(
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', {
onFileExists: 'open',
})
)
);
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));
});

View File

@@ -2,7 +2,6 @@ import * as vscode from 'vscode';
import { FoamFeature } from '../types';
import { mdDocSelector } from '../utils';
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
import { OPEN_COMMAND } from './commands/open-resource';
import { Foam } from '../core/model/foam';
import { FoamWorkspace } from '../core/model/workspace';
import { Resource, ResourceLink, ResourceParser } from '../core/model/note';
@@ -10,6 +9,8 @@ import { URI } from '../core/model/uri';
import { Range } from '../core/model/range';
import { FoamGraph } from '../core/model/graph';
import { Position } from '../core/model/position';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
import { commandAsURI } from '../utils/commands';
const feature: FoamFeature = {
activate: async (
@@ -57,7 +58,8 @@ export class NavigationProvider
implements
vscode.DefinitionProvider,
vscode.DocumentLinkProvider,
vscode.ReferenceProvider {
vscode.ReferenceProvider
{
constructor(
private workspace: FoamWorkspace,
private graph: FoamGraph,
@@ -162,7 +164,9 @@ export class NavigationProvider
return targets
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
.map(o => {
const command = OPEN_COMMAND.asURI(o.target);
const command = CREATE_NOTE_COMMAND.forPlaceholder(o.target.path, {
onFileExists: 'open',
});
const documentLink = new vscode.DocumentLink(
new vscode.Range(
@@ -171,7 +175,7 @@ export class NavigationProvider
o.link.range.end.line,
o.link.range.end.character - 2
),
command
commandAsURI(command)
);
documentLink.tooltip = `Create note for '${o.target.path}'`;
return documentLink;

View File

@@ -44,9 +44,7 @@ describe('Backlinks panel', () => {
uri: './note-c.md',
links: [{ slug: 'note-a' }],
});
ws.set(noteA)
.set(noteB)
.set(noteC);
ws.set(noteA).set(noteB).set(noteC);
const graph = FoamGraph.fromWorkspace(ws, true);
const provider = new BacklinksTreeDataProvider(ws, graph);

View File

@@ -37,7 +37,8 @@ const feature: FoamFeature = {
export default feature;
export class BacklinksTreeDataProvider
implements vscode.TreeDataProvider<BacklinkPanelTreeItem> {
implements vscode.TreeDataProvider<BacklinkPanelTreeItem>
{
public target?: URI = undefined;
// prettier-ignore
private _onDidChangeTreeDataEmitter = new vscode.EventEmitter<BacklinkPanelTreeItem | undefined | void>();
@@ -53,7 +54,7 @@ export class BacklinksTreeDataProvider
return item;
}
getChildren(item?: ResourceTreeItem): Thenable<BacklinkPanelTreeItem[]> {
getChildren(item?: ResourceTreeItem): Promise<BacklinkPanelTreeItem[]> {
const uri = this.target;
if (item) {
const resource = item.resource;
@@ -61,10 +62,7 @@ export class BacklinksTreeDataProvider
const backlinkRefs = Promise.all(
resource.links
.filter(link =>
this.workspace
.resolveLink(resource, link)
.asPlain()
.isEqual(uri)
this.workspace.resolveLink(resource, link).asPlain().isEqual(uri)
)
.map(async link => {
const item = new BacklinkTreeItem(resource, link);
@@ -105,9 +103,9 @@ export class BacklinksTreeDataProvider
.map(uri => this.workspace.get(uri))
.sort(Resource.sortByTitle)
.map(note => {
const connections = backlinksByResourcePath[
note.uri.path
].sort((a, b) => Range.isBefore(a.link.range, b.link.range));
const connections = backlinksByResourcePath[note.uri.path].sort(
(a, b) => Range.isBefore(a.link.range, b.link.range)
);
const item = new ResourceTreeItem(
note,
this.workspace,

View File

@@ -67,7 +67,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
return element;
}
getChildren(element?: TagItem): Thenable<TagTreeItem[]> {
getChildren(element?: TagItem): Promise<TagTreeItem[]> {
if (element) {
const nestedTagItems: TagTreeItem[] = this.tags
.filter(item => item.tag.indexOf(element.title + TAG_SEPARATOR) > -1)

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { Foam } from '../../core/model/foam';

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import { FoamWorkspace } from '../../core/model/workspace';
export const markdownItRemoveLinkReferences = (

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import markdownItRegex from 'markdown-it-regex';
import { isNone } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import markdownItRegex from 'markdown-it-regex';
import { isSome } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';

View File

@@ -1,3 +1,5 @@
/*global markdownit:readonly*/
import markdownItRegex from 'markdown-it-regex';
import * as vscode from 'vscode';
import { isNone } from '../../utils';

View File

@@ -24,7 +24,8 @@ const feature: FoamFeature = {
};
export class TagCompletionProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
implements vscode.CompletionItemProvider<vscode.CompletionItem>
{
constructor(private foamTags: FoamTags) {}
provideCompletionItems(

View File

@@ -135,9 +135,7 @@ export function asAbsoluteWorkspaceUri(uri: URI): URI {
return res;
}
export async function createMatcherAndDataStore(
excludes: string[]
): Promise<{
export async function createMatcherAndDataStore(excludes: string[]): Promise<{
matcher: IMatcher;
dataStore: IDataStore;
excludePatterns: Map<string, string[]>;

View File

@@ -35,7 +35,7 @@ describe('Create note from template', () => {
new Resolver(new Map(), new Date()),
fileA.uri
);
expect(spy).toBeCalledWith(
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
prompt: `Enter the path for the new note`,
})

View File

@@ -95,10 +95,8 @@ export async function getTemplateInfo(
templateText
);
const [
templateMetadata,
templateWithFoamFrontmatterRemoved,
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
const [templateMetadata, templateWithFoamFrontmatterRemoved] =
extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
return {
metadata: templateMetadata,
@@ -209,62 +207,61 @@ function sortTemplatesMetadata(
return nameSortOrder || pathSortOrder;
}
const createFnForOnRelativePathStrategy = (
onRelativePath: OnRelativePathStrategy | undefined
) => async (existingFile: URI) => {
// Get the default from the configuration
if (isNone(onRelativePath)) {
onRelativePath =
getFoamVsCodeConfig('files.newNotePath') === 'root'
? 'resolve-from-root'
: 'resolve-from-current-dir';
}
if (typeof onRelativePath === 'function') {
return onRelativePath(existingFile);
}
switch (onRelativePath) {
case 'resolve-from-current-dir':
return getCurrentEditorDirectory().joinPath(existingFile.path);
case 'resolve-from-root':
return asAbsoluteWorkspaceUri(existingFile);
case 'cancel':
return undefined;
case 'ask':
default: {
const newProposedPath = await askUserForFilepathConfirmation(
existingFile
);
return newProposedPath && URI.file(newProposedPath);
const createFnForOnRelativePathStrategy =
(onRelativePath: OnRelativePathStrategy | undefined) =>
async (existingFile: URI) => {
// Get the default from the configuration
if (isNone(onRelativePath)) {
onRelativePath =
getFoamVsCodeConfig('files.newNotePath') === 'root'
? 'resolve-from-root'
: 'resolve-from-current-dir';
}
}
};
const createFnForOnFileExistsStrategy = (
onFileExists: OnFileExistStrategy
) => async (existingFile: URI) => {
if (typeof onFileExists === 'function') {
return onFileExists(existingFile);
}
switch (onFileExists) {
case 'open':
await commands.executeCommand('vscode.open', toVsCodeUri(existingFile));
return;
case 'overwrite':
await deleteFile(existingFile);
return existingFile;
case 'cancel':
return undefined;
case 'ask':
default: {
const newProposedPath = await askUserForFilepathConfirmation(
existingFile
);
return newProposedPath && URI.file(newProposedPath);
if (typeof onRelativePath === 'function') {
return onRelativePath(existingFile);
}
}
};
switch (onRelativePath) {
case 'resolve-from-current-dir':
return getCurrentEditorDirectory().joinPath(existingFile.path);
case 'resolve-from-root':
return asAbsoluteWorkspaceUri(existingFile);
case 'cancel':
return undefined;
case 'ask':
default: {
const newProposedPath = await askUserForFilepathConfirmation(
existingFile
);
return newProposedPath && URI.file(newProposedPath);
}
}
};
const createFnForOnFileExistsStrategy =
(onFileExists: OnFileExistStrategy) => async (existingFile: URI) => {
if (typeof onFileExists === 'function') {
return onFileExists(existingFile);
}
switch (onFileExists) {
case 'open':
await commands.executeCommand('vscode.open', toVsCodeUri(existingFile));
return;
case 'overwrite':
await deleteFile(existingFile);
return existingFile;
case 'cancel':
return undefined;
case 'ask':
default: {
const newProposedPath = await askUserForFilepathConfirmation(
existingFile
);
return newProposedPath && URI.file(newProposedPath);
}
}
};
export const NoteFactory = {
createNote: async (
@@ -279,9 +276,8 @@ export const NoteFactory = {
const onRelativePath = createFnForOnRelativePathStrategy(
onRelativePathStrategy
);
const onFileExists = createFnForOnFileExistsStrategy(
onFileExistsStrategy
);
const onFileExists =
createFnForOnFileExistsStrategy(onFileExistsStrategy);
/**
* Make sure the path is absolute and doesn't exist

View File

@@ -59,12 +59,13 @@ export function run(): Promise<void> {
[rootDir]
);
const failures = results.testResults.reduce((acc, res) => {
if (res.failureMessage) {
acc.push(res as any);
}
return acc;
}, [] as jest.TestResult[]);
const failures = results.testResults.filter(t => t.failureMessage);
// const failures = results.testResults.reduce((acc, res) => {
// if (res.failureMessage) {
// acc.push(res as any);
// }
// return acc;
// }, []);
if (failures.length > 0) {
console.log('Some Foam tests failed: ', failures.length);

View File

@@ -1,14 +1,27 @@
import micromatch from 'micromatch';
import { promisify } from 'util';
import { glob } from 'glob';
import { Logger } from '../core/utils/log';
import { IDataStore, IMatcher } from '../core/services/datastore';
import { URI } from '../core/model/uri';
import { isWindows } from '../core/common/platform';
import { asAbsolutePaths } from '../core/utils/path';
import fs from 'fs';
import path from 'path';
const findAllFiles = promisify(glob);
function getFiles(directory: string) {
const files = [];
getFilesFromDir(files, directory);
return files;
}
function getFilesFromDir(files: string[], directory: string) {
fs.readdirSync(directory).forEach(file => {
const absolute = path.join(directory, file);
if (fs.statSync(absolute).isDirectory()) {
getFilesFromDir(files, absolute);
} else {
files.push(absolute);
}
});
}
/**
* File system based data store
*/
@@ -19,7 +32,7 @@ export class FileDataStore implements IDataStore {
) {}
async list(): Promise<URI[]> {
const res = await findAllFiles([this.basedir, '**/*'].join('/'));
const res = getFiles(this.basedir);
return res.map(URI.file);
}

View File

@@ -53,11 +53,12 @@ export const createTestNote = (params: {
text?: string;
sections?: string[];
root?: URI;
type?: string;
}): Resource => {
const root = params.root ?? URI.file('/');
return {
uri: root.resolve(params.uri),
type: 'note',
type: params.type ?? 'note',
properties: {},
title: params.title ?? strToUri(params.uri).getBasename(),
definitions: params.definitions ?? [],

View File

@@ -0,0 +1,20 @@
import { Uri } from 'vscode';
import { merge } from 'lodash';
export interface CommandDescriptor<T> {
name: string;
params: T;
}
export function describeCommand<T>(
base: CommandDescriptor<T>,
...extra: Partial<T>[]
) {
return merge(base, ...extra.map(e => ({ params: e })));
}
export function commandAsURI<T>(command: CommandDescriptor<T>) {
return Uri.parse(`command:${command.name}`).with({
query: encodeURIComponent(JSON.stringify(command.params)),
});
}

View File

@@ -37,7 +37,8 @@ import { IMatcher } from '../core/services/datastore';
* @implements {vscode.TreeDataProvider<GroupedResourceTreeItem>}
*/
export class GroupedResourcesTreeDataProvider
implements vscode.TreeDataProvider<GroupedResourceTreeItem> {
implements vscode.TreeDataProvider<GroupedResourceTreeItem>
{
// prettier-ignore
private _onDidChangeTreeData: vscode.EventEmitter<GroupedResourceTreeItem | undefined | void> = new vscode.EventEmitter<GroupedResourceTreeItem | undefined | void>();
// prettier-ignore
@@ -129,7 +130,7 @@ export class GroupedResourcesTreeDataProvider
getChildren(
directory?: DirectoryTreeItem
): Thenable<GroupedResourceTreeItem[]> {
): Promise<GroupedResourceTreeItem[]> {
if (this.groupBy === GroupedResoucesConfigGroupBy.Folder) {
if (isSome(directory)) {
return Promise.resolve(directory.children.sort(sortByTreeItemLabel));

View File

@@ -4,7 +4,7 @@
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "out",
"lib": ["ES2019", "es2020.string"],
"lib": ["ES2019", "es2020.string", "DOM"],
"sourceMap": true,
"strict": false,
"downlevelIteration": true

View File

@@ -1 +1 @@
declare module 'remark-wiki-link';
declare module 'remark-wiki-link';

View File

@@ -5,9 +5,12 @@
👀*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 -->
[![All Contributors](https://img.shields.io/badge/all_contributors-109-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode?label=VS%20Code%20Installs)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
[![Discord Chat](https://img.shields.io/discord/729975036148056075?color=748AD9&label=discord%20chat&style=flat-square)](https://foambubble.github.io/join-discord/g)
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
@@ -98,7 +101,7 @@ Foam also supports hierarchical tags.
### Orphans and Placeholder Panels
Orphans are notes 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 a better state, by using this panel.
@@ -146,7 +149,7 @@ Whether you want to build a [Second Brain](https://www.buildingasecondbrain.com/
You can also use our Foam template:
1. Log in on your GitHub account.
1. Log in on your GitHub account.
2. [Create a GitHub repository from foam-template](https://github.com/foambubble/foam-template/generate). If you want to keep your thoughts to yourself, remember to set the repository private.
3. Clone the repository and open it in VS Code.
4. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).

12926
yarn.lock

File diff suppressed because it is too large Load Diff