Refactored grouped resource tree data provider to use new matcher

This commit is contained in:
Riccardo Ferretti
2022-10-26 19:18:13 +02:00
parent 66e74966ee
commit ff2dd23918
10 changed files with 168 additions and 179 deletions

View File

@@ -109,4 +109,48 @@ export class FileListBasedMatcher implements IMatcher {
async refresh() {
this.files = (await this.listFiles()).map(f => f.path);
}
static async createFromListFn(listFiles: () => Promise<URI[]>) {
const files = await listFiles();
return new FileListBasedMatcher(files, listFiles);
}
}
/**
* A matcher that includes all URIs passed to it
*/
export class AlwaysIncludeMatcher implements IMatcher {
include: string[] = ['**/*'];
exclude: string[] = [];
match(files: URI[]): URI[] {
return files;
}
isMatch(uri: URI): boolean {
return true;
}
refresh(): Promise<void> {
return;
}
}
export class SubstringExcludeMatcher implements IMatcher {
include: string[] = ['**/*'];
exclude: string[] = [];
constructor(exclude: string) {
this.exclude = [exclude];
}
match(files: URI[]): URI[] {
return files.filter(f => this.isMatch(f));
}
isMatch(uri: URI): boolean {
return !uri.path.includes(this.exclude[0]);
}
refresh(): Promise<void> {
return;
}
}

View File

@@ -1,28 +1,16 @@
import {
workspace,
ExtensionContext,
window,
commands,
RelativePattern,
Uri,
} from 'vscode';
import { workspace, ExtensionContext, window, commands } from 'vscode';
import { MarkdownResourceProvider } from './core/services/markdown-provider';
import { bootstrap } from './core/model/foam';
import { URI } from './core/model/uri';
import {
FileListBasedMatcher,
GenericDataStore,
} from './core/services/datastore';
import { Logger } from './core/utils/log';
import { features } from './features';
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
import { getIgnoredFilesSetting } from './settings';
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
import { AttachmentResourceProvider } from './core/services/attachment-provider';
import { VsCodeWatcher } from './services/watcher';
import { createMarkdownParser } from './core/services/markdown-parser';
import VsCodeBasedParserCache from './services/cache';
import { createMatcherAndDataStore } from './services/editor';
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -38,23 +26,12 @@ export async function activate(context: ExtensionContext) {
}
// Prepare Foam
const excludePatterns = new Map<string, string[]>();
workspace.workspaceFolders.forEach(f => excludePatterns.set(f.name, []));
const excludes = getIgnoredFilesSetting().map(g => g.toString());
for (const exclude of excludes) {
const tokens = exclude.split('/');
const matchesFolder = workspace.workspaceFolders.find(
f => f.name === tokens[0]
);
if (matchesFolder) {
excludePatterns.get(tokens[0]).push(tokens.slice(1).join('/'));
} else {
for (const [, value] of excludePatterns.entries()) {
value.push(exclude);
}
}
}
const {
matcher,
dataStore,
excludePatterns,
} = await createMatcherAndDataStore(excludes);
Logger.info('Loading from directories:');
for (const folder of workspace.workspaceFolders) {
@@ -63,30 +40,6 @@ export async function activate(context: ExtensionContext) {
Logger.info(' Exclude: ' + excludePatterns.get(folder.name).join(','));
}
const listFiles = async () => {
let files: Uri[] = [];
for (const folder of workspace.workspaceFolders) {
const uris = await workspace.findFiles(
new RelativePattern(folder.uri.path, '**/*'),
new RelativePattern(
folder.uri.path,
`{${excludePatterns.get(folder.name).join(',')}}`
)
);
files = [...files, ...uris];
}
return files.map(fromVsCodeUri);
};
const readFile = async (uri: URI) =>
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
const dataStore = new GenericDataStore(listFiles, readFile);
const files = await dataStore.list();
const matcher = new FileListBasedMatcher(files, listFiles);
const watcher = new VsCodeWatcher(
workspace.createFileSystemWatcher('**/*')
);

View File

@@ -1,32 +0,0 @@
import { FoamGraph } from '../../core/model/graph';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
import { isOrphan } from './orphans';
const orphanA = createTestNote({
uri: '/path/orphan-a.md',
title: 'Orphan A',
});
const nonOrphan1 = createTestNote({
uri: '/path/non-orphan-1.md',
});
const nonOrphan2 = createTestNote({
uri: '/path/non-orphan-2.md',
links: [{ slug: 'non-orphan-1' }],
});
const workspace = createTestWorkspace()
.set(orphanA)
.set(nonOrphan1)
.set(nonOrphan2);
const graph = FoamGraph.fromWorkspace(workspace);
describe('isOrphan', () => {
it('should return true when a note with no connections is provided', () => {
expect(isOrphan(orphanA.uri, graph)).toBeTruthy();
});
it('should return false when a note with connections is provided', () => {
expect(isOrphan(nonOrphan1.uri, graph)).toBeFalsy();
});
});

View File

@@ -1,7 +1,6 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { FoamGraph } from '../../core/model/graph';
import { URI } from '../../core/model/uri';
import { createMatcherAndDataStore } from '../../services/editor';
import { getOrphansConfig } from '../../settings';
import { FoamFeature } from '../../types';
import {
@@ -9,7 +8,6 @@ import {
ResourceTreeItem,
UriTreeItem,
} from '../../utils/grouped-resources-tree-data-provider';
import { fromVsCodeUri } from '../../utils/vsc-utils';
const feature: FoamFeature = {
activate: async (
@@ -18,24 +16,24 @@ const feature: FoamFeature = {
) => {
const foam = await foamPromise;
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
fromVsCodeUri(dir.uri)
const { matcher } = await createMatcherAndDataStore(
getOrphansConfig().exclude
);
const provider = new GroupedResourcesTreeDataProvider(
'orphans',
'orphan',
getOrphansConfig(),
workspacesURIs,
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
() =>
foam.graph
.getAllNodes()
.filter(uri => foam.graph.getConnections(uri).length === 0),
uri => {
if (uri.isPlaceholder()) {
return new UriTreeItem(uri);
}
const resource = foam.workspace.find(uri);
return new ResourceTreeItem(resource, foam.workspace);
}
return uri.isPlaceholder()
? new UriTreeItem(uri)
: new ResourceTreeItem(foam.workspace.find(uri), foam.workspace);
},
matcher
);
provider.setGroupBy(getOrphansConfig().groupBy);
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
@@ -45,7 +43,4 @@ const feature: FoamFeature = {
},
};
export const isOrphan = (uri: URI, graph: FoamGraph) =>
graph.getConnections(uri).length === 0;
export default feature;

View File

@@ -1,12 +1,12 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { createMatcherAndDataStore } from '../../services/editor';
import { getPlaceholdersConfig } from '../../settings';
import { FoamFeature } from '../../types';
import {
GroupedResourcesTreeDataProvider,
UriTreeItem,
} from '../../utils/grouped-resources-tree-data-provider';
import { fromVsCodeUri } from '../../utils/vsc-utils';
const feature: FoamFeature = {
activate: async (
@@ -14,19 +14,19 @@ const feature: FoamFeature = {
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
fromVsCodeUri(dir.uri)
const { matcher } = await createMatcherAndDataStore(
getPlaceholdersConfig().exclude
);
const provider = new GroupedResourcesTreeDataProvider(
'placeholders',
'placeholder',
getPlaceholdersConfig(),
workspacesURIs,
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
uri => {
return new UriTreeItem(uri);
}
},
matcher
);
provider.setGroupBy(getPlaceholdersConfig().groupBy);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(

View File

@@ -1,10 +1,13 @@
import { isEmpty } from 'lodash';
import { asAbsoluteUri, URI } from '../core/model/uri';
import { TextEncoder } from 'util';
import {
FileType,
RelativePattern,
Selection,
SnippetString,
TextDocument,
Uri,
ViewColumn,
window,
workspace,
@@ -13,6 +16,13 @@ import {
import { focusNote } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { isSome } from '../core/utils';
import {
AlwaysIncludeMatcher,
FileListBasedMatcher,
GenericDataStore,
IDataStore,
IMatcher,
} from '../core/services/datastore';
interface SelectionInfo {
document: TextDocument;
@@ -124,3 +134,54 @@ export function asAbsoluteWorkspaceUri(uri: URI): URI {
const res = asAbsoluteUri(uri, folders);
return res;
}
export const createMatcherAndDataStore = async (
excludes: string[]
): Promise<{
matcher: IMatcher;
dataStore: IDataStore;
excludePatterns: Map<string, string[]>;
}> => {
const excludePatterns = new Map<string, string[]>();
workspace.workspaceFolders.forEach(f => excludePatterns.set(f.name, []));
for (const exclude of excludes) {
const tokens = exclude.split('/');
const matchesFolder = workspace.workspaceFolders.find(
f => f.name === tokens[0]
);
if (matchesFolder) {
excludePatterns.get(tokens[0]).push(tokens.slice(1).join('/'));
} else {
for (const [, value] of excludePatterns.entries()) {
value.push(exclude);
}
}
}
const listFiles = async () => {
let files: Uri[] = [];
for (const folder of workspace.workspaceFolders) {
const uris = await workspace.findFiles(
new RelativePattern(folder.uri.path, '**/*'),
new RelativePattern(
folder.uri.path,
`{${excludePatterns.get(folder.name).join(',')}}`
)
);
files = [...files, ...uris];
}
return files.map(fromVsCodeUri);
};
const readFile = async (uri: URI) =>
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
const dataStore = new GenericDataStore(listFiles, readFile);
const matcher = isEmpty(excludes)
? new AlwaysIncludeMatcher()
: await FileListBasedMatcher.createFromListFn(listFiles);
return { matcher, dataStore, excludePatterns };
};

View File

@@ -8,7 +8,6 @@ import {
workspace,
Selection,
MarkdownString,
version,
ViewColumn,
} from 'vscode';
import matter from 'gray-matter';

View File

@@ -1,16 +1,19 @@
import { FoamWorkspace } from '../core/model/workspace';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import {
GroupedResoucesConfigGroupBy,
GroupedResourcesConfig,
} from '../settings';
import { createTestNote, strToUri } from '../test/test-utils';
AlwaysIncludeMatcher,
SubstringExcludeMatcher,
} from '../core/services/datastore';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import { GroupedResoucesConfigGroupBy } from '../settings';
import { createTestNote } from '../test/test-utils';
import {
DirectoryTreeItem,
GroupedResourcesTreeDataProvider,
UriTreeItem,
} from './grouped-resources-tree-data-provider';
const testMatcher = new SubstringExcludeMatcher('path-exclude');
describe('GroupedResourcesTreeDataProvider', () => {
const matchingNote1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
const matchingNote2 = createTestNote({
@@ -32,25 +35,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
.set(excludedPathNote)
.set(notMatchingNote);
// Mock config
const config: GroupedResourcesConfig = {
exclude: ['path-exclude/**/*'],
groupBy: GroupedResoucesConfigGroupBy.Folder,
};
it('should return the grouped resources as a folder tree', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
@@ -72,15 +69,16 @@ describe('GroupedResourcesTreeDataProvider', () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const directory = new DirectoryTreeItem(
'/path',
[new UriTreeItem(matchingNote1.uri)],
@@ -98,22 +96,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
});
it('should return the flattened resources', async () => {
const mockConfig = {
...config,
groupBy: GroupedResoucesConfigGroupBy.Off,
};
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
mockConfig,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Off);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
@@ -132,19 +127,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
});
it('should return the grouped resources without exclusion', async () => {
const mockConfig = { ...config, exclude: [] };
const provider = new GroupedResourcesTreeDataProvider(
'length3',
'note',
mockConfig,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
new AlwaysIncludeMatcher()
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
expect.anything(),
@@ -163,15 +158,15 @@ describe('GroupedResourcesTreeDataProvider', () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
description,
config,
[strToUri('')],
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri)
uri => new UriTreeItem(uri),
testMatcher
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
const result = await provider.getChildren();
expect(result).toMatchObject([
{

View File

@@ -1,16 +1,13 @@
import * as path from 'path';
import * as vscode from 'vscode';
import micromatch from 'micromatch';
import {
GroupedResourcesConfig,
GroupedResoucesConfigGroupBy,
} from '../settings';
import { GroupedResoucesConfigGroupBy } from '../settings';
import { getContainsTooltip, getNoteTooltip, isSome } from '../utils';
import { OPEN_COMMAND } from '../features/commands/open-resource';
import { toVsCodeUri } from './vsc-utils';
import { URI } from '../core/model/uri';
import { Resource } from '../core/model/note';
import { FoamWorkspace } from '../core/model/workspace';
import { IMatcher } from '../core/services/datastore';
/**
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
@@ -82,13 +79,10 @@ export class GroupedResourcesTreeDataProvider
constructor(
private providerId: string,
private resourceName: string,
config: GroupedResourcesConfig,
workspaceUris: URI[],
private computeResources: () => Array<URI>,
private createTreeItem: (item: URI) => GroupedResourceTreeItem
private createTreeItem: (item: URI) => GroupedResourceTreeItem,
private matcher: IMatcher
) {
this.groupBy = config.groupBy;
this.exclude = this.getGlobs(workspaceUris, config.exclude);
this.setContext();
this.doComputeResources();
}
@@ -163,30 +157,10 @@ export class GroupedResourcesTreeDataProvider
private doComputeResources(): void {
this.flatUris = this.computeResources()
.filter(uri => !this.isMatch(uri))
.filter(uri => this.matcher.isMatch(uri))
.filter(isSome);
}
private isMatch(uri: URI) {
return micromatch.isMatch(uri.toFsPath(), this.exclude);
}
private getGlobs(fsURI: URI[], globs: string[]): string[] {
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
const exclude: string[] = [];
for (const fsPath of fsURI) {
let folder = fsPath.path.replace(/\\/g, '/');
if (folder.substr(-1) === '/') {
folder = folder.slice(0, -1);
}
exclude.push(...globs.map(g => `${folder}/${g}`));
}
return exclude;
}
private getUrisByDirectory(): UrisByDirectory {
const resourcesByDirectory: UrisByDirectory = {};
for (const uri of this.flatUris) {

View File

@@ -2222,9 +2222,9 @@
"@babel/types" "^7.3.0"
"@types/braces@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb"
integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b"
integrity sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==
"@types/dateformat@^3.0.1":
version "3.0.1"
@@ -2340,9 +2340,9 @@
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/micromatch@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
integrity sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.2.tgz#ce29c8b166a73bf980a5727b1e4a4d099965151d"
integrity sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==
dependencies:
"@types/braces" "*"