From ff2dd239188c3f132cb6cd6f91b8046ee013ee1c Mon Sep 17 00:00:00 2001 From: Riccardo Ferretti Date: Wed, 26 Oct 2022 19:18:13 +0200 Subject: [PATCH] Refactored grouped resource tree data provider to use new matcher --- .../src/core/services/datastore.ts | 44 +++++++++++++ packages/foam-vscode/src/extension.ts | 61 +++---------------- .../src/features/panels/orphans.spec.ts | 32 ---------- .../src/features/panels/orphans.ts | 31 ++++------ .../src/features/panels/placeholders.ts | 12 ++-- packages/foam-vscode/src/services/editor.ts | 61 +++++++++++++++++++ packages/foam-vscode/src/utils.ts | 1 - ...ouped-resources-tree-data-provider.spec.ts | 57 ++++++++--------- .../grouped-resources-tree-data-provider.ts | 36 ++--------- yarn.lock | 12 ++-- 10 files changed, 168 insertions(+), 179 deletions(-) delete mode 100644 packages/foam-vscode/src/features/panels/orphans.spec.ts diff --git a/packages/foam-vscode/src/core/services/datastore.ts b/packages/foam-vscode/src/core/services/datastore.ts index 093c759f..f807dfbe 100644 --- a/packages/foam-vscode/src/core/services/datastore.ts +++ b/packages/foam-vscode/src/core/services/datastore.ts @@ -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) { + 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 { + 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 { + return; + } } diff --git a/packages/foam-vscode/src/extension.ts b/packages/foam-vscode/src/extension.ts index c0bc42dd..313da0cc 100644 --- a/packages/foam-vscode/src/extension.ts +++ b/packages/foam-vscode/src/extension.ts @@ -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(); - 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('**/*') ); diff --git a/packages/foam-vscode/src/features/panels/orphans.spec.ts b/packages/foam-vscode/src/features/panels/orphans.spec.ts deleted file mode 100644 index 20b2790b..00000000 --- a/packages/foam-vscode/src/features/panels/orphans.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/foam-vscode/src/features/panels/orphans.ts b/packages/foam-vscode/src/features/panels/orphans.ts index a1927611..0d080527 100644 --- a/packages/foam-vscode/src/features/panels/orphans.ts +++ b/packages/foam-vscode/src/features/panels/orphans.ts @@ -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; diff --git a/packages/foam-vscode/src/features/panels/placeholders.ts b/packages/foam-vscode/src/features/panels/placeholders.ts index 7925ccde..68830f4e 100644 --- a/packages/foam-vscode/src/features/panels/placeholders.ts +++ b/packages/foam-vscode/src/features/panels/placeholders.ts @@ -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 ) => { 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( diff --git a/packages/foam-vscode/src/services/editor.ts b/packages/foam-vscode/src/services/editor.ts index 28b2225b..f675220a 100644 --- a/packages/foam-vscode/src/services/editor.ts +++ b/packages/foam-vscode/src/services/editor.ts @@ -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; +}> => { + const excludePatterns = new Map(); + 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 }; +}; diff --git a/packages/foam-vscode/src/utils.ts b/packages/foam-vscode/src/utils.ts index 8d6158ee..46454c03 100644 --- a/packages/foam-vscode/src/utils.ts +++ b/packages/foam-vscode/src/utils.ts @@ -8,7 +8,6 @@ import { workspace, Selection, MarkdownString, - version, ViewColumn, } from 'vscode'; import matter from 'gray-matter'; diff --git a/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.spec.ts b/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.spec.ts index 1d4b316e..a83d3079 100644 --- a/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.spec.ts +++ b/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.spec.ts @@ -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([ { diff --git a/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.ts b/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.ts index dc491a1b..7187c5fb 100644 --- a/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.ts +++ b/packages/foam-vscode/src/utils/grouped-resources-tree-data-provider.ts @@ -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, - 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) { diff --git a/yarn.lock b/yarn.lock index 5595c321..99151ffd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" "*"