mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 15:08:01 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4eaf5c5ff | ||
|
|
b4830eaf30 | ||
|
|
0cda6aed50 | ||
|
|
89c9bb5a7f | ||
|
|
941e870a65 | ||
|
|
c6655c33ff | ||
|
|
c94fb18f8a | ||
|
|
cbd55bac74 | ||
|
|
83a90177b9 | ||
|
|
37aec28af6 | ||
|
|
447f7fc068 | ||
|
|
ad1243665a | ||
|
|
f07de73bc4 | ||
|
|
c431ccfb62 | ||
|
|
f31ef897cc | ||
|
|
7a5f45c0ce | ||
|
|
df32d9e708 | ||
|
|
b3d4691bfa | ||
|
|
f5260f7d3f | ||
|
|
9b4b7ec84d | ||
|
|
52b7f86a9f | ||
|
|
2db7060124 | ||
|
|
a4f04b3b6b | ||
|
|
b5a8a5d7c7 | ||
|
|
f5a29e431c |
@@ -1013,6 +1013,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jimgraham",
|
||||
"name": "Jim Graham",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/430293?v=4",
|
||||
"profile": "http://www.jim-graham.net/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
43
.github/workflows/update-docs.yml
vendored
Normal file
43
.github/workflows/update-docs.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- docs/user/**/*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: foambubble/foam-template
|
||||
path: foam-template
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: foam
|
||||
- name: Copy and fixup user docs files
|
||||
id: copy
|
||||
run: |
|
||||
rm -r foam-template/docs
|
||||
cp -r foam/docs/user foam-template/docs
|
||||
|
||||
# Strip autogenerated wikileaks references because
|
||||
# they are not an appropriate default user experience.
|
||||
(cd foam-template/docs; sed -i '/\[\/\/begin\]/,/\[\/\/end\]/d' $(find . -type f -name \*.md))
|
||||
|
||||
# Set the commit message format
|
||||
echo "message=Docs sync @ $(cd foam; git log --pretty='format:%h %s')" >> $GITHUB_OUTPUT
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.FOAM_DOCS_SYNC_TOKEN }}
|
||||
path: foam-template
|
||||
commit-message: ${{ steps.copy.outputs.message }}
|
||||
branch: bot/foam-docs-sync
|
||||
delete-branch: true
|
||||
title: Sync docs from foam
|
||||
body: Copy docs from main foam repo
|
||||
@@ -248,6 +248,7 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://yongliangliu.com"><img src="https://avatars.githubusercontent.com/u/41845017?v=4?s=60" width="60px;" alt="Liu YongLiang"/><br /><sub><b>Liu YongLiang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tlylt" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://scottakerman.com"><img src="https://avatars.githubusercontent.com/u/15224439?v=4?s=60" width="60px;" alt="Scott Akerman"/><br /><sub><b>Scott Akerman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skakerman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [Links/Graphs/BackLinks don't work. How do I enable them?](#linksgraphsbacklinks-dont-work-how-do-i-enable-them)
|
||||
- [I don't want Foam enabled for all my workspaces](#i-dont-want-foam-enabled-for-all-my-workspaces)
|
||||
- [I want to publish the graph view to GitHub pages or Vercel](#i-want-to-publish-the-graph-view-to-github-pages-or-vercel)
|
||||
|
||||
## Links/Graphs/BackLinks don't work. How do I enable them?
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Using Foam
|
||||
|
||||
Foam is a collection VS Code extensions and recipes that power up the editor into a full-blown note taking system.
|
||||
This folder contains user documentation describing how to get started using Foam, what its main features are, and strategies for getting the most out of Foam.
|
||||
The full docs are included in the `foam-template` repo that most users start from.
|
||||
Foam is a collection VS Code extensions and recipes that power up the editor
|
||||
into a full-blown note taking system. This folder contains user documentation
|
||||
describing how to get started using Foam, what its main features are, and
|
||||
strategies for getting the most out of Foam. The full docs are included in the
|
||||
`foam-template` repo that most users start from.
|
||||
|
||||
> See also [[frequently-asked-questions]].
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.21.1"
|
||||
"version": "0.22.1"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,45 @@ 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:
|
||||
|
||||
- Fixed issue with generated daily note template due to path escape (#1188, #1190)
|
||||
|
||||
## [0.21.3] - 2023-04-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed relative path from workspace root in templates (#1188)
|
||||
|
||||
## [0.21.2] - 2023-04-11
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed embed with relative paths (#1168, #1170)
|
||||
- Improved multi-root folder support for daily notes (#1126, #1175)
|
||||
- Improved use of tag completion (#1183 - thanks @jimgraham)
|
||||
- Fixed relative path use in note creation when using templates (#1170)
|
||||
|
||||
Internal:
|
||||
|
||||
- Sync user docs with foam-template docs (#1180 - thanks @infogulch)
|
||||
|
||||
## [0.21.1] - 2023-02-24
|
||||
|
||||
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.1",
|
||||
"version": "0.22.1",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { Range } from '../model/range';
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text text on which the textEdit will be applied
|
||||
* @param textEdit
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const eol = detectNewline(text) || os.EOL;
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
const endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
};
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import matter from 'gray-matter';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { TextEdit } from '../services/text-edit';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
|
||||
export const generateHeading = async (
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../services/markdown-provider';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { TextEdit } from './apply-text-edit';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '../services/text-edit';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ResourceLink {
|
||||
type: 'wikilink' | 'link';
|
||||
rawText: string;
|
||||
range: Range;
|
||||
isEmbed: boolean;
|
||||
}
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ describe('MarkdownLink', () => {
|
||||
type: 'link',
|
||||
rawText: '[link](#section)',
|
||||
range: Range.create(0, 0),
|
||||
isEmbed: false,
|
||||
};
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('');
|
||||
@@ -161,7 +162,7 @@ describe('MarkdownLink', () => {
|
||||
target: 'new-link',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
@@ -172,7 +173,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -184,7 +185,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'new-section',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[new-link#new-section]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -195,7 +196,7 @@ describe('MarkdownLink', () => {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to rename the alias', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
|
||||
@@ -204,7 +205,7 @@ describe('MarkdownLink', () => {
|
||||
alias: 'new-alias',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[[wikilink|new-alias]]`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +217,7 @@ describe('MarkdownLink', () => {
|
||||
target: 'to/another-path.md',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename the section only', () => {
|
||||
const link = parser.parse(
|
||||
@@ -227,7 +228,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should rename both target and section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -239,7 +240,7 @@ describe('MarkdownLink', () => {
|
||||
section: 'section2',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/another-path.md#section2)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
it('should be able to remove the section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -250,7 +251,7 @@ describe('MarkdownLink', () => {
|
||||
section: '',
|
||||
});
|
||||
expect(edit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(edit.selection).toEqual(link.range);
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,16 +46,17 @@ export abstract class MarkdownLink {
|
||||
const newAlias = delta.alias ?? alias ?? '';
|
||||
const sectionDivider = newSection ? '#' : '';
|
||||
const aliasDivider = newAlias ? '|' : '';
|
||||
const embed = link.isEmbed ? '!' : '';
|
||||
if (link.type === 'wikilink') {
|
||||
return {
|
||||
newText: `[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
selection: link.range,
|
||||
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
return {
|
||||
newText: `[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
selection: link.range,
|
||||
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('Markdown parsing', () => {
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect links that have formatting in label', () => {
|
||||
@@ -48,6 +49,15 @@ describe('Markdown parsing', () => {
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect embed links', () => {
|
||||
const note = createNoteFromMarkdown('this is ');
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.isEmbed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should detect wikilinks', () => {
|
||||
@@ -61,6 +71,16 @@ describe('Markdown parsing', () => {
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[a file]]');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect wikilink embeds', () => {
|
||||
const note = createNoteFromMarkdown('Some content and ![[an embed]]');
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('![[an embed]]');
|
||||
expect(link.isEmbed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should detect wikilinks that have aliases', () => {
|
||||
@@ -74,6 +94,7 @@ describe('Markdown parsing', () => {
|
||||
link = note.links[1];
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[other link | spaced]]');
|
||||
expect(link.isEmbed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should skip wikilinks in codeblocks', () => {
|
||||
|
||||
@@ -299,18 +299,33 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note, noteSource) => {
|
||||
if (node.type === 'wikiLink') {
|
||||
const isEmbed =
|
||||
noteSource.charAt(node.position!.start.offset - 1) === '!';
|
||||
|
||||
const literalContent = noteSource.substring(
|
||||
node.position!.start.offset!,
|
||||
isEmbed
|
||||
? node.position!.start.offset! - 1
|
||||
: node.position!.start.offset!,
|
||||
node.position!.end.offset!
|
||||
);
|
||||
|
||||
const range = isEmbed
|
||||
? Range.create(
|
||||
node.position.start.line - 1,
|
||||
node.position.start.column - 2,
|
||||
node.position.end.line - 1,
|
||||
node.position.end.column - 1
|
||||
)
|
||||
: astPositionToFoamRange(node.position!);
|
||||
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
rawText: literalContent,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
range,
|
||||
isEmbed,
|
||||
});
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
if (node.type === 'link' || node.type === 'image') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = note.uri.resolve(targetUri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
@@ -324,6 +339,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'link',
|
||||
rawText: literalContent,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
isEmbed: literalContent.startsWith('!'),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Range } from '../model/range';
|
||||
import { Logger } from '../utils/log';
|
||||
import { applyTextEdit } from './apply-text-edit';
|
||||
import { TextEdit } from './text-edit';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
4. this is fourth line`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('applyTextEdit', () => {
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
const actual = TextEdit.apply(text, textEdit);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
44
packages/foam-vscode/src/core/services/text-edit.ts
Normal file
44
packages/foam-vscode/src/core/services/text-edit.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { Range } from '../model/range';
|
||||
|
||||
export interface TextEdit {
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export abstract class TextEdit {
|
||||
/**
|
||||
*
|
||||
* @param text text on which the textEdit will be applied
|
||||
* @param textEdit
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
public static apply(text: string, textEdit: TextEdit): string {
|
||||
const eol = detectNewline.graceful(text);
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
const endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
}
|
||||
}
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i]?.length ?? 0);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX =
|
||||
export const HASHTAG_REGEX =
|
||||
/(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX =
|
||||
/(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
|
||||
@@ -86,9 +86,7 @@ export async function createDailyNoteIfNotExists(targetDate: Date) {
|
||||
|
||||
const templateFallbackText = `---
|
||||
foam_template:
|
||||
filepath: "${workspace.asRelativePath(
|
||||
toVsCodeUri(pathFromLegacyConfiguration)
|
||||
)}"
|
||||
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
|
||||
---
|
||||
# ${dateFormat(targetDate, titleFormat, false)}
|
||||
`;
|
||||
|
||||
@@ -20,8 +20,8 @@ import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { applyTextEdit } from '../../core/janitor/apply-text-edit';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -130,8 +130,8 @@ async function runJanitor(foam: Foam) {
|
||||
// Note: The ordering matters. Definitions need to be inserted
|
||||
// before heading, since inserting a heading changes line numbers below
|
||||
let text = noteText;
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
text = definitions ? TextEdit.apply(text, definitions) : text;
|
||||
text = heading ? TextEdit.apply(text, heading) : text;
|
||||
|
||||
return workspace.fs.writeFile(toVsCodeUri(note.uri), Buffer.from(text));
|
||||
});
|
||||
|
||||
@@ -79,14 +79,6 @@ async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
`${OPEN_COMMAND.command}: No resource matches given args`,
|
||||
JSON.stringify(args)
|
||||
);
|
||||
vscode.window.showInformationMessage(
|
||||
`${OPEN_COMMAND.command}: No resource matches given args`
|
||||
);
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
|
||||
@@ -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' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ const feature: FoamFeature = {
|
||||
markdownItFoamTags,
|
||||
markdownItWikilinkNavigation,
|
||||
markdownItRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, foam.workspace), md);
|
||||
].reduce(
|
||||
(acc, extension) =>
|
||||
extension(acc, foam.workspace, foam.services.parser),
|
||||
md
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Displaying included notes in preview', () => {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
@@ -51,7 +51,7 @@ describe('Displaying included notes in preview', () => {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-a]]`);
|
||||
expect(res).toContain('This is the root node');
|
||||
@@ -80,7 +80,7 @@ This is the third section of note E
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
@@ -123,7 +123,7 @@ This is the third section of note E
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(
|
||||
`This is the root node. ![[note-e-container#Section 3]]`
|
||||
@@ -139,7 +139,11 @@ This is the third section of note E
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), new FoamWorkspace());
|
||||
const md = markdownItWikilinkEmbed(
|
||||
MarkdownIt(),
|
||||
new FoamWorkspace(),
|
||||
parser
|
||||
);
|
||||
|
||||
expect(md.render(`This is the root node. ![[non-existing-note]]`)).toMatch(
|
||||
`<p>This is the root node. ![[non-existing-note]]</p>`
|
||||
@@ -158,12 +162,12 @@ This is the third section of note E
|
||||
const ws = new FoamWorkspace()
|
||||
.set(parser.parse(noteA.uri, noteA.content))
|
||||
.set(parser.parse(noteB.uri, noteB.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws);
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink: note-a');
|
||||
expect(res).toContain('Cyclic link detected for wikilink');
|
||||
|
||||
deleteFile(noteA);
|
||||
deleteFile(noteB);
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { readFileSync } from 'fs';
|
||||
import { workspace as vsWorkspace } from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isSome } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { Resource, ResourceParser } from '../../core/model/note';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { readFileSync } from 'fs';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Position } from '../../core/model/position';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
|
||||
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
|
||||
const refsStack: string[] = [];
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
workspace: FoamWorkspace,
|
||||
parser: ResourceParser
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
@@ -52,6 +58,11 @@ export const markdownItWikilinkEmbed = (
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(
|
||||
noteText,
|
||||
parser,
|
||||
workspace
|
||||
);
|
||||
content = getFoamVsCodeConfig(CONFIG_EMBED_NOTE_IN_CONTAINER)
|
||||
? `<div class="embed-container-note">${md.render(noteText)}</div>`
|
||||
: noteText;
|
||||
@@ -84,4 +95,32 @@ Embed for attachments is not supported
|
||||
});
|
||||
};
|
||||
|
||||
function withLinksRelativeToWorkspaceRoot(
|
||||
noteText: string,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) {
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(vsWorkspace.workspaceFolders[0].uri),
|
||||
noteText
|
||||
);
|
||||
const edits = note.links
|
||||
.map(link => {
|
||||
const info = MarkdownLink.analyzeLink(link);
|
||||
const resource = workspace.find(info.target);
|
||||
const pathFromRoot = vsWorkspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
);
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: pathFromRoot,
|
||||
});
|
||||
})
|
||||
.sort((a, b) => Position.compareTo(b.range.start, a.range.start));
|
||||
const text = edits.reduce(
|
||||
(text, edit) => TextEdit.apply(text, edit),
|
||||
noteText
|
||||
);
|
||||
return text;
|
||||
}
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
|
||||
@@ -23,6 +23,7 @@ export const markdownItWikilinkNavigation = (
|
||||
rawText: '[[' + wikilink + ']]',
|
||||
type: 'wikilink',
|
||||
range: Range.create(0, 0),
|
||||
isEmbed: false,
|
||||
});
|
||||
const formattedSection = section ? `#${section}` : '';
|
||||
const label = isEmpty(alias) ? `${target}${formattedSection}` : alias;
|
||||
|
||||
@@ -45,7 +45,7 @@ const feature: FoamFeature = {
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
@@ -62,7 +62,7 @@ const feature: FoamFeature = {
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.selection),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -93,4 +93,124 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a markdown heading #1182', async () => {
|
||||
const { uri } = await createFile('# primary');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 7)
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { SECTION_REGEX } from './link-completion';
|
||||
|
||||
export const TAG_REGEX = /#(.*)/;
|
||||
// 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 (
|
||||
@@ -36,13 +38,23 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete =
|
||||
cursorPrefix.match(TAG_REGEX) && !cursorPrefix.match(SECTION_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',
|
||||
|
||||
@@ -349,13 +349,15 @@ export const NoteFactory = {
|
||||
resolver
|
||||
);
|
||||
|
||||
const newFilePath = asAbsoluteWorkspaceUri(
|
||||
template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: isSome(filepathFallbackURI)
|
||||
? filepathFallbackURI
|
||||
: await getPathFromTitle(resolver)
|
||||
);
|
||||
let newFilePath = template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: filepathFallbackURI;
|
||||
|
||||
if (isNone(newFilePath)) {
|
||||
newFilePath = await getPathFromTitle(resolver);
|
||||
} else if (!newFilePath.path.startsWith('./')) {
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath);
|
||||
}
|
||||
|
||||
return NoteFactory.createNote(
|
||||
newFilePath,
|
||||
|
||||
@@ -89,11 +89,13 @@ export const createTestNote = (params: {
|
||||
type: 'wikilink',
|
||||
range: range,
|
||||
rawText: `[[${link.slug}]]`,
|
||||
isEmbed: false,
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
range: range,
|
||||
rawText: `[link text](${link.to})`,
|
||||
isEmbed: false,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
@@ -340,6 +340,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://yongliangliu.com"><img src="https://avatars.githubusercontent.com/u/41845017?v=4?s=60" width="60px;" alt="Liu YongLiang"/><br /><sub><b>Liu YongLiang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tlylt" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://scottakerman.com"><img src="https://avatars.githubusercontent.com/u/15224439?v=4?s=60" width="60px;" alt="Scott Akerman"/><br /><sub><b>Scott Akerman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skakerman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jim-graham.net/"><img src="https://avatars.githubusercontent.com/u/430293?v=4?s=60" width="60px;" alt="Jim Graham"/><br /><sub><b>Jim Graham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimgraham" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user