mirror of
https://github.com/foambubble/foam.git
synced 2026-01-12 23:48:19 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1b15eceed | ||
|
|
96f410a453 | ||
|
|
95a14d5dd6 | ||
|
|
10905fd703 |
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.22.1"
|
||||
"version": "0.22.2"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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})`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user