mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4eaf5c5ff | ||
|
|
b4830eaf30 | ||
|
|
0cda6aed50 | ||
|
|
89c9bb5a7f | ||
|
|
941e870a65 | ||
|
|
c6655c33ff | ||
|
|
c94fb18f8a | ||
|
|
cbd55bac74 |
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.21.4"
|
||||
"version": "0.22.1"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,20 @@ 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.1] - 2023-04-15
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Allow the `#` char to trigger tag autocompletion (#1192, #1189 - thanks @jimgraham)
|
||||
|
||||
## [0.22.0] - 2023-04-15
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for deep tag hierarchy in Tag Explorer panel (#1134, #1194)
|
||||
- Consolidated and improved Backlinks, Placeholders and Orphans panels (#1196)
|
||||
- Fixed note resolution when using template without defined path (#1197)
|
||||
|
||||
## [0.21.4] - 2023-04-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
33
packages/foam-vscode/LICENSE
Normal file
33
packages/foam-vscode/LICENSE
Normal file
@@ -0,0 +1,33 @@
|
||||
The MIT Licence (MIT)
|
||||
|
||||
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Where noted, some code uses the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.21.4",
|
||||
"version": "0.22.1",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
|
||||
@@ -69,4 +69,8 @@ export abstract class Range {
|
||||
static isBefore(a: Range, b: Range): number {
|
||||
return a.start.line - b.start.line || a.start.character - b.start.character;
|
||||
}
|
||||
|
||||
static toString(range: Range): string {
|
||||
return `${range.start.line}:${range.start.character} - ${range.end.line}:${range.end.character}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ import {
|
||||
createNote,
|
||||
getUriInWorkspace,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { BacklinksTreeDataProvider, BacklinkTreeItem } from './backlinks';
|
||||
import { ResourceTreeItem } from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { BacklinksTreeDataProvider } from './backlinks';
|
||||
import { OPEN_COMMAND } from '../commands/open-resource';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
} from '../../utils/tree-view-utils';
|
||||
|
||||
describe('Backlinks panel', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -84,19 +87,19 @@ describe('Backlinks panel', () => {
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
)) as BacklinkTreeItem[];
|
||||
expect(linksFromB.map(l => l.link)).toEqual(
|
||||
noteB.links.sort(
|
||||
(a, b) => a.range.start.character - b.range.start.character
|
||||
)
|
||||
)) as ResourceRangeTreeItem[];
|
||||
expect(linksFromB.map(l => l.range)).toEqual(
|
||||
noteB.links
|
||||
.map(l => l.range)
|
||||
.sort((a, b) => a.start.character - b.start.character)
|
||||
);
|
||||
});
|
||||
it('navigates to the document if clicking on note', async () => {
|
||||
provider.target = noteA.uri;
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes[0].command).toMatchObject({
|
||||
command: OPEN_COMMAND.command,
|
||||
arguments: [expect.objectContaining({ uri: noteB.uri })],
|
||||
command: 'vscode.open',
|
||||
arguments: [expect.objectContaining({ path: noteB.uri.path })],
|
||||
});
|
||||
});
|
||||
it('navigates to document with link selection if clicking on backlink', async () => {
|
||||
@@ -104,11 +107,11 @@ describe('Backlinks panel', () => {
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
)) as BacklinkTreeItem[];
|
||||
)) as ResourceRangeTreeItem[];
|
||||
expect(linksFromB[0].command).toMatchObject({
|
||||
command: 'vscode.open',
|
||||
arguments: [
|
||||
noteB.uri,
|
||||
expect.objectContaining({ path: noteB.uri.path }),
|
||||
{
|
||||
selection: expect.arrayContaining([]),
|
||||
},
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { groupBy } from 'lodash';
|
||||
import { URI } from '../../core/model/uri';
|
||||
|
||||
import { getNoteTooltip, isNone } from '../../utils';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { ResourceTreeItem } from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { Resource, ResourceLink } from '../../core/model/note';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
groupRangesByResource,
|
||||
} from '../../utils/tree-view-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -37,7 +38,7 @@ const feature: FoamFeature = {
|
||||
export default feature;
|
||||
|
||||
export class BacklinksTreeDataProvider
|
||||
implements vscode.TreeDataProvider<BacklinkPanelTreeItem>
|
||||
implements vscode.TreeDataProvider<vscode.TreeItem>
|
||||
{
|
||||
public target?: URI = undefined;
|
||||
// prettier-ignore
|
||||
@@ -54,67 +55,32 @@ export class BacklinksTreeDataProvider
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(item?: ResourceTreeItem): Promise<BacklinkPanelTreeItem[]> {
|
||||
getChildren(item?: BacklinkPanelTreeItem): Promise<vscode.TreeItem[]> {
|
||||
const uri = this.target;
|
||||
if (item) {
|
||||
const resource = item.resource;
|
||||
|
||||
const backlinkRefs = Promise.all(
|
||||
resource.links
|
||||
.filter(link =>
|
||||
this.workspace.resolveLink(resource, link).asPlain().isEqual(uri)
|
||||
)
|
||||
.map(async link => {
|
||||
const item = new BacklinkTreeItem(resource, link);
|
||||
const lines = (
|
||||
(await this.workspace.readAsMarkdown(resource.uri)) ?? ''
|
||||
).split('\n');
|
||||
if (link.range.start.line < lines.length) {
|
||||
const line = lines[link.range.start.line];
|
||||
const start = Math.max(0, link.range.start.character - 15);
|
||||
const ellipsis = start === 0 ? '' : '...';
|
||||
|
||||
item.label = `${link.range.start.line}: ${ellipsis}${line.substr(
|
||||
start,
|
||||
300
|
||||
)}`;
|
||||
item.tooltip = getNoteTooltip(line);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
return backlinkRefs;
|
||||
if (item && item instanceof ResourceTreeItem) {
|
||||
return item.getChildren();
|
||||
}
|
||||
|
||||
if (isNone(uri) || isNone(this.workspace.find(uri))) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const backlinksByResourcePath = groupBy(
|
||||
this.graph
|
||||
.getConnections(uri)
|
||||
.filter(c => c.target.asPlain().isEqual(uri)),
|
||||
b => b.source.path
|
||||
);
|
||||
const connections = this.graph
|
||||
.getConnections(uri)
|
||||
.filter(c => c.target.asPlain().isEqual(uri));
|
||||
|
||||
const resources = Object.keys(backlinksByResourcePath)
|
||||
.map(res => backlinksByResourcePath[res][0].source)
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.sort(Resource.sortByTitle)
|
||||
.map(note => {
|
||||
const connections = backlinksByResourcePath[note.uri.path].sort(
|
||||
(a, b) => Range.isBefore(a.link.range, b.link.range)
|
||||
);
|
||||
const item = new ResourceTreeItem(
|
||||
note,
|
||||
this.workspace,
|
||||
vscode.TreeItemCollapsibleState.Expanded
|
||||
);
|
||||
item.description = `(${connections.length}) ${item.description}`;
|
||||
return item;
|
||||
});
|
||||
return Promise.resolve(resources);
|
||||
const backlinkItems = connections.map(c =>
|
||||
ResourceRangeTreeItem.createStandardItem(
|
||||
this.workspace,
|
||||
this.workspace.get(c.source),
|
||||
c.link.range
|
||||
)
|
||||
);
|
||||
return groupRangesByResource(
|
||||
this.workspace,
|
||||
backlinkItems,
|
||||
vscode.TreeItemCollapsibleState.Expanded
|
||||
);
|
||||
}
|
||||
|
||||
resolveTreeItem(item: BacklinkPanelTreeItem): Promise<BacklinkPanelTreeItem> {
|
||||
@@ -122,23 +88,4 @@ export class BacklinksTreeDataProvider
|
||||
}
|
||||
}
|
||||
|
||||
export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
public readonly link: ResourceLink
|
||||
) {
|
||||
super(link.rawText, vscode.TreeItemCollapsibleState.None);
|
||||
this.label = `${link.range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [toVsCodeUri(resource.uri), { selection: link.range }],
|
||||
title: 'Go to link',
|
||||
};
|
||||
}
|
||||
|
||||
resolveTreeItem(): Promise<BacklinkTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
|
||||
type BacklinkPanelTreeItem = ResourceTreeItem | BacklinkTreeItem;
|
||||
type BacklinkPanelTreeItem = ResourceTreeItem | ResourceRangeTreeItem;
|
||||
|
||||
@@ -3,11 +3,8 @@ import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getOrphansConfig } from '../../settings';
|
||||
import { FoamFeature } from '../../types';
|
||||
import {
|
||||
GroupedResourcesTreeDataProvider,
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { GroupedResourcesTreeDataProvider } from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from '../../utils/tree-view-utils';
|
||||
|
||||
const EXCLUDE_TYPES = ['image', 'attachment'];
|
||||
const feature: FoamFeature = {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getPlaceholdersConfig } from '../../settings';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { GroupedResourcesTreeDataProvider } from '../../utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
GroupedResourcesTreeDataProvider,
|
||||
ResourceRangeTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../../utils/grouped-resources-tree-data-provider';
|
||||
groupRangesByResource,
|
||||
} from '../../utils/tree-view-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -22,7 +24,21 @@ const feature: FoamFeature = {
|
||||
'placeholder',
|
||||
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
|
||||
uri => {
|
||||
return new UriTreeItem(uri);
|
||||
return new UriTreeItem(uri, {
|
||||
icon: 'link',
|
||||
getChildren: async () => {
|
||||
return groupRangesByResource(
|
||||
foam.workspace,
|
||||
foam.graph.getBacklinks(uri).map(link => {
|
||||
return ResourceRangeTreeItem.createStandardItem(
|
||||
foam.workspace,
|
||||
foam.workspace.get(link.source),
|
||||
link.link.range
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
matcher
|
||||
);
|
||||
|
||||
@@ -1,65 +1,48 @@
|
||||
import {
|
||||
createTestNote,
|
||||
readFileFromFs,
|
||||
TEST_DATA_DIR,
|
||||
} from '../../test/test-utils';
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagReference, TagsProvider } from './tags-explorer';
|
||||
import { bootstrap, Foam } from '../../core/model/foam';
|
||||
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { FileDataStore, Matcher } from '../../test/test-datastore';
|
||||
import { FoamTags } from '../../core/model/tags';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const dataStore = new FileDataStore(readFileFromFs, TEST_DATA_DIR.toFsPath());
|
||||
const matcher = new Matcher([URI.file(TEST_DATA_DIR.toFsPath())]);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
_foam.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(matcher, undefined, dataStore, parser, [
|
||||
mdProvider,
|
||||
]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_foam.dispose();
|
||||
});
|
||||
|
||||
it('correctly provides a tag from a set of notes', async () => {
|
||||
it('provides a tag from a set of notes', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: ['test'],
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
provider.refresh();
|
||||
|
||||
const treeItems = (await provider.getChildren()) as TagItem[];
|
||||
|
||||
treeItems.forEach(item => expect(item.tag).toContain('test'));
|
||||
expect(treeItems).toHaveLength(1);
|
||||
expect(treeItems[0].label).toEqual('test');
|
||||
expect(treeItems[0].tag).toEqual('test');
|
||||
expect(treeItems[0].nResourcesInSubtree).toEqual(1);
|
||||
});
|
||||
|
||||
it('correctly handles a parent and child tag', async () => {
|
||||
it('handles a simple parent and child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: ['parent/child'],
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -78,17 +61,18 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and multiple child tag', async () => {
|
||||
it('handles a single parent and multiple child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: ['parent/child'],
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
const noteB = createTestNote({
|
||||
tags: ['parent/subchild'],
|
||||
uri: './note-b.md',
|
||||
});
|
||||
_foam.workspace.set(noteB);
|
||||
const workspace = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -114,14 +98,15 @@ describe('Tags tree panel', () => {
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and child tag in the same note', async () => {
|
||||
it('handles a parent and child tag in the same note', async () => {
|
||||
const noteC = createTestNote({
|
||||
tags: ['main', 'main/subtopic'],
|
||||
title: 'Test note',
|
||||
uri: './note-c.md',
|
||||
});
|
||||
|
||||
_foam.workspace.set(noteC);
|
||||
const workspace = new FoamWorkspace().set(noteC);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
@@ -151,4 +136,36 @@ describe('Tags tree panel', () => {
|
||||
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('handles a tag with multiple levels of hierarchy - #1134', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: ['parent/child/second'],
|
||||
uri: './note-a.md',
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
)) as TagItem[];
|
||||
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
expect(childTreeItems[0].label).toMatch(/^Search.*/);
|
||||
expect(childTreeItems[1].label).toEqual('child');
|
||||
|
||||
const grandchildTreeItems = (await provider.getChildren(
|
||||
childTreeItems[1]
|
||||
)) as TagItem[];
|
||||
|
||||
expect(grandchildTreeItems).toHaveLength(2);
|
||||
expect(grandchildTreeItems[0].label).toMatch(/^Search.*/);
|
||||
expect(grandchildTreeItems[1].label).toEqual('second');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
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';
|
||||
|
||||
const TAG_SEPARATOR = '/';
|
||||
const feature: FoamFeature = {
|
||||
@@ -14,7 +15,7 @@ const feature: FoamFeature = {
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const provider = new TagsProvider(foam, foam.workspace);
|
||||
const provider = new TagsProvider(foam.tags, foam.workspace);
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.tags-explorer', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
@@ -48,7 +49,10 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
notes: URI[];
|
||||
}[];
|
||||
|
||||
constructor(private foam: Foam, private workspace: FoamWorkspace) {
|
||||
private foamTags: FoamTags;
|
||||
|
||||
constructor(tags: FoamTags, private workspace: FoamWorkspace) {
|
||||
this.foamTags = tags;
|
||||
this.computeTags();
|
||||
}
|
||||
|
||||
@@ -58,7 +62,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
}
|
||||
|
||||
private computeTags() {
|
||||
this.tags = [...this.foam.tags.tags]
|
||||
this.tags = [...this.foamTags.tags]
|
||||
.map(([tag, notes]) => ({ tag, notes }))
|
||||
.sort((a, b) => a.tag.localeCompare(b.tag));
|
||||
}
|
||||
@@ -68,53 +72,55 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
}
|
||||
|
||||
getChildren(element?: TagItem): Promise<TagTreeItem[]> {
|
||||
if (element) {
|
||||
const nestedTagItems: TagTreeItem[] = this.tags
|
||||
.filter(item => item.tag.indexOf(element.title + TAG_SEPARATOR) > -1)
|
||||
.map(
|
||||
item =>
|
||||
new TagItem(
|
||||
item.tag,
|
||||
item.tag.substring(item.tag.indexOf(TAG_SEPARATOR) + 1),
|
||||
item.notes
|
||||
)
|
||||
)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const parentTag = element ? element.tag : '';
|
||||
const parentPrefix = element ? parentTag + TAG_SEPARATOR : '';
|
||||
|
||||
const references: TagTreeItem[] = element.notes
|
||||
.map(uri => this.foam.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 tagsAtThisLevel = this.tags
|
||||
.filter(({ tag }) => tag.startsWith(parentPrefix))
|
||||
.map(({ tag }) => {
|
||||
const nextSeparator = tag.indexOf(TAG_SEPARATOR, parentPrefix.length);
|
||||
const label =
|
||||
nextSeparator > -1
|
||||
? tag.substring(parentPrefix.length, nextSeparator)
|
||||
: tag.substring(parentPrefix.length);
|
||||
const tagId = parentPrefix + label;
|
||||
return { label, tagId, tag };
|
||||
})
|
||||
.reduce((acc, { label, tagId, tag }) => {
|
||||
const existing = acc.has(label);
|
||||
const nResources = this.foamTags.tags.get(tag).length ?? 0;
|
||||
if (!existing) {
|
||||
acc.set(label, { label, tagId, nResources: 0 });
|
||||
}
|
||||
acc.get(label).nResources += nResources;
|
||||
return acc;
|
||||
}, new Map() as Map<string, { label: string; tagId: string; nResources: number }>);
|
||||
|
||||
return Promise.resolve([
|
||||
new TagSearch(element.tag),
|
||||
...nestedTagItems,
|
||||
...references,
|
||||
]);
|
||||
}
|
||||
if (!element) {
|
||||
const tags: TagItem[] = this.tags
|
||||
.map(({ tag, notes }) => {
|
||||
const parentTag =
|
||||
tag.indexOf(TAG_SEPARATOR) > 0
|
||||
? tag.substring(0, tag.indexOf(TAG_SEPARATOR))
|
||||
: tag;
|
||||
const tagChildren = 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));
|
||||
|
||||
return new TagItem(parentTag, parentTag, notes);
|
||||
})
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
array.findIndex(tag => tag.title === value.title) === index
|
||||
);
|
||||
const referenceChildren: TagTreeItem[] = (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));
|
||||
|
||||
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
|
||||
}
|
||||
return Promise.resolve(
|
||||
[
|
||||
element && new TagSearch(element.tag),
|
||||
...tagChildren,
|
||||
...referenceChildren,
|
||||
].filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
|
||||
@@ -134,11 +140,12 @@ export class TagItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly tag: string,
|
||||
public readonly title: string,
|
||||
public readonly nResourcesInSubtree: number,
|
||||
public readonly notes: URI[]
|
||||
) {
|
||||
super(title, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.description = `${this.notes.length} reference${
|
||||
this.notes.length !== 1 ? 's' : ''
|
||||
this.description = `${nResourcesInSubtree} reference${
|
||||
nResourcesInSubtree !== 1 ? 's' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('Tag Completion', () => {
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a markdown heading #1182', async () => {
|
||||
const { uri } = await createFile('# primary heading 1');
|
||||
const { uri } = await createFile('# primary');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
@@ -107,4 +107,110 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
describe('has robust triggering #1189', () => {
|
||||
it('should provide multiple suggestions when typing #', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 1)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide multiple suggestions when typing # on line with match', async () => {
|
||||
const { uri } = await createFile('Here is #my-tag and #');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 21)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide multiple suggestions when typing # at EOL', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#
|
||||
more text
|
||||
`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 1)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `# `', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
# `);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `#{non-match}`', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#$`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `##`', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
##`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `# ` in a line that already matched', async () => {
|
||||
const { uri } = await createFile('here is #primary and now # ');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 29)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { HASHTAG_REGEX } from '../core/utils/hashtags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
|
||||
// this regex is different from HASHTAG_REGEX in that it does not look for a
|
||||
// #+character. It uses a negative look-ahead for `# `
|
||||
const TAG_REGEX =
|
||||
/(?<=^|\s)#(?![ \t#])([0-9]*[\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/dgu;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -34,12 +38,23 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(HASHTAG_REGEX);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(TAG_REGEX);
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check the match group length.
|
||||
// find the last match group, and ensure the end of that group is
|
||||
// at the cursor position.
|
||||
// This excludes both `#%` and also `here is #my-app1 and now # ` with
|
||||
// trailing space
|
||||
const matches = Array.from(cursorPrefix.matchAll(TAG_REGEX));
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
const lastMatchEndIndex = lastMatch[0].length + lastMatch.index;
|
||||
if (lastMatchEndIndex !== position.character) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completionTags = [];
|
||||
[...this.foamTags.tags].forEach(([tag]) => {
|
||||
const item = new vscode.CompletionItem(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Selection, ViewColumn, window } from 'vscode';
|
||||
import { Selection, Uri, ViewColumn, window, workspace } from 'vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { NoteFactory } from '../services/templates';
|
||||
import {
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { fileExists } from './editor';
|
||||
@@ -19,6 +20,56 @@ describe('Create note from template', () => {
|
||||
});
|
||||
|
||||
describe('User flow', () => {
|
||||
it('should resolve the path using the config when path is derived from note title', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve('Title of note')));
|
||||
|
||||
const noteA = await createFile('Note A', [
|
||||
'path',
|
||||
'of-new-note',
|
||||
'note-a.md',
|
||||
]);
|
||||
await showInEditor(noteA.uri);
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.newNotePath',
|
||||
'currentDir',
|
||||
async () => {
|
||||
const result = await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(result.uri.path).toEqual(
|
||||
noteA.uri.getDirectory().joinPath('Title of note.md').path
|
||||
);
|
||||
await deleteFile(result.uri);
|
||||
}
|
||||
);
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.newNotePath',
|
||||
'root',
|
||||
async () => {
|
||||
const result = await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(result.uri.path).toEqual(
|
||||
Uri.joinPath(workspace.workspaceFolders[0].uri, 'Title of note.md')
|
||||
.path
|
||||
);
|
||||
await deleteFile(result.uri);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(noteA);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should ask a user to confirm the path if note already exists', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
|
||||
@@ -351,13 +351,14 @@ export const NoteFactory = {
|
||||
|
||||
let newFilePath = template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: isSome(filepathFallbackURI)
|
||||
? filepathFallbackURI
|
||||
: await getPathFromTitle(resolver);
|
||||
: filepathFallbackURI;
|
||||
|
||||
if (!newFilePath.path.startsWith('./')) {
|
||||
if (isNone(newFilePath)) {
|
||||
newFilePath = await getPathFromTitle(resolver);
|
||||
} else if (!newFilePath.path.startsWith('./')) {
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath);
|
||||
}
|
||||
|
||||
return NoteFactory.createNote(
|
||||
newFilePath,
|
||||
template.text,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { createTestNote } from '../test/test-utils';
|
||||
import {
|
||||
DirectoryTreeItem,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
UriTreeItem,
|
||||
} from './grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from './tree-view-utils';
|
||||
|
||||
const testMatcher = new SubstringExcludeMatcher('path-exclude');
|
||||
|
||||
@@ -74,14 +74,14 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri),
|
||||
uri => new ResourceTreeItem(workspace.get(uri), workspace),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
|
||||
|
||||
const directory = new DirectoryTreeItem(
|
||||
'/path',
|
||||
[new UriTreeItem(matchingNote1.uri)],
|
||||
[new ResourceTreeItem(matchingNote1, workspace)],
|
||||
'note'
|
||||
);
|
||||
const result = await provider.getChildren(directory);
|
||||
@@ -90,7 +90,7 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
collapsibleState: 0,
|
||||
label: 'ABC',
|
||||
description: '/path/ABC.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri),
|
||||
uri => new ResourceTreeItem(workspace.get(uri), workspace),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Off);
|
||||
@@ -115,13 +115,13 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
collapsibleState: 0,
|
||||
label: matchingNote1.title,
|
||||
description: '/path/ABC.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: matchingNote2.title,
|
||||
description: '/path-bis/XYZ.md',
|
||||
command: { command: OPEN_COMMAND.command },
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { GroupedResoucesConfigGroupBy } from '../settings';
|
||||
import { getContainsTooltip, getNoteTooltip, isSome } from '../utils';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import { toVsCodeUri } from './vsc-utils';
|
||||
import { getContainsTooltip, isSome } from '../utils';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { IMatcher } from '../core/services/datastore';
|
||||
import { UriTreeItem } from './tree-view-utils';
|
||||
|
||||
/**
|
||||
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
|
||||
@@ -128,13 +125,14 @@ export class GroupedResourcesTreeDataProvider
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(
|
||||
directory?: DirectoryTreeItem
|
||||
async getChildren(
|
||||
item?: GroupedResourceTreeItem
|
||||
): Promise<GroupedResourceTreeItem[]> {
|
||||
if ((item as any)?.getChildren) {
|
||||
const children = await (item as any).getChildren();
|
||||
return children.sort(sortByTreeItemLabel);
|
||||
}
|
||||
if (this.groupBy === GroupedResoucesConfigGroupBy.Folder) {
|
||||
if (isSome(directory)) {
|
||||
return Promise.resolve(directory.children.sort(sortByTreeItemLabel));
|
||||
}
|
||||
const directories = Object.entries(this.getUrisByDirectory())
|
||||
.sort(([dir1], [dir2]) => sortByString(dir1, dir2))
|
||||
.map(
|
||||
@@ -186,63 +184,6 @@ type UrisByDirectory = { [key: string]: Array<URI> };
|
||||
|
||||
type GroupedResourceTreeItem = UriTreeItem | DirectoryTreeItem;
|
||||
|
||||
export class UriTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly uri: URI,
|
||||
options: {
|
||||
collapsibleState?: vscode.TreeItemCollapsibleState;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
} = {}
|
||||
) {
|
||||
super(options?.title ?? uri.getName(), options.collapsibleState);
|
||||
this.description = uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(uri))?.uri.path,
|
||||
''
|
||||
);
|
||||
this.tooltip = undefined;
|
||||
this.command = {
|
||||
command: OPEN_COMMAND.command,
|
||||
title: OPEN_COMMAND.title,
|
||||
arguments: [
|
||||
{
|
||||
uri: uri,
|
||||
},
|
||||
],
|
||||
};
|
||||
this.iconPath = new vscode.ThemeIcon(options.icon ?? 'new-file');
|
||||
}
|
||||
|
||||
resolveTreeItem(): Promise<GroupedResourceTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceTreeItem extends UriTreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
private readonly workspace: FoamWorkspace,
|
||||
collapsibleState = vscode.TreeItemCollapsibleState.None
|
||||
) {
|
||||
super(resource.uri, {
|
||||
title: resource.title,
|
||||
icon: 'note',
|
||||
collapsibleState,
|
||||
});
|
||||
this.contextValue = 'resource';
|
||||
}
|
||||
|
||||
async resolveTreeItem(): Promise<ResourceTreeItem> {
|
||||
if (this instanceof ResourceTreeItem) {
|
||||
const content = await this.workspace.readAsMarkdown(this.resource.uri);
|
||||
this.tooltip = isSome(content)
|
||||
? getNoteTooltip(content)
|
||||
: this.resource.title;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectoryTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly dir: string,
|
||||
@@ -264,6 +205,10 @@ export class DirectoryTreeItem extends vscode.TreeItem {
|
||||
this.tooltip = getContainsTooltip(titles);
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
getChildren(): Promise<GroupedResourceTreeItem[]> {
|
||||
return Promise.resolve(this.children);
|
||||
}
|
||||
}
|
||||
|
||||
const sortByTreeItemLabel = (a: vscode.TreeItem, b: vscode.TreeItem) =>
|
||||
|
||||
162
packages/foam-vscode/src/utils/tree-view-utils.ts
Normal file
162
packages/foam-vscode/src/utils/tree-view-utils.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { toVsCodeUri } from './vsc-utils';
|
||||
import { Range } from '../core/model/range';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getNoteTooltip } from '../utils';
|
||||
import { isSome } from '../core/utils';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
export class UriTreeItem extends vscode.TreeItem {
|
||||
private doGetChildren: () => Promise<vscode.TreeItem[]>;
|
||||
|
||||
constructor(
|
||||
public readonly uri: URI,
|
||||
options: {
|
||||
collapsibleState?: vscode.TreeItemCollapsibleState;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
getChildren?: () => Promise<vscode.TreeItem[]>;
|
||||
} = {}
|
||||
) {
|
||||
super(
|
||||
options?.title ?? uri.getName(),
|
||||
options.collapsibleState
|
||||
? options.collapsibleState
|
||||
: options.getChildren
|
||||
? vscode.TreeItemCollapsibleState.Collapsed
|
||||
: vscode.TreeItemCollapsibleState.None
|
||||
);
|
||||
this.doGetChildren = options.getChildren;
|
||||
this.description = uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(uri))?.uri.path,
|
||||
''
|
||||
);
|
||||
this.tooltip = undefined;
|
||||
this.iconPath = new vscode.ThemeIcon(options.icon ?? 'new-file');
|
||||
}
|
||||
|
||||
resolveTreeItem(): Promise<UriTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
getChildren(): Promise<vscode.TreeItem[]> {
|
||||
return isSome(this.doGetChildren)
|
||||
? this.doGetChildren()
|
||||
: Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceTreeItem extends UriTreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
private readonly workspace: FoamWorkspace,
|
||||
options: {
|
||||
collapsibleState?: vscode.TreeItemCollapsibleState;
|
||||
getChildren?: () => Promise<vscode.TreeItem[]>;
|
||||
} = {}
|
||||
) {
|
||||
super(resource.uri, {
|
||||
title: resource.title,
|
||||
icon: 'note',
|
||||
collapsibleState: options.collapsibleState,
|
||||
getChildren: options.getChildren,
|
||||
});
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [toVsCodeUri(resource.uri)],
|
||||
title: 'Go to location',
|
||||
};
|
||||
|
||||
this.contextValue = 'resource';
|
||||
}
|
||||
|
||||
async resolveTreeItem(): Promise<ResourceTreeItem> {
|
||||
if (this instanceof ResourceTreeItem) {
|
||||
const content = await this.workspace.readAsMarkdown(this.resource.uri);
|
||||
this.tooltip = isSome(content)
|
||||
? getNoteTooltip(content)
|
||||
: this.resource.title;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceRangeTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public label: string,
|
||||
public readonly resource: Resource,
|
||||
public readonly range: Range
|
||||
) {
|
||||
super(label, vscode.TreeItemCollapsibleState.None);
|
||||
this.label = `${range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [toVsCodeUri(resource.uri), { selection: range }],
|
||||
title: 'Go to location',
|
||||
};
|
||||
}
|
||||
|
||||
resolveTreeItem(): Promise<ResourceRangeTreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
static async createStandardItem(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
range: Range
|
||||
): Promise<ResourceRangeTreeItem> {
|
||||
const lines = ((await workspace.readAsMarkdown(resource.uri)) ?? '').split(
|
||||
'\n'
|
||||
);
|
||||
|
||||
const line = lines[range.start.line];
|
||||
const start = Math.max(0, range.start.character - 15);
|
||||
const ellipsis = start === 0 ? '' : '...';
|
||||
|
||||
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;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
export const groupRangesByResource = async (
|
||||
workspace: FoamWorkspace,
|
||||
items:
|
||||
| ResourceRangeTreeItem[]
|
||||
| Promise<ResourceRangeTreeItem[]>
|
||||
| Promise<ResourceRangeTreeItem>[],
|
||||
collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
|
||||
) => {
|
||||
let itemsArray = [] as ResourceRangeTreeItem[];
|
||||
if (items instanceof Promise) {
|
||||
itemsArray = await items;
|
||||
}
|
||||
if (items instanceof Array && items[0] instanceof Promise) {
|
||||
itemsArray = await Promise.all(items);
|
||||
}
|
||||
if (items instanceof Array && items[0] instanceof ResourceRangeTreeItem) {
|
||||
itemsArray = items as any;
|
||||
}
|
||||
const byResource = groupBy(itemsArray, item => item.resource.uri.path);
|
||||
const resourceItems = Object.values(byResource).map(items => {
|
||||
const resourceItem = new ResourceTreeItem(items[0].resource, workspace, {
|
||||
collapsibleState,
|
||||
getChildren: () => {
|
||||
return Promise.resolve(
|
||||
items.sort((a, b) => Range.isBefore(a.range, b.range))
|
||||
);
|
||||
},
|
||||
});
|
||||
resourceItem.description = `(${items.length}) ${resourceItem.description}`;
|
||||
return resourceItem;
|
||||
});
|
||||
resourceItems.sort((a, b) => Resource.sortByTitle(a.resource, b.resource));
|
||||
return resourceItems;
|
||||
};
|
||||
Reference in New Issue
Block a user