Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/blank note explorer view #493

Merged
Merged
62 changes: 62 additions & 0 deletions packages/foam-vscode/package.json
Expand Up @@ -42,6 +42,12 @@
"name": "Orphans",
"icon": "media/dep.svg",
"contextualTitle": "Orphans"
},
{
"id": "foam-vscode.blank-notes",
"name": "Blank Notes",
"icon": "media/dep.svg",
"contextualTitle": "Blank Notes"
}
]
},
Expand All @@ -53,6 +59,10 @@
{
"view": "foam-vscode.orphans",
"contents": "No orphans found. Notes that have no backlinks nor links will show up here."
},
{
"view": "foam-vscode.blank-notes",
"contents": "No blank notes found. Notes that have no content will show up here."
joeltjames marked this conversation as resolved.
Show resolved Hide resolved
}
],
"menus": {
Expand All @@ -66,6 +76,16 @@
"command": "foam-vscode.group-orphans-off",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
"group": "navigation"
},
{
"command": "foam-vscode.group-blank-notes-by-folder",
"when": "view == foam-vscode.blank-notes && foam-vscode.blank-notes-grouped-by-folder == false",
"group": "navigation"
},
{
"command": "foam-vscode.group-blank-notes-off",
"when": "view == foam-vscode.blank-notes && foam-vscode.blank-notes-grouped-by-folder == true",
"group": "navigation"
}
],
"commandPalette": [
Expand All @@ -76,6 +96,14 @@
{
"command": "foam-vscode.group-orphans-off",
"when": "false"
},
{
"command": "foam-vscode.group-blank-notes-by-folder",
"when": "false"
},
{
"command": "foam-vscode.group-blank-notes-off",
"when": "false"
}
]
},
Expand Down Expand Up @@ -121,6 +149,16 @@
"command": "foam-vscode.group-orphans-off",
"title": "Foam: Don't Group Orphans",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.group-blank-notes-by-folder",
"title": "Foam: Group Blank Notes By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-blank-notes-off",
"title": "Foam: Don't Group Blank Notes",
"icon": "$(list-flat)"
}
],
"configuration": {
Expand Down Expand Up @@ -215,6 +253,30 @@
"markdownDescription": "Group orphans report entries by.",
"scope": "resource"
},
"foam.blankNotes.exclude": {
"type": [
"array"
],
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the blankNotes report. To ignore the all the content of a given folder, use `**<folderName>/**/*`",
"scope": "resource"
},
"foam.blankNotes.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group blank note report entries by.",
"scope": "resource"
},
"foam.dateSnippets.afterCompletion": {
"type": "string",
"default": "createNote",
Expand Down
59 changes: 59 additions & 0 deletions packages/foam-vscode/src/features/blank-notes.test.ts
@@ -0,0 +1,59 @@
import { createTestNote } from '../test/test-utils';
import { isBlank } from './blank-notes';

describe('isBlank', () => {
it('should return true when an empty note is provided', () => {
expect(
isBlank(
createTestNote({
uri: '',
text: '',
})
)
).toBeTruthy();
});

it('should return true when a note containing only whitespace is provided', () => {
expect(
isBlank(
createTestNote({
uri: '',
text: ' \n\t\n\t ',
})
)
).toBeTruthy();
});

it('should return true when a note containing only a title is provided', () => {
expect(
isBlank(
createTestNote({
uri: '',
text: '# Title',
})
)
).toBeTruthy();
});

it('should return true when a note containing a title followed by whitespace is provided', () => {
expect(
isBlank(
createTestNote({
uri: '',
text: '# Title \n\t\n \t \n ',
})
)
).toBeTruthy();
});

it('should return false when there is more than one line containing more than just whitespace', () => {
expect(
isBlank(
createTestNote({
uri: '',
text: '# Title\nA line that is not the title\nAnother line',
})
)
).toBeFalsy();
});
});
51 changes: 51 additions & 0 deletions packages/foam-vscode/src/features/blank-notes.ts
@@ -0,0 +1,51 @@
import * as vscode from 'vscode';
import { Foam, Note } from 'foam-core';
import { FilteredNotesConfigGroupBy, getBlankNotesConfig } from '../settings';
import { FoamFeature } from '../types';
import { FilteredNotesProvider } from './filtered-notes';

const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesFsPaths = vscode.workspace.workspaceFolders.map(
dir => dir.uri.fsPath
);
const provider = new FilteredNotesProvider(
foam.workspace,
foam.services.dataStore,
'blank-notes',
'blank note',
isBlank,
{
...getBlankNotesConfig(),
workspacesFsPaths,
}
);

context.subscriptions.push(
vscode.window.registerTreeDataProvider(
'foam-vscode.blank-notes',
provider
),
vscode.commands.registerCommand(
'foam-vscode.group-blank-notes-by-folder',
() => provider.setGroupBy(FilteredNotesConfigGroupBy.Folder)
),
vscode.commands.registerCommand('foam-vscode.group-blank-notes-off', () =>
provider.setGroupBy(FilteredNotesConfigGroupBy.Off)
),
foam.workspace.onDidAdd(() => provider.refresh()),
foam.workspace.onDidUpdate(() => provider.refresh()),
foam.workspace.onDidDelete(() => provider.refresh())
);
},
};

export default feature;

export function isBlank(note: Note) {
return note.source.text.trim().split('\n').length <= 1;
}
2 changes: 1 addition & 1 deletion packages/foam-vscode/src/features/dataviz.ts
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { FoamFeature } from '../types';
import { Foam, Logger, FoamWorkspace } from 'foam-core';
import { Foam, Logger } from 'foam-core';
import { TextDecoder } from 'util';
import { getGraphStyle, getTitleMaxLength } from '../settings';
import { isSome } from '../utils';
Expand Down
167 changes: 167 additions & 0 deletions packages/foam-vscode/src/features/filtered-notes.test.ts
@@ -0,0 +1,167 @@
import { FoamWorkspace, Note } from 'foam-core';
import { FilteredNotesConfigGroupBy } from '../settings';
import { createTestNote } from '../test/test-utils';
import {
Directory,
FilteredNotesProvider,
FilteredNotesProviderConfig,
} from './filtered-notes';

describe('filteredNotes', () => {
const isMatch = (note: Note) => {
return note.title.length === 3;
};
const matchingNote1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
const matchingNote2 = createTestNote({
uri: '/path-bis/XYZ.md',
title: 'XYZ',
});
const excludedPathNote = createTestNote({
uri: '/path-exclude/HIJ.m',
title: 'HIJ',
});
const notMatchingNote = createTestNote({
uri: '/path-bis/ABCDEFG.md',
title: 'ABCDEFG',
});

const workspace = new FoamWorkspace()
.set(matchingNote1)
.set(matchingNote2)
.set(excludedPathNote)
.set(notMatchingNote)
.resolveLinks();

const dataStore = { read: () => '' } as any;

// Mock config
const config: FilteredNotesProviderConfig = {
exclude: ['path-exclude/**/*'],
groupBy: FilteredNotesConfigGroupBy.Folder,
workspacesFsPaths: [''],
};

it('should return the filtered notes as a folder tree', async () => {
const provider = new FilteredNotesProvider(
workspace,
dataStore,
'length3',
'note',
isMatch,
config
);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 1,
label: '/path',
description: '1 note',
notes: [{ title: matchingNote1.title }],
},
{
collapsibleState: 1,
label: '/path-bis',
description: '1 note',
notes: [{ title: matchingNote2.title }],
},
]);
});

it('should return the filtered notes in a directory', async () => {
const provider = new FilteredNotesProvider(
workspace,
dataStore,
'length3',
'note',
isMatch,
config
);
const directory = new Directory('/path', [matchingNote1 as any], 'note');
const result = await provider.getChildren(directory);
expect(result).toMatchObject([
{
collapsibleState: 0,
label: 'ABC',
description: '/path/ABC.md',
command: { command: 'vscode.open' },
},
]);
});

it('should return the flattened filtered notes', async () => {
const mockConfig = { ...config, groupBy: FilteredNotesConfigGroupBy.Off };
const provider = new FilteredNotesProvider(
workspace,
dataStore,
'length3',
'note',
isMatch,
mockConfig
);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 0,
label: matchingNote1.title,
description: matchingNote1.uri.fsPath,
command: { command: 'vscode.open' },
},
{
collapsibleState: 0,
label: matchingNote2.title,
description: matchingNote2.uri.fsPath,
command: { command: 'vscode.open' },
},
]);
});

it('should return the filtered notes without exclusion', async () => {
const mockConfig = { ...config, exclude: [] };
const provider = new FilteredNotesProvider(
workspace,
dataStore,
'length3',
'note',
isMatch,
mockConfig
);
const result = await provider.getChildren();
expect(result).toMatchObject([
expect.anything(),
expect.anything(),
{
collapsibleState: 1,
label: '/path-exclude',
description: '1 note',
notes: [{ title: excludedPathNote.title }],
},
]);
});

it('should dynamically set the description', async () => {
const description = 'test description';
const provider = new FilteredNotesProvider(
workspace,
dataStore,
'length3',
description,
isMatch,
config
);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 1,
label: '/path',
description: `1 ${description}`,
notes: expect.anything(),
},
{
collapsibleState: 1,
label: '/path-bis',
description: `1 ${description}`,
notes: expect.anything(),
},
]);
});
});