Compare commits

...

4 Commits

Author SHA1 Message Date
Riccardo Ferretti
f1b15eceed v0.22.2 2023-04-20 18:25:37 -07:00
Riccardo Ferretti
96f410a453 Prepare for next release 2023-04-20 18:25:06 -07:00
Riccardo Ferretti
95a14d5dd6 Create blocks markdown parser only once 2023-04-20 18:25:00 -07:00
Riccardo
10905fd703 Various improvements in tree view panels (#1201)
* Show note block in panels on hover preview
* Show tag references within tag explorer
* Improved structure of view related commands
* Refactored grouped resources tree data provider and added support for placeholders filter
  - Consolidated the naming of the accessory commands
  - Consolidated the management of the state/context related to grouping
  - Removed group-by config, simply restore the last used setting
  - Added filter to only show the placeholders related to the open file
* Refreshing placeholders when changing editor and filtering by active document
2023-04-21 03:20:22 +02:00
14 changed files with 399 additions and 213 deletions

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.22.1"
"version": "0.22.2"
}

View File

@@ -4,6 +4,16 @@ 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.22.2] - 2023-04-20
Fixes and Improvements:
- Support to show placeholders only for open file in panel (#1201, #988)
- Show note block in panels on hover preview (#1201, #800)
- Show tag references within tag explorer (#1201)
- Improved structure of view related commands (#1201)
- Ignore `.foam` directory
## [0.22.1] - 2023-04-15
Fixes and Improvements:

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.22.1",
"version": "0.22.2",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -96,29 +96,39 @@
},
{
"view": "foam-vscode.placeholders",
"contents": "No placeholders found. Pending links and notes without content will show up here."
"contents": "No placeholders found for selected resource or workspace."
}
],
"menus": {
"view/title": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == false",
"command": "foam-vscode.views.orphans.group-by:folder",
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'off'",
"group": "navigation"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
"command": "foam-vscode.views.orphans.group-by:off",
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == false",
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'all'",
"group": "navigation"
},
{
"command": "foam-vscode.group-placeholders-off",
"when": "view == foam-vscode.placeholders && foam-vscode.placeholders-grouped-by-folder == true",
"command": "foam-vscode.views.placeholders.show:all",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'for-current-file'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'off'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.group-by:off",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'folder'",
"group": "navigation"
}
],
@@ -132,19 +142,27 @@
"when": "false"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"command": "foam-vscode.views.orphans.group-by:folder",
"when": "false"
},
{
"command": "foam-vscode.group-orphans-off",
"command": "foam-vscode.views.orphans.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "false"
},
{
"command": "foam-vscode.group-placeholders-off",
"command": "foam-vscode.views.placeholders.show:all",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.group-by:off",
"when": "false"
},
{
@@ -215,23 +233,33 @@
"title": "Foam: Open Resource"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"title": "Foam: Group Orphans By Folder",
"command": "foam-vscode.views.orphans.group-by:folder",
"title": "Group By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-orphans-off",
"title": "Foam: Don't Group Orphans",
"command": "foam-vscode.views.orphans.group-by:off",
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.group-placeholders-by-folder",
"title": "Foam: Group Placeholders By Folder",
"command": "foam-vscode.views.placeholders.show:for-current-file",
"title": "Show placeholders in current file",
"icon": "$(file)"
},
{
"command": "foam-vscode.views.placeholders.show:all",
"title": "Show placeholders in workspace",
"icon": "$(files)"
},
{
"command": "foam-vscode.views.placeholders.group-by:folder",
"title": "Group By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-placeholders-off",
"title": "Foam: Don't Group Placeholders",
"command": "foam-vscode.views.placeholders.group-by:off",
"title": "Flat list",
"icon": "$(list-flat)"
},
{
@@ -375,21 +403,6 @@
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the orphans report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
},
"foam.orphans.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group orphans report entries by."
},
"foam.placeholders.exclude": {
"type": [
"array"
@@ -397,21 +410,6 @@
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the placeholders report. To ignore the all the content of a given folder, use `**<folderName>/**/*`"
},
"foam.placeholders.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group blank note report entries by."
},
"foam.dateSnippets.afterCompletion": {
"type": "string",
"default": "createNote",

View File

@@ -1,4 +1,8 @@
import { createMarkdownParser, ParserPlugin } from './markdown-parser';
import {
createMarkdownParser,
getBlockFor,
ParserPlugin,
} from './markdown-parser';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { Range } from '../model/range';
@@ -459,3 +463,48 @@ But with some content.
]);
});
});
describe('Block detection', () => {
const md = `
- this is block 1
- this is [[block]] 2
- this is block 2.1
- this is block 3
- this is block 3.1
- this is block 3.1.1
- this is block 3.2
- this is block 4
this is a simple line
this is another simple line
`;
it('can detect block', () => {
const { block } = getBlockFor(md, 1);
expect(block).toEqual('- this is block 1');
});
it('supports nested blocks 1', () => {
const { block } = getBlockFor(md, 2);
expect(block).toEqual(`- this is [[block]] 2
- this is block 2.1`);
});
it('supports nested blocks 2', () => {
const { block } = getBlockFor(md, 5);
expect(block).toEqual(` - this is block 3.1
- this is block 3.1.1`);
});
it('returns the line if no block is detected', () => {
const { block } = getBlockFor(md, 9);
expect(block).toEqual(`this is a simple line`);
});
it('is compatible with Range object', () => {
const note = parser.parse(URI.file('/path/to/a'), md);
const { start } = note.links[0].range;
const { block } = getBlockFor(md, start);
expect(block).toEqual(`- this is [[block]] 2
- this is block 2.1`);
});
});

View File

@@ -424,3 +424,28 @@ const astPositionToFoamRange = (pos: AstPosition): Range =>
pos.end.line - 1,
pos.end.column - 1
);
const blockParser = unified().use(markdownParse, { gfm: true });
export const getBlockFor = (
markdown: string,
line: number | Position
): { block: string; nLines: number } => {
const searchLine = typeof line === 'number' ? line : line.line;
const tree = blockParser.parse(markdown);
const lines = markdown.split('\n');
let block = null;
let nLines = 0;
visit(tree, ['listItem'], (node: any) => {
if (node.position.start.line === searchLine + 1) {
block = lines
.slice(node.position.start.line - 1, node.position.end.line)
.join('\n');
nLines = node.position.end.line - node.position.start.line;
return visit.EXIT;
}
});
if (block == null) {
block = lines[searchLine];
}
return { block, nLines };
};

View File

@@ -20,6 +20,8 @@ const feature: FoamFeature = {
const provider = new GroupedResourcesTreeDataProvider(
'orphans',
'orphan',
context.globalState,
matcher,
() =>
foam.graph
.getAllNodes()
@@ -32,21 +34,20 @@ const feature: FoamFeature = {
return uri.isPlaceholder()
? new UriTreeItem(uri)
: new ResourceTreeItem(foam.workspace.find(uri), foam.workspace);
},
matcher
}
);
provider.setGroupBy(getOrphansConfig().groupBy);
const treeView = vscode.window.createTreeView('foam-vscode.orphans', {
treeDataProvider: provider,
showCollapseAll: true,
});
provider.refresh();
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${provider.numElements})`;
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
...provider.commands,
provider,
foam.graph.onDidUpdate(() => {
provider.refresh();
treeView.title = baseTitle + ` (${provider.numElements})`;

View File

@@ -9,6 +9,10 @@ import {
UriTreeItem,
groupRangesByResource,
} from '../../utils/tree-view-utils';
import { IMatcher } from '../../core/services/datastore';
import { ContextMemento, fromVsCodeUri } from '../../utils/vsc-utils';
import { FoamGraph } from '../../core/model/graph';
import { URI } from '../../core/model/uri';
const feature: FoamFeature = {
activate: async (
@@ -19,10 +23,56 @@ const feature: FoamFeature = {
const { matcher } = await createMatcherAndDataStore(
getPlaceholdersConfig().exclude
);
const provider = new GroupedResourcesTreeDataProvider(
const provider = new PlaceholderTreeView(
context.globalState,
foam,
matcher
);
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
treeDataProvider: provider,
showCollapseAll: true,
});
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${provider.numElements})`;
provider.refresh();
context.subscriptions.push(
treeView,
provider,
foam.graph.onDidUpdate(() => {
provider.refresh();
}),
provider.onDidChangeTreeData(() => {
treeView.title = baseTitle + ` (${provider.numElements})`;
}),
vscode.window.onDidChangeActiveTextEditor(() => {
if (provider.show.get() === 'for-current-file') {
provider.refresh();
}
})
);
},
};
export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
private graph: FoamGraph;
public show = new ContextMemento<'all' | 'for-current-file'>(
this.state,
`foam-vscode.views.${this.providerId}.show`,
'all'
);
public constructor(state: vscode.Memento, foam: Foam, matcher: IMatcher) {
super(
'placeholders',
'placeholder',
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
state,
matcher,
() => {
// we override computeResources below (as we can't use "this" here)
throw new Error('Not implemented');
},
uri => {
return new UriTreeItem(uri, {
icon: 'link',
@@ -39,27 +89,39 @@ const feature: FoamFeature = {
);
},
});
},
matcher
}
);
provider.setGroupBy(getPlaceholdersConfig().groupBy);
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
treeDataProvider: provider,
showCollapseAll: true,
});
const baseTitle = treeView.title;
treeView.title = baseTitle + ` (${provider.numElements})`;
context.subscriptions.push(
treeView,
...provider.commands,
foam.graph.onDidUpdate(() => {
provider.refresh();
treeView.title = baseTitle + ` (${provider.numElements})`;
})
this.graph = foam.graph;
this.disposables.push(
vscode.commands.registerCommand(
`foam-vscode.views.${this.providerId}.show:all`,
() => {
this.show.update('all');
this.refresh();
}
),
vscode.commands.registerCommand(
`foam-vscode.views.${this.providerId}.show:for-current-file`,
() => {
this.show.update('for-current-file');
this.refresh();
}
)
);
},
};
}
computeResources = (): URI[] => {
if (this.show.get() === 'for-current-file') {
const currentFile = vscode.window.activeTextEditor?.document.uri;
return currentFile
? this.graph
.getLinks(fromVsCodeUri(currentFile))
.map(link => link.target)
.filter(uri => uri.isPlaceholder())
: [];
}
return this.graph.getAllNodes().filter(uri => uri.isPlaceholder());
};
}
export default feature;

View File

@@ -1,8 +1,9 @@
import { createTestNote } from '../../test/test-utils';
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
import { TagItem, TagReference, TagsProvider } from './tags-explorer';
import { TagItem, TagsProvider } from './tags-explorer';
import { FoamTags } from '../../core/model/tags';
import { FoamWorkspace } from '../../core/model/workspace';
import { ResourceTreeItem } from '../../utils/tree-view-utils';
describe('Tags tree panel', () => {
beforeAll(async () => {
@@ -122,9 +123,9 @@ describe('Tags tree panel', () => {
)) as TagItem[];
childTreeItems
.filter(item => item instanceof TagReference)
.filter(item => item instanceof ResourceTreeItem)
.forEach(item => {
expect(item.title).toEqual('Test note');
expect(item.label).toEqual('Test note');
});
childTreeItems

View File

@@ -7,6 +7,11 @@ import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import { Resource, Tag } from '../../core/model/note';
import { FoamTags } from '../../core/model/tags';
import {
ResourceRangeTreeItem,
ResourceTreeItem,
groupRangesByResource,
} from '../../utils/tree-view-utils';
const TAG_SEPARATOR = '/';
const feature: FoamFeature = {
@@ -71,7 +76,11 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
return element;
}
getChildren(element?: TagItem): Promise<TagTreeItem[]> {
async getChildren(element?: TagItem): Promise<TagTreeItem[]> {
if ((element as any)?.getChildren) {
const children = await (element as any).getChildren();
return children;
}
const parentTag = element ? element.tag : '';
const parentPrefix = element ? parentTag + TAG_SEPARATOR : '';
@@ -96,45 +105,52 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
return acc;
}, new Map() as Map<string, { label: string; tagId: string; nResources: number }>);
const tagChildren = Array.from(tagsAtThisLevel.values())
const subtags = Array.from(tagsAtThisLevel.values())
.map(({ label, tagId, nResources }) => {
const resources = this.foamTags.tags.get(tagId) ?? [];
return new TagItem(tagId, label, nResources, resources);
})
.sort((a, b) => a.title.localeCompare(b.title));
const referenceChildren: TagTreeItem[] = (element?.notes ?? [])
const resourceTags: ResourceRangeTreeItem[] = (element?.notes ?? [])
.map(uri => this.workspace.get(uri))
.reduce((acc, note) => {
const tags = note.tags.filter(t => t.label === element.tag);
return [
...acc,
...tags.slice(0, 1).map(t => new TagReference(t, note)),
];
}, [])
.sort((a, b) => a.title.localeCompare(b.title));
const items = tags.map(t =>
ResourceRangeTreeItem.createStandardItem(
this.workspace,
note,
t.range
)
);
return [...acc, ...items];
}, []);
const resources = await groupRangesByResource(this.workspace, resourceTags);
return Promise.resolve(
[
element && new TagSearch(element.tag),
...tagChildren,
...referenceChildren,
].filter(Boolean)
[element && new TagSearch(element.tag), ...subtags, ...resources].filter(
Boolean
)
);
}
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
if (item instanceof TagReference) {
const content = await this.workspace.readAsMarkdown(item.note.uri);
if (isSome(content)) {
item.tooltip = getNoteTooltip(content);
}
if (
item instanceof ResourceTreeItem ||
item instanceof ResourceRangeTreeItem
) {
return item.resolveTreeItem();
}
return item;
return Promise.resolve(item);
}
}
type TagTreeItem = TagItem | TagReference | TagSearch;
type TagTreeItem =
| TagItem
| TagSearch
| ResourceTreeItem
| ResourceRangeTreeItem;
export class TagItem extends vscode.TreeItem {
constructor(
@@ -176,28 +192,3 @@ export class TagSearch extends vscode.TreeItem {
iconPath = new vscode.ThemeIcon('search');
contextValue = 'tag-search';
}
export class TagReference extends vscode.TreeItem {
public readonly title: string;
constructor(public readonly tag: Tag, public readonly note: Resource) {
super(note.title, vscode.TreeItemCollapsibleState.None);
const uri = toVsCodeUri(note.uri);
this.title = note.title;
this.description = vscode.workspace.asRelativePath(uri);
this.tooltip = undefined;
this.command = {
command: 'vscode.open',
arguments: [
uri,
{
preview: true,
selection: toVsCodeRange(tag.range),
},
],
title: 'Open File',
};
}
iconPath = new vscode.ThemeIcon('note');
contextValue = 'reference';
}

View File

@@ -19,6 +19,7 @@ export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting
/** Retrieve the list of file ignoring globs. */
export function getIgnoredFilesSetting(): GlobPattern[] {
return [
'**/.foam/**',
...workspace.getConfiguration().get('foam.files.ignore', []),
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
];
@@ -42,24 +43,16 @@ export function getFoamLoggerLevel(): LogLevel {
export function getOrphansConfig(): GroupedResourcesConfig {
const orphansConfig = workspace.getConfiguration('foam.orphans');
const exclude: string[] = orphansConfig.get('exclude');
const groupBy: GroupedResoucesConfigGroupBy = orphansConfig.get('groupBy');
return { exclude, groupBy };
return { exclude };
}
/** Retrieve the placeholders configuration */
export function getPlaceholdersConfig(): GroupedResourcesConfig {
const placeholderCfg = workspace.getConfiguration('foam.placeholders');
const exclude: string[] = placeholderCfg.get('exclude');
const groupBy: GroupedResoucesConfigGroupBy = placeholderCfg.get('groupBy');
return { exclude, groupBy };
return { exclude };
}
export interface GroupedResourcesConfig {
exclude: string[];
groupBy: GroupedResoucesConfigGroupBy;
}
export enum GroupedResoucesConfigGroupBy {
Folder = 'folder',
Off = 'off',
}

View File

@@ -3,14 +3,14 @@ import {
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,
} from './grouped-resources-tree-data-provider';
import { ResourceTreeItem, UriTreeItem } from './tree-view-utils';
import { randomString } from '../test/test-utils';
import { MapBasedMemento } from './vsc-utils';
const testMatcher = new SubstringExcludeMatcher('path-exclude');
@@ -37,17 +37,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
it('should return the grouped resources as a folder tree', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
randomString(),
'note',
new MapBasedMemento(),
testMatcher,
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri),
testMatcher
uri => new UriTreeItem(uri)
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
provider.groupBy.update('folder');
provider.refresh();
const result = await provider.getChildren();
expect(result).toMatchObject([
{
@@ -67,17 +69,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
it('should return the grouped resources in a directory', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
randomString(),
'note',
new MapBasedMemento(),
testMatcher,
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new ResourceTreeItem(workspace.get(uri), workspace),
testMatcher
uri => new ResourceTreeItem(workspace.get(uri), workspace)
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
provider.groupBy.update('folder');
provider.refresh();
const directory = new DirectoryTreeItem(
'/path',
@@ -97,17 +101,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
it('should return the flattened resources', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
randomString(),
'note',
new MapBasedMemento(),
testMatcher,
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new ResourceTreeItem(workspace.get(uri), workspace),
testMatcher
uri => new ResourceTreeItem(workspace.get(uri), workspace)
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Off);
provider.groupBy.update('off');
provider.refresh();
const result = await provider.getChildren();
expect(result).toMatchObject([
@@ -128,17 +134,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
it('should return the grouped resources without exclusion', async () => {
const provider = new GroupedResourcesTreeDataProvider(
'length3',
randomString(),
'note',
new MapBasedMemento(),
new AlwaysIncludeMatcher(),
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri),
new AlwaysIncludeMatcher()
uri => new UriTreeItem(uri)
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
provider.groupBy.update('folder');
provider.refresh();
const result = await provider.getChildren();
expect(result).toMatchObject([
@@ -156,17 +164,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
it('should dynamically set the description', async () => {
const description = 'test description';
const provider = new GroupedResourcesTreeDataProvider(
'length3',
randomString(),
description,
new MapBasedMemento(),
testMatcher,
() =>
workspace
.list()
.filter(r => r.title.length === 3)
.map(r => r.uri),
uri => new UriTreeItem(uri),
testMatcher
uri => new UriTreeItem(uri)
);
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
provider.groupBy.update('folder');
provider.refresh();
const result = await provider.getChildren();
expect(result).toMatchObject([
{

View File

@@ -1,10 +1,10 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { GroupedResoucesConfigGroupBy } from '../settings';
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 './vsc-utils';
/**
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
@@ -13,8 +13,8 @@ import { UriTreeItem } from './tree-view-utils';
*
* **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file:
* ```
* foam-vscode.group-${providerId}-by-folder
* foam-vscode.group-${providerId}-off
* 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:
* ```
@@ -34,24 +34,29 @@ import { UriTreeItem } from './tree-view-utils';
* @implements {vscode.TreeDataProvider<GroupedResourceTreeItem>}
*/
export class GroupedResourcesTreeDataProvider
implements vscode.TreeDataProvider<GroupedResourceTreeItem>
implements
vscode.TreeDataProvider<GroupedResourceTreeItem>,
vscode.Disposable
{
// prettier-ignore
private _onDidChangeTreeData: vscode.EventEmitter<GroupedResourceTreeItem | undefined | void> = new vscode.EventEmitter<GroupedResourceTreeItem | undefined | void>();
// prettier-ignore
readonly onDidChangeTreeData: vscode.Event<GroupedResourceTreeItem | undefined | void> = this._onDidChangeTreeData.event;
// prettier-ignore
private groupBy: GroupedResoucesConfigGroupBy = GroupedResoucesConfigGroupBy.Folder;
private exclude: string[] = [];
private flatUris: Array<URI> = [];
private root = vscode.workspace.workspaceFolders[0].uri.path;
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.group-${providerId}-by-folder
* foam-vscode.group-${providerId}-off
* 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:
* ```
@@ -75,47 +80,39 @@ export class GroupedResourcesTreeDataProvider
* @memberof GroupedResourcesTreeDataProvider
*/
constructor(
private providerId: string,
protected providerId: string,
private resourceName: string,
private computeResources: () => Array<URI>,
private createTreeItem: (item: URI) => GroupedResourceTreeItem,
private matcher: IMatcher
protected state: vscode.Memento,
private matcher: IMatcher,
protected computeResources: () => Array<URI>,
private createTreeItem: (item: URI) => GroupedResourceTreeItem
) {
this.setContext();
this.doComputeResources();
this.disposables.push(
vscode.commands.registerCommand(
`foam-vscode.views.${this.providerId}.group-by:folder`,
() => {
this.groupBy.update('folder');
this.refresh();
}
),
vscode.commands.registerCommand(
`foam-vscode.views.${this.providerId}.group-by:off`,
() => {
this.groupBy.update('off');
this.refresh();
}
)
);
}
public get commands() {
return [
vscode.commands.registerCommand(
`foam-vscode.group-${this.providerId}-by-folder`,
() => this.setGroupBy(GroupedResoucesConfigGroupBy.Folder)
),
vscode.commands.registerCommand(
`foam-vscode.group-${this.providerId}-off`,
() => this.setGroupBy(GroupedResoucesConfigGroupBy.Off)
),
];
dispose() {
this.disposables.forEach(d => d.dispose());
}
public get numElements() {
return this.flatUris.length;
}
setGroupBy(groupBy: GroupedResoucesConfigGroupBy): void {
this.groupBy = groupBy;
this.setContext();
this.refresh();
}
private setContext(): void {
vscode.commands.executeCommand(
'setContext',
`foam-vscode.${this.providerId}-grouped-by-folder`,
this.groupBy === GroupedResoucesConfigGroupBy.Folder
);
}
refresh(): void {
this.doComputeResources();
this._onDidChangeTreeData.fire();
@@ -129,10 +126,9 @@ export class GroupedResourcesTreeDataProvider
item?: GroupedResourceTreeItem
): Promise<GroupedResourceTreeItem[]> {
if ((item as any)?.getChildren) {
const children = await (item as any).getChildren();
return children.sort(sortByTreeItemLabel);
return (item as any).getChildren();
}
if (this.groupBy === GroupedResoucesConfigGroupBy.Folder) {
if (this.groupBy.get() === 'folder') {
const directories = Object.entries(this.getUrisByDirectory())
.sort(([dir1], [dir2]) => sortByString(dir1, dir2))
.map(

View File

@@ -8,6 +8,7 @@ import { FoamWorkspace } from '../core/model/workspace';
import { getNoteTooltip } from '../utils';
import { isSome } from '../core/utils';
import { groupBy } from 'lodash';
import { getBlockFor } from '../core/services/markdown-parser';
export class UriTreeItem extends vscode.TreeItem {
private doGetChildren: () => Promise<vscode.TreeItem[]>;
@@ -88,7 +89,10 @@ export class ResourceRangeTreeItem extends vscode.TreeItem {
constructor(
public label: string,
public readonly resource: Resource,
public readonly range: Range
public readonly range: Range,
private resolveFn?: (
item: ResourceRangeTreeItem
) => Promise<ResourceRangeTreeItem>
) {
super(label, vscode.TreeItemCollapsibleState.None);
this.label = `${range.start.line}: ${this.label}`;
@@ -100,7 +104,7 @@ export class ResourceRangeTreeItem extends vscode.TreeItem {
}
resolveTreeItem(): Promise<ResourceRangeTreeItem> {
return Promise.resolve(this);
return this.resolveFn ? this.resolveFn(this) : Promise.resolve(this);
}
static async createStandardItem(
@@ -108,9 +112,8 @@ export class ResourceRangeTreeItem extends vscode.TreeItem {
resource: Resource,
range: Range
): Promise<ResourceRangeTreeItem> {
const lines = ((await workspace.readAsMarkdown(resource.uri)) ?? '').split(
'\n'
);
const markdown = (await workspace.readAsMarkdown(resource.uri)) ?? '';
const lines = markdown.split('\n');
const line = lines[range.start.line];
const start = Math.max(0, range.start.character - 15);
@@ -119,9 +122,22 @@ export class ResourceRangeTreeItem extends vscode.TreeItem {
const label = line
? `${range.start.line}: ${ellipsis}${line.slice(start, start + 300)}`
: Range.toString(range);
const tooltip = line && getNoteTooltip(line);
const item = new ResourceRangeTreeItem(label, resource, range);
item.tooltip = tooltip;
const resolveFn = (item: ResourceRangeTreeItem) => {
let { block, nLines } = getBlockFor(markdown, range.start);
// Long blocks need to be interrupted or they won't display in hover preview
// We keep the extra lines so that the count in the preview is correct
if (nLines > 15) {
let tmp = block.split('\n');
tmp.splice(15, 1, '\n'); // replace a line with a blank line to interrupt the block
block = tmp.join('\n');
}
const tooltip = getNoteTooltip(block ?? line ?? '');
item.tooltip = tooltip;
return Promise.resolve(item);
};
const item = new ResourceRangeTreeItem(label, resource, range, resolveFn);
return item;
}
}

View File

@@ -1,4 +1,4 @@
import { Position, Range, Uri } from 'vscode';
import { Memento, Position, Range, Uri, commands } from 'vscode';
import { Position as FoamPosition } from '../core/model/position';
import { Range as FoamRange } from '../core/model/range';
import { URI as FoamURI } from '../core/model/uri';
@@ -12,3 +12,37 @@ export const toVsCodeRange = (r: FoamRange): Range =>
export const toVsCodeUri = (u: FoamURI): Uri => Uri.parse(u.toString());
export const fromVsCodeUri = (u: Uri): FoamURI => FoamURI.parse(u.toString());
/**
* A class that wraps context value, syncs it via setContext, and provides a typed interface to it.
*/
export class ContextMemento<T> {
constructor(private data: Memento, private key: string, defaultValue: T) {
const value = data.get(key) ?? defaultValue;
commands.executeCommand('setContext', this.key, value);
}
public get(): T {
return this.data.get(this.key);
}
public async update(value: T): Promise<void> {
this.data.update(this.key, value);
await commands.executeCommand('setContext', this.key, value);
}
}
/**
* Implementation of the Memento interface that uses a Map as backend
*/
export class MapBasedMemento implements Memento {
get<T>(key: unknown, defaultValue?: unknown | T): T | T {
return (this.map.get(key as string) as T) || (defaultValue as T);
}
private map: Map<string, string> = new Map();
keys(): readonly string[] {
return Array.from(this.map.keys());
}
update(key: string, value: any): Promise<void> {
this.map.set(key, value);
return Promise.resolve();
}
}