From c2241f16ded8af38247842020d1641c4a38d768c Mon Sep 17 00:00:00 2001 From: Riccardo Date: Sun, 7 May 2023 22:06:22 +0200 Subject: [PATCH] Refactoring of Backlinks, Placeholder and Orphan panels (#1226) - Placeholder and Orphan panels using the Folder hierarchy - Backlinks using the same pattern as the other tree views --- .../commands/create-new-template.spec.ts | 28 --- .../src/features/panels/backlinks.spec.ts | 21 +- .../src/features/panels/backlinks.ts | 67 +++--- .../src/features/panels/notes-explorer.ts | 4 + .../src/features/panels/orphans.ts | 11 +- .../src/features/panels/placeholders.ts | 26 ++- .../panels/utils/folder-tree-provider.ts | 38 ++-- ...ouped-resources-tree-data-provider.spec.ts | 163 ++++++-------- .../grouped-resources-tree-data-provider.ts | 202 +++++------------- .../features/panels/utils/tree-view-utils.ts | 32 +-- 10 files changed, 225 insertions(+), 367 deletions(-) delete mode 100644 packages/foam-vscode/src/features/commands/create-new-template.spec.ts diff --git a/packages/foam-vscode/src/features/commands/create-new-template.spec.ts b/packages/foam-vscode/src/features/commands/create-new-template.spec.ts deleted file mode 100644 index 152211aae..000000000 --- a/packages/foam-vscode/src/features/commands/create-new-template.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { commands, window } from 'vscode'; -import * as editor from '../../services/editor'; - -describe('create-note-from-default-template command', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('can be cancelled while resolving FOAM_TITLE', async () => { - const spy = jest - .spyOn(window, 'showInputBox') - .mockImplementation(jest.fn(() => Promise.resolve(undefined))); - - const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus'); - - await commands.executeCommand( - 'foam-vscode.create-note-from-default-template' - ); - - expect(spy).toHaveBeenCalledWith({ - prompt: `Enter a title for the new note`, - value: 'Title of my New Note', - validateInput: expect.anything(), - }); - - expect(docCreatorSpy).toHaveBeenCalledTimes(0); - }); -}); diff --git a/packages/foam-vscode/src/features/panels/backlinks.spec.ts b/packages/foam-vscode/src/features/panels/backlinks.spec.ts index 8b4852f67..9097c4232 100644 --- a/packages/foam-vscode/src/features/panels/backlinks.spec.ts +++ b/packages/foam-vscode/src/features/panels/backlinks.spec.ts @@ -9,7 +9,6 @@ import { import { BacklinksTreeDataProvider } from './backlinks'; import { toVsCodeUri } from '../../utils/vsc-utils'; import { FoamGraph } from '../../core/model/graph'; -import { URI } from '../../core/model/uri'; import { ResourceRangeTreeItem, ResourceTreeItem, @@ -56,12 +55,6 @@ describe('Backlinks panel', () => { provider.target = undefined; }); - // Skipping these as still figuring out how to interact with the provider - // running in the test instance of VS Code - it.skip('does not target excluded files', async () => { - provider.target = URI.file('/excluded-file.txt'); - expect(await provider.getChildren()).toEqual([]); - }); it.skip('targets active editor', async () => { const docA = await workspace.openTextDocument(toVsCodeUri(noteA.uri)); const docB = await workspace.openTextDocument(toVsCodeUri(noteB.uri)); @@ -75,6 +68,7 @@ describe('Backlinks panel', () => { it('shows linking resources alphaetically by name', async () => { provider.target = noteA.uri; + await provider.refresh(); const notes = (await provider.getChildren()) as ResourceTreeItem[]; expect(notes.map(n => n.resource.uri.path)).toEqual([ noteB.uri.path, @@ -83,6 +77,7 @@ describe('Backlinks panel', () => { }); it('shows references in range order', async () => { provider.target = noteA.uri; + await provider.refresh(); const notes = (await provider.getChildren()) as ResourceTreeItem[]; const linksFromB = (await provider.getChildren( notes[0] @@ -95,14 +90,19 @@ describe('Backlinks panel', () => { }); it('navigates to the document if clicking on note', async () => { provider.target = noteA.uri; + await provider.refresh(); const notes = (await provider.getChildren()) as ResourceTreeItem[]; expect(notes[0].command).toMatchObject({ command: 'vscode.open', - arguments: [expect.objectContaining({ path: noteB.uri.path })], + arguments: [ + expect.objectContaining({ path: noteB.uri.path }), + expect.objectContaining({ selection: expect.anything() }), + ], }); }); it('navigates to document with link selection if clicking on backlink', async () => { provider.target = noteA.uri; + await provider.refresh(); const notes = (await provider.getChildren()) as ResourceTreeItem[]; const linksFromB = (await provider.getChildren( notes[0] @@ -120,7 +120,7 @@ describe('Backlinks panel', () => { it('refreshes upon changes in the workspace', async () => { let notes: ResourceTreeItem[] = []; provider.target = noteA.uri; - + await provider.refresh(); notes = (await provider.getChildren()) as ResourceTreeItem[]; expect(notes.length).toEqual(2); @@ -129,6 +129,7 @@ describe('Backlinks panel', () => { uri: './note-d.md', }); ws.set(noteD); + await provider.refresh(); notes = (await provider.getChildren()) as ResourceTreeItem[]; expect(notes.length).toEqual(2); @@ -138,8 +139,8 @@ describe('Backlinks panel', () => { links: [{ slug: 'note-a' }], }); ws.set(noteDBis); + await provider.refresh(); notes = (await provider.getChildren()) as ResourceTreeItem[]; - expect(notes.length).toEqual(3); expect(notes.map(n => n.resource.uri.path)).toEqual( [noteB.uri, noteC.uri, noteD.uri].map(uri => uri.path) ); diff --git a/packages/foam-vscode/src/features/panels/backlinks.ts b/packages/foam-vscode/src/features/panels/backlinks.ts index fcf82434c..ffa4553ef 100644 --- a/packages/foam-vscode/src/features/panels/backlinks.ts +++ b/packages/foam-vscode/src/features/panels/backlinks.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import { URI } from '../../core/model/uri'; - import { isNone } from '../../utils'; import { Foam } from '../../core/model/foam'; import { FoamWorkspace } from '../../core/model/workspace'; @@ -12,6 +11,7 @@ import { createBacklinkItemsForResource, groupRangesByResource, } from './utils/tree-view-utils'; +import { BaseTreeProvider } from './utils/base-tree-provider'; export default async function activate( context: vscode.ExtensionContext, @@ -20,64 +20,63 @@ export default async function activate( const foam = await foamPromise; const provider = new BacklinksTreeDataProvider(foam.workspace, foam.graph); + const treeView = vscode.window.createTreeView('foam-vscode.backlinks', { + treeDataProvider: provider, + showCollapseAll: true, + }); + const baseTitle = treeView.title; - vscode.window.onDidChangeActiveTextEditor(async () => { + const updateTreeView = async () => { provider.target = vscode.window.activeTextEditor ? fromVsCodeUri(vscode.window.activeTextEditor?.document.uri) : undefined; await provider.refresh(); - }); + treeView.title = baseTitle + ` (${provider.nValues})`; + }; + + updateTreeView(); context.subscriptions.push( - vscode.window.registerTreeDataProvider('foam-vscode.backlinks', provider), - foam.graph.onDidUpdate(() => provider.refresh()) + provider, + treeView, + foam.graph.onDidUpdate(() => updateTreeView()), + vscode.window.onDidChangeActiveTextEditor(() => updateTreeView()) ); } -export class BacklinksTreeDataProvider - implements vscode.TreeDataProvider -{ +export class BacklinksTreeDataProvider extends BaseTreeProvider { public target?: URI = undefined; - // prettier-ignore - private _onDidChangeTreeDataEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event; + public nValues = 0; + private backlinkItems: ResourceRangeTreeItem[]; - constructor(private workspace: FoamWorkspace, private graph: FoamGraph) {} - - refresh(): void { - this._onDidChangeTreeDataEmitter.fire(); + constructor(private workspace: FoamWorkspace, private graph: FoamGraph) { + super(); } - getTreeItem(item: BacklinkPanelTreeItem): vscode.TreeItem { - return item; + async refresh(): Promise { + const uri = this.target; + + const backlinkItems = + isNone(uri) || isNone(this.workspace.find(uri)) + ? [] + : await createBacklinkItemsForResource(this.workspace, this.graph, uri); + + this.backlinkItems = backlinkItems; + this.nValues = backlinkItems.length; + super.refresh(); } - getChildren(item?: BacklinkPanelTreeItem): Promise { - const uri = this.target; + async getChildren(item?: BacklinkPanelTreeItem): Promise { if (item && item instanceof ResourceTreeItem) { return item.getChildren(); } - if (isNone(uri) || isNone(this.workspace.find(uri))) { - return Promise.resolve([]); - } - - const backlinkItems = createBacklinkItemsForResource( - this.workspace, - this.graph, - uri - ); - return groupRangesByResource( this.workspace, - backlinkItems, + this.backlinkItems, vscode.TreeItemCollapsibleState.Expanded ); } - - resolveTreeItem(item: BacklinkPanelTreeItem): Promise { - return item.resolveTreeItem(); - } } type BacklinkPanelTreeItem = ResourceTreeItem | ResourceRangeTreeItem; diff --git a/packages/foam-vscode/src/features/panels/notes-explorer.ts b/packages/foam-vscode/src/features/panels/notes-explorer.ts index 0734473e2..2f3ca09d4 100644 --- a/packages/foam-vscode/src/features/panels/notes-explorer.ts +++ b/packages/foam-vscode/src/features/panels/notes-explorer.ts @@ -154,6 +154,10 @@ export class NotesProvider extends FolderTreeProvider< this.graph, res.uri ); + backlinks.forEach(item => { + item.description = item.label; + item.label = item.resource.title; + }); return backlinks; }; return res; diff --git a/packages/foam-vscode/src/features/panels/orphans.ts b/packages/foam-vscode/src/features/panels/orphans.ts index cb411376a..e5c23c95c 100644 --- a/packages/foam-vscode/src/features/panels/orphans.ts +++ b/packages/foam-vscode/src/features/panels/orphans.ts @@ -31,14 +31,15 @@ export default async function activate( }); provider.refresh(); const baseTitle = treeView.title; - treeView.title = baseTitle + ` (${provider.numElements})`; + treeView.title = baseTitle + ` (${provider.nValues})`; context.subscriptions.push( vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider), provider, + treeView, foam.graph.onDidUpdate(() => { provider.refresh(); - treeView.title = baseTitle + ` (${provider.numElements})`; + treeView.title = baseTitle + ` (${provider.nValues})`; }) ); } @@ -50,16 +51,16 @@ export class OrphanTreeView extends GroupedResourcesTreeDataProvider { private graph: FoamGraph, matcher: IMatcher ) { - super('orphans', 'orphan', state, matcher); + super('orphans', state, matcher); } - createTreeItem = uri => { + createValueTreeItem = uri => { return uri.isPlaceholder() ? new UriTreeItem(uri) : new ResourceTreeItem(this.workspace.find(uri), this.workspace); }; - computeResources = () => + getUris = () => this.graph .getAllNodes() .filter( diff --git a/packages/foam-vscode/src/features/panels/placeholders.ts b/packages/foam-vscode/src/features/panels/placeholders.ts index 12650012b..8be1e79c6 100644 --- a/packages/foam-vscode/src/features/panels/placeholders.ts +++ b/packages/foam-vscode/src/features/panels/placeholders.ts @@ -13,6 +13,7 @@ import { ContextMemento, fromVsCodeUri } from '../../utils/vsc-utils'; import { FoamGraph } from '../../core/model/graph'; import { URI } from '../../core/model/uri'; import { FoamWorkspace } from '../../core/model/workspace'; +import { FolderTreeItem } from './utils/folder-tree-provider'; export default async function activate( context: vscode.ExtensionContext, @@ -33,9 +34,9 @@ export default async function activate( treeDataProvider: provider, showCollapseAll: true, }); - const baseTitle = treeView.title; - treeView.title = baseTitle + ` (${provider.numElements})`; provider.refresh(); + const baseTitle = treeView.title; + treeView.title = baseTitle + ` (${provider.nValues})`; context.subscriptions.push( treeView, @@ -44,7 +45,7 @@ export default async function activate( provider.refresh(); }), provider.onDidChangeTreeData(() => { - treeView.title = baseTitle + ` (${provider.numElements})`; + treeView.title = baseTitle + ` (${provider.nValues})`; }), vscode.window.onDidChangeActiveTextEditor(() => { if (provider.show.get() === 'for-current-file') { @@ -67,7 +68,7 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider { private graph: FoamGraph, matcher: IMatcher ) { - super('placeholders', 'placeholder', state, matcher); + super('placeholders', state, matcher); this.disposables.push( vscode.commands.registerCommand( `foam-vscode.views.${this.providerId}.show:all`, @@ -86,21 +87,26 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider { ); } - createTreeItem = uri => { + createValueTreeItem(uri: URI, parent: FolderTreeItem): UriTreeItem { const item = new UriTreeItem(uri, { + parent, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }); item.getChildren = async () => { return groupRangesByResource( this.workspace, - createBacklinkItemsForResource(this.workspace, this.graph, uri) + await createBacklinkItemsForResource( + this.workspace, + this.graph, + uri, + 'link' + ) ); }; - item.iconPath = new vscode.ThemeIcon('link'); return item; - }; + } - computeResources = (): URI[] => { + getUris(): URI[] { if (this.show.get() === 'for-current-file') { const currentFile = vscode.window.activeTextEditor?.document.uri; return currentFile @@ -111,5 +117,5 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider { : []; } return this.graph.getAllNodes().filter(uri => uri.isPlaceholder()); - }; + } } diff --git a/packages/foam-vscode/src/features/panels/utils/folder-tree-provider.ts b/packages/foam-vscode/src/features/panels/utils/folder-tree-provider.ts index 7b921c8e9..6feb53a95 100644 --- a/packages/foam-vscode/src/features/panels/utils/folder-tree-provider.ts +++ b/packages/foam-vscode/src/features/panels/utils/folder-tree-provider.ts @@ -33,9 +33,12 @@ export class FolderTreeItem extends vscode.TreeItem { */ export abstract class FolderTreeProvider extends BaseTreeProvider { private root: Folder; + public nValues = 0; refresh(): void { - this.createTree(this.getValues(), this.getFilterFn()); + const values = this.getValues(); + this.nValues = values.length; + this.createTree(values, this.getFilterFn()); super.refresh(); } @@ -48,6 +51,14 @@ export abstract class FolderTreeProvider extends BaseTreeProvider { } } + createFolderTreeItem( + value: Folder, + name: string, + parent: FolderTreeItem + ) { + return new FolderTreeItem(value, name, parent); + } + async getChildren(item?: I): Promise { if (item instanceof BaseTreeItem) { return item.getChildren() as Promise; @@ -60,8 +71,8 @@ export abstract class FolderTreeProvider extends BaseTreeProvider { if (this.isValueType(value)) { return this.createValueTreeItem(value, undefined); } else { - return new FolderTreeItem( - value as Folder, + return this.createFolderTreeItem( + value, name, item as unknown as FolderTreeItem ); @@ -139,6 +150,18 @@ export abstract class FolderTreeProvider extends BaseTreeProvider { : Promise.resolve(null); } + /** + * Returns a function that can be used to filter the values. + * The difference between using this function vs not including the values + * is that in this case, the tree will be created with all the folders + * and subfolders, but the values will only be displayed if they pass + * the filter. + * By default it doesn't filter anything. + */ + getFilterFn(): (value: T) => boolean { + return () => true; + } + /** * Converts a value to a path of strings that can be used to create a tree. */ @@ -149,15 +172,6 @@ export abstract class FolderTreeProvider extends BaseTreeProvider { */ abstract getValues(): T[]; - /** - * Returns a function that can be used to filter the values. - * The difference between using this function vs not including the values - * is that in this case, the tree will be created with all the folders - * and subfolders, but the values will only be displayed if they pass - * the filter. - */ - abstract getFilterFn(): (value: T) => boolean; - /** * Returns true if the given value is of the type that should be displayed * as a leaf in the tree. That is, not as a folder. diff --git a/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.spec.ts b/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.spec.ts index a5ebdba93..be5133085 100644 --- a/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.spec.ts +++ b/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.spec.ts @@ -1,51 +1,60 @@ import { FoamWorkspace } from '../../../core/model/workspace'; import { AlwaysIncludeMatcher, + IMatcher, SubstringExcludeMatcher, } from '../../../core/services/datastore'; import { createTestNote } from '../../../test/test-utils'; -import { - DirectoryTreeItem, - GroupedResourcesTreeDataProvider, -} from './grouped-resources-tree-data-provider'; import { ResourceTreeItem, UriTreeItem } from './tree-view-utils'; import { randomString } from '../../../test/test-utils'; import { MapBasedMemento } from '../../../utils/vsc-utils'; +import { URI } from 'packages/foam-vscode/src/core/model/uri'; +import { TreeItem } from 'vscode'; +import { GroupedResourcesTreeDataProvider } 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({ +class TestProvider extends GroupedResourcesTreeDataProvider { + constructor( + matcher: IMatcher, + private list: () => URI[], + private create: (uri: URI) => TreeItem + ) { + super(randomString(), new MapBasedMemento(), matcher); + } + getUris(): URI[] { + return this.list(); + } + createValueTreeItem(value: URI) { + return this.create(value) as any; + } +} + +describe('TestProvider', () => { + const note1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' }); + const note2 = createTestNote({ uri: '/path-bis/XYZ.md', title: 'XYZ', }); - const excludedPathNote = createTestNote({ - uri: '/path-exclude/HIJ.m', - title: 'HIJ', - }); - const notMatchingNote = createTestNote({ + const note3 = createTestNote({ uri: '/path-bis/ABCDEFG.md', title: 'ABCDEFG', }); + const excludedNote = createTestNote({ + uri: '/path-exclude/HIJ.m', + title: 'HIJ', + }); const workspace = new FoamWorkspace() - .set(matchingNote1) - .set(matchingNote2) - .set(excludedPathNote) - .set(notMatchingNote); + .set(note1) + .set(note2) + .set(note3) + .set(excludedNote); it('should return the grouped resources as a folder tree', async () => { - const provider = new GroupedResourcesTreeDataProvider( - randomString(), - 'note', - new MapBasedMemento(), + const provider = new TestProvider( testMatcher, - () => - workspace - .list() - .filter(r => r.title.length === 3) - .map(r => r.uri), + () => workspace.list().map(r => r.uri), uri => new UriTreeItem(uri) ); provider.groupBy.update('folder'); @@ -55,39 +64,28 @@ describe('GroupedResourcesTreeDataProvider', () => { { collapsibleState: 1, label: '/path', - description: '1 note', - children: [new UriTreeItem(matchingNote1.uri)], + description: '(1)', }, { collapsibleState: 1, label: '/path-bis', - description: '1 note', - children: [new UriTreeItem(matchingNote2.uri)], + description: '(2)', }, ]); }); - it('should return the grouped resources in a directory', async () => { - const provider = new GroupedResourcesTreeDataProvider( - randomString(), - 'note', - new MapBasedMemento(), + const provider = new TestProvider( testMatcher, - () => - workspace - .list() - .filter(r => r.title.length === 3) - .map(r => r.uri), + () => workspace.list().map(r => r.uri), uri => new ResourceTreeItem(workspace.get(uri), workspace) ); provider.groupBy.update('folder'); provider.refresh(); - - const directory = new DirectoryTreeItem( - '/path', - [new ResourceTreeItem(matchingNote1, workspace)], - 'note' - ); + const paths = await provider.getChildren(); + const directory = paths[0]; + expect(directory).toMatchObject({ + label: '/path', + }); const result = await provider.getChildren(directory); expect(result).toMatchObject([ { @@ -98,98 +96,61 @@ describe('GroupedResourcesTreeDataProvider', () => { }, ]); }); - it('should return the flattened resources', async () => { - const provider = new GroupedResourcesTreeDataProvider( - randomString(), - 'note', - new MapBasedMemento(), + const provider = new TestProvider( testMatcher, - () => - workspace - .list() - .filter(r => r.title.length === 3) - .map(r => r.uri), + () => workspace.list().map(r => r.uri), uri => new ResourceTreeItem(workspace.get(uri), workspace) ); provider.groupBy.update('off'); provider.refresh(); - const result = await provider.getChildren(); expect(result).toMatchObject([ { collapsibleState: 0, - label: matchingNote1.title, + label: note1.title, description: '/path/ABC.md', command: { command: 'vscode.open' }, }, { collapsibleState: 0, - label: matchingNote2.title, + label: note3.title, + description: '/path-bis/ABCDEFG.md', + command: { command: 'vscode.open' }, + }, + { + collapsibleState: 0, + label: note2.title, description: '/path-bis/XYZ.md', command: { command: 'vscode.open' }, }, ]); }); - it('should return the grouped resources without exclusion', async () => { - const provider = new GroupedResourcesTreeDataProvider( - randomString(), - 'note', - new MapBasedMemento(), + const provider = new TestProvider( new AlwaysIncludeMatcher(), - () => - workspace - .list() - .filter(r => r.title.length === 3) - .map(r => r.uri), + () => workspace.list().map(r => r.uri), uri => new UriTreeItem(uri) ); provider.groupBy.update('folder'); provider.refresh(); - const result = await provider.getChildren(); + expect(result.length).toEqual(3); expect(result).toMatchObject([ - expect.anything(), - expect.anything(), { collapsibleState: 1, - label: '/path-exclude', - description: '1 note', - children: [new UriTreeItem(excludedPathNote.uri)], + label: '/path', + description: '(1)', }, - ]); - }); - - it('should dynamically set the description', async () => { - const description = 'test description'; - const provider = new GroupedResourcesTreeDataProvider( - randomString(), - description, - new MapBasedMemento(), - testMatcher, - () => - workspace - .list() - .filter(r => r.title.length === 3) - .map(r => r.uri), - uri => new UriTreeItem(uri) - ); - provider.groupBy.update('folder'); - provider.refresh(); - const result = await provider.getChildren(); - expect(result).toMatchObject([ { collapsibleState: 1, - label: '/path', - description: `1 ${description}`, - children: expect.anything(), + label: '/path-bis', + description: '(2)', }, { collapsibleState: 1, - label: '/path-bis', - description: `1 ${description}`, - children: expect.anything(), + label: '/path-exclude', + description: '(1)', }, ]); }); diff --git a/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.ts b/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.ts index 01920f70d..6cc96e152 100644 --- a/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.ts +++ b/packages/foam-vscode/src/features/panels/utils/grouped-resources-tree-data-provider.ts @@ -1,10 +1,16 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { getContainsTooltip, isSome } from '../../../utils'; import { URI } from '../../../core/model/uri'; import { IMatcher } from '../../../core/services/datastore'; import { UriTreeItem } from './tree-view-utils'; import { ContextMemento } from '../../../utils/vsc-utils'; +import { + FolderTreeItem, + FolderTreeProvider, + Folder, +} from './folder-tree-provider'; + +type GroupedResourceTreeItem = UriTreeItem | FolderTreeItem; /** * Provides the ability to expose a TreeDataExplorerView in VSCode. This class will @@ -12,85 +18,45 @@ import { ContextMemento } from '../../../utils/vsc-utils'; * display the Resources. * * **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file: - * ``` - * foam-vscode.views.${providerId}.group-by-folder - * foam-vscode.views.${providerId}.group-off - * ``` - * Where `providerId` is the same string provided to the constructor. You must also register the commands in your context subscriptions as follows: - * ``` - * const provider = new GroupedResourcesTreeDataProvider( - ... - ); - context.subscriptions.push( - vscode.window.registerTreeDataProvider( - 'foam-vscode.placeholders', - provider - ), - ...provider.commands, - ); - ``` + * ``` + * foam-vscode.views.${providerId}.group-by-folder + * foam-vscode.views.${providerId}.group-off + * ``` + * Where `providerId` is the same string provided to the constructor. * @export * @class GroupedResourcesTreeDataProvider * @implements {vscode.TreeDataProvider} */ -export class GroupedResourcesTreeDataProvider - implements - vscode.TreeDataProvider, - vscode.Disposable -{ - // prettier-ignore - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - // prettier-ignore - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - private flatUris: Array = []; - private root = vscode.workspace.workspaceFolders[0].uri.path; +export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvider< + GroupedResourceTreeItem, + URI +> { public groupBy = new ContextMemento<'off' | 'folder'>( this.state, `foam-vscode.views.${this.providerId}.group-by`, 'folder' ); - protected disposables: vscode.Disposable[] = []; /** * Creates an instance of GroupedResourcesTreeDataProvider. * **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file: * ``` - * foam-vscode.views.${this.providerId}.group-by-folder - * foam-vscode.views.${this.providerId}.group-by-off - * ``` - * Where `providerId` is the same string provided to this constructor. You must also register the commands in your context subscriptions as follows: + * foam-vscode.views.${this.providerId}.group-by:folder + * foam-vscode.views.${this.providerId}.group-by:off * ``` - * const provider = new GroupedResourcesTreeDataProvider( - ... - ); - context.subscriptions.push( - vscode.window.registerTreeDataProvider( - 'foam-vscode.placeholders', - provider - ), - ...provider.commands, - ); - ``` + * Where `providerId` is the same string provided to this constructor. + * * @param {string} providerId A **unique** providerId, this will be used to generate necessary commands within the provider. - * @param {string} resourceName A display name used in the explorer view - * @param {() => Array} computeResources - * @param {(item: URI) => GroupedResourceTreeItem} createTreeItem - * @param {GroupedResourcesConfig} config - * @param {URI[]} workspaceUris The workspace URIs + * @param {vscode.Memento} state The state to use for persisting the panel settings. + * @param {IMatcher} matcher The matcher to use for filtering the uris. * @memberof GroupedResourcesTreeDataProvider */ constructor( protected providerId: string, - private resourceName: string, protected state: vscode.Memento, - private matcher: IMatcher, - protected computeResources: () => Array = () => { - throw new Error('Not implemented'); - }, - protected createTreeItem: (item: URI) => GroupedResourceTreeItem = () => { - throw new Error('Not implemented'); - } + private matcher: IMatcher ) { + super(); this.disposables.push( vscode.commands.registerCommand( `foam-vscode.views.${this.providerId}.group-by:folder`, @@ -109,110 +75,40 @@ export class GroupedResourcesTreeDataProvider ); } - dispose() { - this.disposables.forEach(d => d.dispose()); - } - - public get numElements() { - return this.flatUris.length; - } - - refresh(): void { - this.doComputeResources(); - this._onDidChangeTreeData.fire(); - } - - getTreeItem(item: GroupedResourceTreeItem): vscode.TreeItem { - return item; - } - - async getChildren( - item?: GroupedResourceTreeItem - ): Promise { - if ((item as any)?.getChildren) { - return (item as any).getChildren(); - } + valueToPath(value: URI) { + const p = vscode.workspace.asRelativePath( + value.path, + vscode.workspace.workspaceFolders.length > 1 + ); if (this.groupBy.get() === 'folder') { - const directories = Object.entries(this.getUrisByDirectory()) - .sort(([dir1], [dir2]) => sortByString(dir1, dir2)) - .map( - ([dir, children]) => - new DirectoryTreeItem( - dir, - children.map(this.createTreeItem), - this.resourceName - ) - ); - return Promise.resolve(directories); + const { dir, base } = path.parse(p); + return [dir, base]; } - - const items = this.flatUris - .map(uri => this.createTreeItem(uri)) - .sort(sortByTreeItemLabel); - return Promise.resolve(items); + return [p]; } - resolveTreeItem( - item: GroupedResourceTreeItem - ): Promise { - return item.resolveTreeItem() as Promise; + getValues(): URI[] { + const uris = this.getUris(); + return uris.filter(uri => this.matcher.isMatch(uri)); } - private doComputeResources(): void { - this.flatUris = this.computeResources() - .filter(uri => this.matcher.isMatch(uri)) - .filter(isSome); - } - - private getUrisByDirectory(): UrisByDirectory { - const resourcesByDirectory: UrisByDirectory = {}; - for (const uri of this.flatUris) { - const p = uri.path.replace(this.root, ''); - const { dir } = path.parse(p); - - if (resourcesByDirectory[dir]) { - resourcesByDirectory[dir].push(uri); - } else { - resourcesByDirectory[dir] = [uri]; - } - } - return resourcesByDirectory; + isValueType(value: URI): value is URI { + return value instanceof URI; } -} - -type UrisByDirectory = { [key: string]: Array }; -type GroupedResourceTreeItem = UriTreeItem | DirectoryTreeItem; - -export class DirectoryTreeItem extends vscode.TreeItem { - constructor( - public readonly dir: string, - public readonly children: Array, - itemLabel: string + createFolderTreeItem( + value: Folder, + name: string, + parent: FolderTreeItem ) { - super(dir || 'Not Created', vscode.TreeItemCollapsibleState.Collapsed); - const s = this.children.length > 1 ? 's' : ''; - this.description = `${this.children.length} ${itemLabel}${s}`; - } - - iconPath = new vscode.ThemeIcon('folder'); - contextValue = 'directory'; - - resolveTreeItem(): Promise { - const titles = this.children - .map(c => c.label?.toString()) - .sort(sortByString); - this.tooltip = getContainsTooltip(titles); - return Promise.resolve(this); + const item = super.createFolderTreeItem(value, name, parent); + item.label = item.label || '(Not Created)'; + item.description = `(${Object.keys(value).length})`; + return item; } - getChildren(): Promise { - return Promise.resolve(this.children); - } + /** + * Return the URIs before the filtering by the matcher is applied + */ + abstract getUris(): URI[]; } - -const sortByTreeItemLabel = (a: vscode.TreeItem, b: vscode.TreeItem) => - a.label.toString().localeCompare(b.label.toString()); - -const sortByString = (a: string, b: string) => - a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()); diff --git a/packages/foam-vscode/src/features/panels/utils/tree-view-utils.ts b/packages/foam-vscode/src/features/panels/utils/tree-view-utils.ts index e559687f9..2a60c30cb 100644 --- a/packages/foam-vscode/src/features/panels/utils/tree-view-utils.ts +++ b/packages/foam-vscode/src/features/panels/utils/tree-view-utils.ts @@ -37,7 +37,7 @@ export class UriTreeItem extends BaseTreeItem { vscode.workspace.getWorkspaceFolder(toVsCodeUri(uri))?.uri.path, '' ); - this.iconPath = new vscode.ThemeIcon('new-file'); + this.iconPath = new vscode.ThemeIcon('link'); } } @@ -107,11 +107,16 @@ export class ResourceRangeTreeItem extends BaseTreeItem { return Promise.resolve(this); } + static icons = { + backlink: 'arrow-left', + link: 'arrow-right', + tag: 'symbol-number', + }; static async createStandardItem( workspace: FoamWorkspace, resource: Resource, range: Range, - type?: 'backlink' | 'tag' + variant: 'backlink' | 'tag' | 'link' ): Promise { const markdown = (await workspace.readAsMarkdown(resource.uri)) ?? ''; const lines = markdown.split('\n'); @@ -126,7 +131,7 @@ export class ResourceRangeTreeItem extends BaseTreeItem { const item = new ResourceRangeTreeItem(label, resource, range, workspace); item.iconPath = new vscode.ThemeIcon( - type === 'backlink' ? 'arrow-left' : 'symbol-number', + ResourceRangeTreeItem.icons[variant], new vscode.ThemeColor('charts.purple') ); @@ -157,9 +162,10 @@ export const groupRangesByResource = async ( const resourceItem = new ResourceTreeItem(items[0].resource, workspace, { collapsibleState, }); - resourceItem.getChildren = () => - Promise.resolve(items.sort((a, b) => Range.isBefore(a.range, b.range))); + const children = items.sort((a, b) => Range.isBefore(a.range, b.range)); + resourceItem.getChildren = () => Promise.resolve(children); resourceItem.description = `(${items.length}) ${resourceItem.description}`; + resourceItem.command = children[0].command; return resourceItem; }); resourceItems.sort((a, b) => Resource.sortByTitle(a.resource, b.resource)); @@ -169,22 +175,20 @@ export const groupRangesByResource = async ( export function createBacklinkItemsForResource( workspace: FoamWorkspace, graph: FoamGraph, - uri: URI + uri: URI, + variant: 'backlink' | 'link' = 'backlink' ) { const connections = graph .getConnections(uri) .filter(c => c.target.asPlain().isEqual(uri)); - const backlinkItems = connections.map(async c => { - const item = await ResourceRangeTreeItem.createStandardItem( + const backlinkItems = connections.map(async c => + ResourceRangeTreeItem.createStandardItem( workspace, workspace.get(c.source), c.link.range, - 'backlink' - ); - item.description = item.label; - item.label = workspace.get(c.source).title; - return item; - }); + variant + ) + ); return Promise.all(backlinkItems); }