mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3876811fb6 | ||
|
|
d9299ee9d4 | ||
|
|
23e21a62f3 | ||
|
|
e7c8d5a4eb | ||
|
|
3ef1b69b2e | ||
|
|
5859b2a9c6 | ||
|
|
54086fdd7e | ||
|
|
a308dfd109 | ||
|
|
e327115673 | ||
|
|
c019767476 | ||
|
|
5e8b817a82 | ||
|
|
e0acc0ba8c | ||
|
|
df4efc5138 | ||
|
|
1e2b3b1bc3 | ||
|
|
eee364de50 | ||
|
|
3ed2bcd37b | ||
|
|
797aa9f29a | ||
|
|
30ab58485c | ||
|
|
ba98f1990c | ||
|
|
6c1d6868f7 | ||
|
|
e773e1ff68 | ||
|
|
86e2bb1ba0 | ||
|
|
fc2fb6a0ab | ||
|
|
956d0119be | ||
|
|
8ddb6a2d12 | ||
|
|
fb9447630f | ||
|
|
06a5988a52 | ||
|
|
fac2247382 | ||
|
|
b089b997bb | ||
|
|
a504054504 | ||
|
|
a00d18cfbb | ||
|
|
5ca7c3eb52 | ||
|
|
571b6a3528 | ||
|
|
b6c9eac86c | ||
|
|
557330413c | ||
|
|
20894a1166 | ||
|
|
d0b3d5ff11 | ||
|
|
34fb62bb0b | ||
|
|
f297139e0c | ||
|
|
09e13f77b0 | ||
|
|
56d8c4c7a0 | ||
|
|
626a323193 | ||
|
|
25d9b5e459 | ||
|
|
c2241f16de | ||
|
|
5dee7cb2c0 | ||
|
|
154ded382b | ||
|
|
5de69ff3c3 | ||
|
|
8aefcfd515 | ||
|
|
e0e08a2a0f | ||
|
|
93c5d2f80c | ||
|
|
1c294d84c5 |
@@ -1022,8 +1022,45 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hezhizhen",
|
||||
"name": "Zhizhen He",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4",
|
||||
"profile": "https://t.me/littlepoint",
|
||||
"contributions": [
|
||||
"tool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tcheneau",
|
||||
"name": "Tony Cheneau",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/952059?v=4",
|
||||
"profile": "https://amnesiak.org/me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nicholas-l",
|
||||
"name": "Nicholas Latham",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12977174?v=4",
|
||||
"profile": "https://github.com/nicholas-l",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thara",
|
||||
"name": "Tomochika Hara",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1532891?v=4",
|
||||
"profile": "https://thara.dev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitType": "docs"
|
||||
}
|
||||
|
||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -9,6 +9,16 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
typos-check:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Check spelling with custom config file
|
||||
uses: crate-ci/typos@v1.14.8
|
||||
with:
|
||||
config: ./typos.toml
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -34,12 +44,13 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Build and Test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-12, ubuntu-22.04, windows-2022]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
# strategy:
|
||||
# matrix:
|
||||
# os: [macos-12, ubuntu-22.04, windows-2022]
|
||||
# runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-22.04
|
||||
# env:
|
||||
# OS: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/update-docs.yml
vendored
2
.github/workflows/update-docs.yml
vendored
@@ -24,7 +24,9 @@ jobs:
|
||||
id: copy
|
||||
run: |
|
||||
rm -r foam-template/docs
|
||||
rm -r foam-template/assets
|
||||
cp -r foam/docs/user foam-template/docs
|
||||
cp -r foam/docs/assets foam-template/assets
|
||||
|
||||
# Strip autogenerated wikileaks references because
|
||||
# they are not an appropriate default user experience.
|
||||
|
||||
BIN
docs/assets/images/note-embed-type-demo.gif
Normal file
BIN
docs/assets/images/note-embed-type-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 568 KiB |
@@ -93,7 +93,7 @@ After you have made your changes to your copy of the project, it is time to try
|
||||
|
||||
1. Return to the project's [home repository page](https://github.com/foambubble/foam).
|
||||
2. Github should show you an button called "Compare & pull request" linking your forked repository to the community repository.
|
||||
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
|
||||
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://blog.robsewell.com/blog/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
|
||||
4. Add as many relevant details to the PR message to make it clear to the project maintainers and other members of the community what you have accomplished with your new changes. Link to any issues the changes are related to.
|
||||
5. Your PR will then need to be reviewed and accepted by the other members of the community. Any discussion about the changes will occur in your PR thread.
|
||||
6. Once reviewed and accept you can complete the merge request!
|
||||
|
||||
@@ -14,7 +14,7 @@ Currently it is not possible within Foam to include other notes into a note. Nex
|
||||
|
||||
Initial work and thought on including a note was ignited by issue [#652](https://github.com/foambubble/foam/issues/652). Requested by a user was a likewise functionality as offered in Obsidian. This was simply the ability to include a note.
|
||||
|
||||
Whilst researching digital gardening for my own setup, I came across an in-depth overview by [Maggie Appleton](https://maggieappleton.com/roam-garden). Showing examples of her personal Roam Research I see valuable possibilites to connect more information, if we would add additional functionalities to the possibility of including a note. This proposal displays these possible functionalities and markup.
|
||||
Whilst researching digital gardening for my own setup, I came across an in-depth overview by [Maggie Appleton](https://maggieappleton.com/roam-garden). Showing examples of her personal Roam Research I see valuable possibilities to connect more information, if we would add additional functionalities to the possibility of including a note. This proposal displays these possible functionalities and markup.
|
||||
|
||||
## New features
|
||||
|
||||
@@ -29,7 +29,7 @@ The minimal functionality is the ability to fully include a note. Markup used in
|
||||
|
||||
### Include a section of a note
|
||||
|
||||
It could be interesting to only include a section of a note instead of the entire note. In order to do so thse user should be able to use the following syntax:
|
||||
It could be interesting to only include a section of a note instead of the entire note. In order to do so the user should be able to use the following syntax:
|
||||
|
||||
`![[wikilink#section-b]]`
|
||||
|
||||
@@ -37,11 +37,11 @@ As a result it will include the section title + section content until the next s
|
||||
|
||||
### Include an attribute of a file (note property or frontmatter)
|
||||
|
||||
As a user I could be interested in collecting the value of any given proeprty for a note. For example, I might want to include the tags as defined in the frontmatter of note A. This should be possible via the syntax:
|
||||
As a user I could be interested in collecting the value of any given property for a note. For example, I might want to include the tags as defined in the frontmatter of note A. This should be possible via the syntax:
|
||||
|
||||
`![[wikilink:<property>]]`
|
||||
|
||||
The property value should be lookedup by foam defined properties, e.g. title, **or** any property defined in the frontmatter of a note.
|
||||
The property value should be looked up by foam defined properties, e.g. title, **or** any property defined in the frontmatter of a note.
|
||||
|
||||
So, the example of including the tags of a note should be:
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ The potential solution:
|
||||
enum LinkReferenceDefinitions {
|
||||
Off, // link reference definitions are not generated
|
||||
WithExtensions, // link reference definitions contain .md (or similar) file extensions
|
||||
WithoutExtensions // link reference definitions do not contain file extenions
|
||||
WithoutExtensions // link reference definitions do not contain file extensions
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
- select "tags" in top left
|
||||
- select the tag that was just released, click "edit" and copy release information from changelog
|
||||
- publish (no need to attach artifacts)
|
||||
8. Annouce on Discord
|
||||
8. Announce on Discord
|
||||
|
||||
Steps 1 to 6 should really be replaced by a GitHub action...
|
||||
Steps 1 to 6 should really be replaced by a GitHub action...
|
||||
|
||||
@@ -249,6 +249,12 @@ 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="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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=60" width="60px;" alt="Zhizhen He"/><br /><sub><b>Zhizhen He</b></sub></a><br /><a href="#tool-hezhizhen" title="Tools">🔧</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://amnesiak.org/me"><img src="https://avatars.githubusercontent.com/u/952059?v=4?s=60" width="60px;" alt="Tony Cheneau"/><br /><sub><b>Tony Cheneau</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tcheneau" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-l"><img src="https://avatars.githubusercontent.com/u/12977174?v=4?s=60" width="60px;" alt="Nicholas Latham"/><br /><sub><b>Nicholas Latham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nicholas-l" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Backlinking
|
||||
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the **Backlinks Explorer**
|
||||
When using [[wikilinks]], you can find all notes that link to a specific note in the **Connections Explorer**
|
||||
|
||||
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "backlinks" and run the **Explorer: Focus on Backlinks** view.
|
||||
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "connections" and run the **Explorer: Focus on Connections** view.
|
||||
- Keep this pane always visible to discover relationships between your thoughts
|
||||
- You can drag the backlinks pane to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
|
||||
- You can drag the connections panel to a different section in VS Code if you prefer. See: [[make-backlinks-more-prominent]]
|
||||
- You can filter the connections to see just backlinks, forward links, or all connections
|
||||
- Finding backlinks in published Foam workspaces via [[materialized-backlinks]] is on the [[roadmap]] but not yet implemented.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
29
docs/user/features/built-in-note-embedding-types.md
Normal file
29
docs/user/features/built-in-note-embedding-types.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Built-In Note Embedding Types
|
||||
|
||||
When embedding a note, there are a few ways to modify the scope of the content as well as its display style. The following are Foam keywords that are used to describe note embedding.
|
||||
|
||||
Note, this only applies to note embedding, not embedding of attachments or images.
|
||||
|
||||

|
||||
|
||||
## Scope
|
||||
|
||||
- `full` - the entire note in the case of `![[note]]` or the entire section in the case of `![[note#section1]]`
|
||||
- `content` - everything excluding the title of the section. So the entire note minus the title for `![[note]]`, or the entire section minus the section header for `![[note#section1]]`
|
||||
|
||||
## Style
|
||||
|
||||
- `card` - outlines the embedded note with a border
|
||||
- `inline` - adds the note continuously as if the text were part of the calling note
|
||||
|
||||
## Default Setting
|
||||
|
||||
Foam expresses note display type as `<scope>-<style>`.
|
||||
|
||||
By default, Foam configures note embedding to be `full-card`. That is, whenever the standard embedding syntax is used, `![[note]]`, the note will have `full` scope and `card` style display. This setting is stored under `foam.preview.embedNoteStyle` and can be modified.
|
||||
|
||||
## Explicit Modifiers
|
||||
|
||||
Prepend the wikilink with one of the scope or style keywords, or a combination of the two to explicitly modify a note embedding if you would like to override the default setting.
|
||||
|
||||
For example, given your `foam.embedNoteStyle` is set to `content-card`, embedding a note with standard syntax `![[note-a]]` would show a bordered note without its title. Say, for a specific `note-b` you would like to display the title. You can simply use one of the above keywords to override your default setting like so: `full![[note-b]]`. In this case, `full` overrides the default `content` scope and because a style is not specified, it falls back to the default style setting, `card`. If you would like it to be inline, override that as well: `full-inline![[note-b]]`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Foam has various commands that you can explore by calling the command palette and typing "Foam".
|
||||
|
||||
In particular, some commands can be very customizible and can help with custom workflows and use cases.
|
||||
In particular, some commands can be very customizable and can help with custom workflows and use cases.
|
||||
|
||||
## foam-vscode.create-note command
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ It is possible to customize the style of a node based on the `type` property in
|
||||
|
||||
There are a few default node types defined by Foam that are displayed in the graph:
|
||||
|
||||
- `note` defines the color for regular nodes whose documents have not overriden the `type` property.
|
||||
- `note` defines the color for regular nodes whose documents have not overridden the `type` property.
|
||||
- `placeholder` defines the color for links that don't match any existing note. This is a [[placeholder]] because no file with such name exists.
|
||||
- see [[wikilinks]] for more info <!--NOTE: this placeholder link should NOT have an associated file. This is to demonstrate the custom coloring-->
|
||||
- `tag` defines the color for nodes representing #tags, allowing tags to be used as graph nodes similar to backlinks.
|
||||
|
||||
@@ -4,17 +4,20 @@ In some situations it might be useful to include the content of another note in
|
||||
|
||||
## Including a note
|
||||
|
||||
Including a note can be done by adding an `!` before a wikilink defintion. For example `![[wikilink]]`.
|
||||
Including a note can be done by adding an `!` before a wikilink definition. For example `![[wikilink]]`.
|
||||
|
||||
## Custom styling
|
||||
|
||||
Displaying the inclusion of notes allows for some custom styling, see [[custom-markdown-preview-styles]]
|
||||
To modify how an embedded note looks and the scope of its content, see [[built-in-note-embedding-types]]
|
||||
|
||||
For more fine-grained custom styling, see [[custom-markdown-preview-styles]]
|
||||
|
||||
## Future possibilities
|
||||
|
||||
Work on this feature is evolving and progressing. See the [[inclusion-of-notes]] proposal for the current discussion.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md "Custom Markdown Preview Styles"
|
||||
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md "Inclusion of notes Proposal "
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[built-in-note-embedding-types]: built-in-note-embedding-types.md 'Built-In Note Embedding Types'
|
||||
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md 'Custom Markdown Preview Styles'
|
||||
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md 'Inclusion of notes Proposal '
|
||||
[//end]: # 'Autogenerated link references'
|
||||
|
||||
@@ -19,6 +19,7 @@ The following example:
|
||||
```
|
||||
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/user/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
|
||||
## Specification
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ keywords: hello world, bonjour
|
||||
---
|
||||
```
|
||||
|
||||
This sets the `type` of this document to `feature` and sets **three** keywords for the document: `hello`, `world`, and `bonjour`. The YAML parser will treat both spaces and commas as the seperators for these YAML properties. If you want to use multi-word values for these properties, you will need to combine the words with dashes or underscores (i.e. instead of `hello world`, use `hello_world` or `hello-world`).
|
||||
This sets the `type` of this document to `feature` and sets **three** keywords for the document: `hello`, `world`, and `bonjour`. The YAML parser will treat both spaces and commas as the separators for these YAML properties. If you want to use multi-word values for these properties, you will need to combine the words with dashes or underscores (i.e. instead of `hello world`, use `hello_world` or `hello-world`).
|
||||
|
||||
> You can set as many custom properties for a document as you like, but there are a few [special properties](#special-properties) defined by Foam.
|
||||
|
||||
@@ -27,11 +27,11 @@ This sets the `type` of this document to `feature` and sets **three** keywords f
|
||||
|
||||
Some properties have special meaning for Foam:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------- | ------------------- |
|
||||
| `title` | will assign the name to the note that you will see in the graph, regardless of the filename or the first heading (also see how to [[write-notes-in-foam]]) |
|
||||
| `type` | can be used to style notes differently in the graph (also see [[graph-visualization]]). The default type for a document is `note` unless otherwise specified with this property. |
|
||||
| `tags` | can be used to add tags to a note (see [[tags]]) |
|
||||
| Name | Description |
|
||||
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `title` | will assign the name to the note that you will see in the graph, regardless of the filename or the first heading (also see how to [[write-notes-in-foam]]) |
|
||||
| `type` | can be used to style notes differently in the graph (also see [[graph-visualization]]). The default type for a document is `note` unless otherwise specified with this property. |
|
||||
| `tags` | can be used to add tags to a note (see [[tags]]) |
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ In addition, you can also use variables provided by Foam:
|
||||
| -------------------- | ------------ |
|
||||
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| `FOAM_TITLE_SAFE` | The title of the note in a file system safe format. If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, `FOAM_DATE_WEEK` etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
|
||||
|
||||
@@ -193,7 +194,7 @@ You can add the template metadata to its own YAML Frontmatter block at the start
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
@@ -205,7 +206,7 @@ If the note already has a Frontmatter block, a Foam-specific Frontmatter block c
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
@@ -15,6 +15,10 @@ There are two ways of creating a tag:
|
||||
|
||||
Tags can also be hierarchical, so you can have `#parent/child` such as #my-tag3/info.
|
||||
|
||||
### Tag completion
|
||||
|
||||
Typing the `#` character will launch VS Code's "Intellisense." This provider will show a list of possible tags that match the character. If you are editing in the frontmatter [[note-properties|note property]], you can invoke tag completion on the `tags:` line by either typing the `#` character, or using the ["trigger suggest"](https://code.visualstudio.com/docs/editor/intellisense) keybinding (usually `ctrl+space`). If the `#` is used in the frontmatter, it will be removed when the tag is inserted.
|
||||
|
||||
## Using *Tag Explorer*
|
||||
|
||||
It's possible to navigate tags via the Tag Explorer panel. Expand the Tag Explorer view in the left side bar which will list all the tags found in current Foam environment. Then, each level of tags can be expanded until the options to search by tag and a list of all files containing a particular tag are shown.
|
||||
@@ -33,7 +37,7 @@ It is possible to customize the way that tags look in the Markdown Preview panel
|
||||
|
||||
> Note: the file path for the stylesheet will be relative to the currently open folder in the workspace when changing this setting for the current workspace. If changing this setting for the user, then the file path will be relative to your global [VSCode settings](https://code.visualstudio.com/docs/getstarted/settings).
|
||||
|
||||
The end result will be a CSS file that looks similiar to the content below. Now you can make your tags standout in your note previews.
|
||||
The end result will be a CSS file that looks similar to the content below. Now you can make your tags standout in your note previews.
|
||||
|
||||
```css
|
||||
.foam-tag{
|
||||
@@ -49,7 +53,10 @@ The end result will be a CSS file that looks similiar to the content below. Now
|
||||
Given the power of backlinks, some people prefer to use them as tags.
|
||||
For example you can tag your notes about books with [[book]].
|
||||
|
||||
[note-properties|note property]: note-properties.md "Note Properties"
|
||||
[graph-visualization]: graph-visualization.md "Graph Visualization"
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[note-properties|note property]: note-properties.md "Note Properties"
|
||||
[graph-visualization]: graph-visualization.md "Graph Visualization"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -14,7 +14,7 @@ strategies for getting the most out of Foam. The full docs are included in the
|
||||
- [[recommended-extensions]]
|
||||
- [[creating-new-notes]]
|
||||
- [[write-notes-in-foam]]
|
||||
- [[sync-notes-with-soruce-control]]
|
||||
- [[sync-notes-with-source-control]]
|
||||
- [[keyboard-shortcuts]]
|
||||
|
||||
## Features
|
||||
@@ -57,7 +57,7 @@ See [[publishing]] for more details.
|
||||
[recommended-extensions]: getting-started/recommended-extensions.md "Recommended Extensions"
|
||||
[creating-new-notes]: getting-started/creating-new-notes.md "Creating New Notes"
|
||||
[write-notes-in-foam]: getting-started/write-notes-in-foam.md "Writing Notes"
|
||||
[sync-notes-with-soruce-control]: getting-started/sync-notes-with-soruce-control.md "Sync notes with source control"
|
||||
[sync-notes-with-source-control]: getting-started/sync-notes-with-source-control.md "Sync notes with source control"
|
||||
[keyboard-shortcuts]: getting-started/keyboard-shortcuts.md "Keyboard Shortcuts"
|
||||
[wikilinks]: features/wikilinks.md "Wikilinks"
|
||||
[tags]: features/tags.md "Tags"
|
||||
|
||||
@@ -50,7 +50,7 @@ In our case, we'll be using the latter tag to wrap our {% raw %}`{{ content }}`{
|
||||
|
||||
You may have noticed that we only made modifications to the template `_layouts/page.html`, which means that `_layouts/home.html` won't have KaTeX support. If you wan't to render math in Foam's home page, you'll need to make the same modifications to `_layouts/home.html` as well.
|
||||
|
||||
Finally, if all goes well, then our site hosted on Vercel will support rendering math equations with KaTeX after commiting these changes to GitHub. Here's a demo of the default template with KaTeX support: [Foam Template with KaTeX support](https://foam-template.vercel.app/).
|
||||
Finally, if all goes well, then our site hosted on Vercel will support rendering math equations with KaTeX after committing these changes to GitHub. Here's a demo of the default template with KaTeX support: [Foam Template with KaTeX support](https://foam-template.vercel.app/).
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[math-support-with-mathjax]: math-support-with-mathjax.md "Math Support"
|
||||
|
||||
@@ -65,7 +65,7 @@ gem "jekyll-katex" # Optional, the package that enables KaTeX math rendering
|
||||
|
||||
Besides adding the plugin `jekyll-katex` in `_config.yml` and `Gemfile`, we'll also have to follow the guides in [[math-support-with-katex]] to let our site fully support using KaTeX to render math equations.
|
||||
|
||||
### Commiting changes to GitHub repo
|
||||
### Committing changes to GitHub repo
|
||||
|
||||
Finally, commit the newly created files to GitHub.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ With this #recipe you can create notes on your iOS device, which will automatica
|
||||
|
||||
* You use [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) to manage your notes
|
||||
* You wish to adopt a practice such as [A writing inbox for transient and incomplete notes](https://notes.andymatuschak.org/A%20writing%20inbox%20for%20transient%20and%20incomplete%20notes)
|
||||
* You wish to use [Shorcuts](https://support.apple.com/guide/shortcuts/welcome/ios) to capture quick notes into your Foam notes from your iOS device
|
||||
* You wish to use [Shortcuts](https://support.apple.com/guide/shortcuts/welcome/ios) to capture quick notes into your Foam notes from your iOS device
|
||||
|
||||
## Other tools
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ If such an app was worth building, it would have to have the following features:
|
||||
|
||||
- Instant loading and syncing for quick notes
|
||||
- Sleek, simple, beautifully designed user experience.
|
||||
- Ability to search and navigate forward links and back links (onlly in paid GitJournal version)
|
||||
- Ability to search and navigate forward links and back links (only in paid GitJournal version)
|
||||
- Killer feature that makes it the best note taking tool for Foam (?)
|
||||
|
||||
Given the effort vs reward ratio, it's a low priority for core team, but if someone wants to work on this, we can provide support! Talk to us on the #mobile-apps channel on [Foam Discord](https://foambubble.github.io/join-discord/w).
|
||||
|
||||
@@ -44,7 +44,7 @@ When editing a file, you can easily navigate `[[links]]` by hovering over them t
|
||||
|
||||
You can view a page's backlinks using either of the following techniques:
|
||||
|
||||
1. Expanding the file's node in the `Repositories` tree, since it's child nodes will represent backlinks. This makes it easy to browse your pages and their backlinks in a single hierachical view.
|
||||
1. Expanding the file's node in the `Repositories` tree, since it's child nodes will represent backlinks. This makes it easy to browse your pages and their backlinks in a single hierarchical view.
|
||||
|
||||
1. Opening a file, and then viewing it's backlinks list at the bottom of the editor view. This makes it easy to read a page and then see its backlinks in a contextually rich way.
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.22.2"
|
||||
"version": "0.25.4"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "yarn workspace foam-vscode test",
|
||||
"test": "yarn workspace foam-vscode test --stream",
|
||||
"lint": "lerna run lint",
|
||||
"watch": "lerna run watch --concurrency 20 --stream"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,76 @@ 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.25.4] - 2023-09-19
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for linking sections within same document (#1289)
|
||||
- Fixed note embedding bug (#1286 - thanks @badsketch)
|
||||
|
||||
## [0.25.3] - 2023-09-07
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed incorrect handling of embedding of non-existing notes (#1283 - thanks @badsketch)
|
||||
- Introduced Note Embedding Sytanx (#1281 - thanks @badsketch)
|
||||
- Attachments are not considered when computing orphan notes (#1242)
|
||||
|
||||
## [0.25.2] - 2023-09-02
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added content-only embed styles (#1279 - thanks @badsketch)
|
||||
- Added expand-all button to tree views (#1276)
|
||||
|
||||
## [0.25.1] - 2023-08-23
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for path parameter in filter (#1250)
|
||||
- Added grouping and filtering to tag explorer (#1275)
|
||||
- Added new setting to control note embedding (#1273 - thanks @badsketch)
|
||||
- Added last week's days to snippets (#1248 - thanks @jimgraham)
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated jest to v29 (#1271 - thanks @nicholas-l)
|
||||
- Improved test cleanup and management (#1274)
|
||||
|
||||
## [0.25.0] - 2023-06-30
|
||||
|
||||
Features:
|
||||
|
||||
- Support for multiple extensions and custom default extension (#1235)
|
||||
- Added `FOAM_TITLE_SAFE` template variable (#1232)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Connections panel tweaks (#1233)
|
||||
|
||||
## [0.24.0] - 2023-05-19
|
||||
|
||||
Features:
|
||||
|
||||
- Converted backlinks panel into more general connections panel (#1230)
|
||||
|
||||
Internal:
|
||||
|
||||
- Improved janitor code (#1228)
|
||||
- Refactored code related to tree view panels (#1226)
|
||||
- Lint and cleanup (#1224)
|
||||
|
||||
## [0.23.0] - 2023-05-06
|
||||
|
||||
Features:
|
||||
|
||||
- Added notes explorer (#1223)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Enabled tag completion in front matter (#1191 - thanks @jimgraham)
|
||||
- Various improvements to tree views (#1220)
|
||||
|
||||
## [0.22.2] - 2023-04-20
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.22.2",
|
||||
"version": "0.25.4",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -56,28 +56,34 @@
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
"id": "foam-vscode.backlinks",
|
||||
"name": "Backlinks",
|
||||
"id": "foam-vscode.connections",
|
||||
"name": "Connections",
|
||||
"icon": "$(references)",
|
||||
"contextualTitle": "Backlinks"
|
||||
"contextualTitle": "Foam"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.tags-explorer",
|
||||
"name": "Tag Explorer",
|
||||
"icon": "$(tag)",
|
||||
"contextualTitle": "Tags Explorer"
|
||||
"contextualTitle": "Foam"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.notes-explorer",
|
||||
"name": "Notes",
|
||||
"icon": "$(notebook)",
|
||||
"contextualTitle": "Foam"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.orphans",
|
||||
"name": "Orphans",
|
||||
"icon": "$(debug-gripper)",
|
||||
"contextualTitle": "Orphans"
|
||||
"contextualTitle": "Foam"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.placeholders",
|
||||
"name": "Placeholders",
|
||||
"icon": "$(debug-disconnect)",
|
||||
"contextualTitle": "Placeholders"
|
||||
"contextualTitle": "Foam"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -87,8 +93,8 @@
|
||||
"contents": "No tags found. Notes that contain tags will show up here. You may add tags to a note with a hashtag (#tag) or by adding a tag list to the front matter (tags: tag1, tag2)."
|
||||
},
|
||||
{
|
||||
"view": "foam-vscode.backlinks",
|
||||
"contents": "No backlinks found for selected resource."
|
||||
"view": "foam-vscode.connections",
|
||||
"contents": "Nothing found for the selected resource and the current filter."
|
||||
},
|
||||
{
|
||||
"view": "foam-vscode.orphans",
|
||||
@@ -101,6 +107,21 @@
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:backlinks",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'all links'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'backlinks'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"when": "view == foam-vscode.connections && foam-vscode.views.connections.show == 'forward links'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:folder",
|
||||
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'off'",
|
||||
@@ -111,6 +132,31 @@
|
||||
"when": "view == foam-vscode.orphans && foam-vscode.views.orphans.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'all'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.show == 'for-current-file'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'off'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"when": "view == foam-vscode.tags-explorer",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'all'",
|
||||
@@ -130,6 +176,26 @@
|
||||
"command": "foam-vscode.views.placeholders.group-by:off",
|
||||
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'folder'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"when": "view == foam-vscode.placeholders",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'all'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'notes-only'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"when": "view == foam-vscode.notes-explorer",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
@@ -141,6 +207,18 @@
|
||||
"command": "foam-vscode.update-graph",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:backlinks",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:folder",
|
||||
"when": "false"
|
||||
@@ -149,6 +227,26 @@
|
||||
"command": "foam-vscode.views.orphans.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"when": "false"
|
||||
@@ -165,6 +263,22 @@
|
||||
"command": "foam-vscode.views.placeholders.group-by:off",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"when": "false"
|
||||
@@ -197,8 +311,8 @@
|
||||
"title": "Foam: Show graph"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.update-wikilinks",
|
||||
"title": "Foam: Update Markdown Reference List"
|
||||
"command": "foam-vscode.update-wikilink-definitions",
|
||||
"title": "Foam: Update wikilink definitions"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
@@ -237,11 +351,51 @@
|
||||
"title": "Group By Folder",
|
||||
"icon": "$(list-tree)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:backlinks",
|
||||
"title": "Show Backlinks",
|
||||
"icon": "$(arrow-left)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:forward-links",
|
||||
"title": "Show Links",
|
||||
"icon": "$(arrow-right)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.connections.show:all-links",
|
||||
"title": "Show All",
|
||||
"icon": "$(arrow-swap)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:off",
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:for-current-file",
|
||||
"title": "Show tags in current file",
|
||||
"icon": "$(file)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.show:all",
|
||||
"title": "Show tags in workspace",
|
||||
"icon": "$(files)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:folder",
|
||||
"title": "Group By Folder",
|
||||
"icon": "$(list-tree)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.group-by:off",
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"title": "Show placeholders in current file",
|
||||
@@ -262,6 +416,26 @@
|
||||
"title": "Flat list",
|
||||
"icon": "$(list-flat)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:all",
|
||||
"title": "Show all resources",
|
||||
"icon": "$(files)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.expand-all",
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.notes-explorer.show:notes",
|
||||
"title": "Show only notes",
|
||||
"icon": "$(file)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-new-template",
|
||||
"title": "Foam: Create New Template"
|
||||
@@ -319,6 +493,16 @@
|
||||
"default": "pdf mp3 webm wav m4a mp4 avi mov rtf txt doc docx pages xls xlsx numbers ppt pptm pptx",
|
||||
"description": "Space separated list of file extensions that will be considered attachments"
|
||||
},
|
||||
"foam.files.notesExtensions": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Space separated list of extra file extensions that will be considered text notes (e.g. 'mdx txt markdown')"
|
||||
},
|
||||
"foam.files.defaultNoteExtension": {
|
||||
"type": "string",
|
||||
"default": "md",
|
||||
"description": "The default extension for new notes"
|
||||
},
|
||||
"foam.files.newNotePath": {
|
||||
"type": "string",
|
||||
"default": "root",
|
||||
@@ -425,10 +609,21 @@
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.preview.embedNoteInContainer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Wrap embedded notes in a container when displayed in preview panel"
|
||||
"foam.preview.embedNoteType": {
|
||||
"type": "string",
|
||||
"default": "full-card",
|
||||
"enum": [
|
||||
"full-inline",
|
||||
"full-card",
|
||||
"content-inline",
|
||||
"content-card"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Include the section with title and style inline",
|
||||
"Include the section with title and style it within a container",
|
||||
"Include the section without title and style inline",
|
||||
"Include the section without title and style it within a container"
|
||||
]
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
@@ -475,7 +670,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
@@ -492,12 +687,12 @@
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^27.5.1",
|
||||
"jest": "^29.6.2",
|
||||
"jest-extended": "^3.2.3",
|
||||
"markdown-it": "^12.0.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vscode-test": "^1.3.0",
|
||||
@@ -517,7 +712,7 @@
|
||||
"title-case": "^3.0.2",
|
||||
"unified": "^9.0.0",
|
||||
"unist-util-visit": "^2.0.2",
|
||||
"yaml": "^1.10.0"
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"__metadata": {
|
||||
"id": "b85c6625-454b-4b61-8a22-c42f3d0f2e1e",
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('SnippetParser', () => {
|
||||
assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');
|
||||
});
|
||||
|
||||
test('Parser, choise marker', () => {
|
||||
test('Parser, choice marker', () => {
|
||||
const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');
|
||||
|
||||
assert.strictEqual(placeholders.length, 1);
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('generateLinkReferences', () => {
|
||||
newText: textForNote(
|
||||
`
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[Note being refered as angel]: <Note being refered as angel> "Note being refered as angel"
|
||||
[Note being referred as angel]: <Note being referred as angel> "Note being referred as angel"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: Range.create(3, 0, 3, 0),
|
||||
@@ -183,13 +183,11 @@ describe('generateLinkReferences', () => {
|
||||
const note = findBySlug('file-with-explicit-and-implicit-link-references');
|
||||
const expected = {
|
||||
newText: textForNote(
|
||||
`[^footerlink]: https://foambubble.github.io/
|
||||
[linkrefenrece]: https://foambubble.github.io/
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
`[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: Range.create(5, 0, 10, 42),
|
||||
range: Range.create(8, 0, 10, 42),
|
||||
};
|
||||
|
||||
const noteText = await _workspace.readAsMarkdown(note.uri);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Resource } from '../model/note';
|
||||
import { NoteLinkDefinition, Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../services/markdown-provider';
|
||||
import { createMarkdownReferences } from '../services/markdown-provider';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '../services/text-edit';
|
||||
import { Position } from '../model/position';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
@@ -22,112 +19,62 @@ export const generateLinkReferences = async (
|
||||
return null;
|
||||
}
|
||||
|
||||
const markdownReferences = createMarkdownReferences(
|
||||
const newWikilinkDefinitions = createMarkdownReferences(
|
||||
workspace,
|
||||
note.uri,
|
||||
note,
|
||||
includeExtensions
|
||||
);
|
||||
|
||||
const beginDelimiterDef = note.definitions.find(
|
||||
({ label }) => label === '//begin'
|
||||
);
|
||||
const endDelimiterDef = note.definitions.find(
|
||||
({ label }) => label === '//end'
|
||||
);
|
||||
|
||||
const lines = text.split(eol);
|
||||
|
||||
const targetRange =
|
||||
beginDelimiterDef && endDelimiterDef
|
||||
? Range.createFromPosition(
|
||||
beginDelimiterDef.range.start,
|
||||
endDelimiterDef.range.end
|
||||
)
|
||||
: Range.create(
|
||||
lines.length - 1,
|
||||
lines[lines.length - 1].length,
|
||||
lines.length - 1,
|
||||
lines[lines.length - 1].length
|
||||
);
|
||||
|
||||
const newReferences =
|
||||
markdownReferences.length === 0
|
||||
newWikilinkDefinitions.length === 0
|
||||
? ''
|
||||
: [
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
...markdownReferences.map(stringifyMarkdownLinkReferenceDefinition),
|
||||
...newWikilinkDefinitions.map(NoteLinkDefinition.format),
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
].join(eol);
|
||||
|
||||
if (note.definitions.length === 0) {
|
||||
if (newReferences.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// check if the new references match the existing references
|
||||
const existingReferences = lines
|
||||
.slice(targetRange.start.line, targetRange.end.line + 1)
|
||||
.join(eol);
|
||||
|
||||
const lines = text.split(eol);
|
||||
const end = Position.create(
|
||||
lines.length - 1,
|
||||
lines[lines.length - 1].length
|
||||
);
|
||||
const padding = end.character === 0 ? eol : `${eol}${eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: Range.createFromPosition(end, end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
const last = note.definitions[note.definitions.length - 1];
|
||||
// adjust padding based on whether there are existing definitions
|
||||
// and, if not, whether we are on an empty line at the end of the file
|
||||
const padding =
|
||||
newWikilinkDefinitions.length === 0 || // no definitions
|
||||
!Position.isEqual(targetRange.start, targetRange.end) // replace existing definitions
|
||||
? ''
|
||||
: targetRange.start.character > 0 // not an empty line
|
||||
? `${eol}${eol}`
|
||||
: eol;
|
||||
|
||||
let nonGeneratedReferenceDefinitions = note.definitions;
|
||||
|
||||
// if we have more definitions then referenced pages AND the page refers to a page
|
||||
// we expect non-generated link definitions to be present
|
||||
// Collect all non-generated definitions, by removing the generated ones
|
||||
if (
|
||||
note.definitions.length > markdownReferences.length &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
// remove all autogenerated definitions
|
||||
const beginIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//begin'
|
||||
);
|
||||
const endIndex = note.definitions.findIndex(
|
||||
({ label }) => label === '//end'
|
||||
);
|
||||
|
||||
const generatedDefinitions = [...note.definitions].splice(
|
||||
beginIndex,
|
||||
endIndex - beginIndex + 1
|
||||
);
|
||||
|
||||
nonGeneratedReferenceDefinitions = note.definitions.filter(
|
||||
x => !generatedDefinitions.includes(x)
|
||||
);
|
||||
}
|
||||
|
||||
// When we only have explicitly defined link definitions &&
|
||||
// no indication of previously defined generated links &&
|
||||
// there is no reference to another page, return null
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
note.definitions.findIndex(({ label }) => label === '//begin') < 0 &&
|
||||
markdownReferences.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format link definitions for non-generated links
|
||||
const nonGeneratedReferences = nonGeneratedReferenceDefinitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(eol);
|
||||
|
||||
const oldReferences = note.definitions
|
||||
.map(stringifyMarkdownLinkReferenceDefinition)
|
||||
.join(eol);
|
||||
|
||||
// When the newly formatted references match the old ones, OR
|
||||
// when non-generated references are present, but no new ones are generated
|
||||
// return null
|
||||
if (
|
||||
oldReferences === newReferences ||
|
||||
(nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
newReferences === '' &&
|
||||
markdownReferences.length > 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fullReferences = `${newReferences}`;
|
||||
// If there are any non-generated definitions, add those to the output as well
|
||||
if (
|
||||
nonGeneratedReferenceDefinitions.length > 0 &&
|
||||
markdownReferences.length > 0
|
||||
) {
|
||||
fullReferences = `${nonGeneratedReferences}${eol}${newReferences}`;
|
||||
}
|
||||
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${fullReferences}`,
|
||||
range: Range.createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
return existingReferences === newReferences
|
||||
? null
|
||||
: {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: targetRange,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -25,13 +25,15 @@ export const bootstrap = async (
|
||||
watcher: IWatcher | undefined,
|
||||
dataStore: IDataStore,
|
||||
parser: ResourceParser,
|
||||
initialProviders: ResourceProvider[]
|
||||
initialProviders: ResourceProvider[],
|
||||
defaultExtension: string = '.md'
|
||||
) => {
|
||||
const tsStart = Date.now();
|
||||
|
||||
const workspace = await FoamWorkspace.fromProviders(
|
||||
initialProviders,
|
||||
dataStore
|
||||
dataStore,
|
||||
defaultExtension
|
||||
);
|
||||
|
||||
const tsWsDone = Date.now();
|
||||
|
||||
@@ -15,6 +15,19 @@ export interface NoteLinkDefinition {
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export abstract class NoteLinkDefinition {
|
||||
static format(definition: NoteLinkDefinition) {
|
||||
const url =
|
||||
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
|
||||
let text = `[${definition.label}]: ${url}`;
|
||||
if (definition.title) {
|
||||
text = `${text} "${definition.title}"`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
label: string;
|
||||
range: Range;
|
||||
@@ -53,6 +66,10 @@ export abstract class Resource {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
public static sortByPath(a: Resource, b: Resource) {
|
||||
return a.uri.path.localeCompare(b.uri.path);
|
||||
}
|
||||
|
||||
public static isResource(thing: any): thing is Resource {
|
||||
if (!thing) {
|
||||
return false;
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri)).toEqual('to/page-a');
|
||||
expect(ws.getIdentifier(second.uri)).toEqual('way/for/page-a');
|
||||
@@ -124,7 +124,7 @@ describe('Identifier computation', () => {
|
||||
const third = createTestNote({
|
||||
uri: '/another/path/for/page-a.md',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(first).set(second).set(third);
|
||||
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
@@ -170,7 +170,7 @@ describe('Identifier computation', () => {
|
||||
});
|
||||
|
||||
it('should ignore elements from the exclude list', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = new FoamWorkspace('.md');
|
||||
const noteA = createTestNote({ uri: '/path/to/note-a.md' });
|
||||
const noteB = createTestNote({ uri: '/path/to/note-b.md' });
|
||||
const noteC = createTestNote({ uri: '/path/to/note-c.md' });
|
||||
|
||||
@@ -22,6 +22,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
*/
|
||||
private _resources: Map<string, Resource> = new Map();
|
||||
|
||||
/**
|
||||
* @param defaultExtension: The default extension for notes in this workspace (e.g. `.md`)
|
||||
*/
|
||||
constructor(public defaultExtension: string = '.md') {}
|
||||
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
}
|
||||
@@ -67,14 +72,16 @@ export class FoamWorkspace implements IDisposable {
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
const needle = normalize('/' + identifier);
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
|
||||
const resources = [];
|
||||
getExtension(needle) !== this.defaultExtension
|
||||
? needle + this.defaultExtension
|
||||
: undefined;
|
||||
const resources: Resource[] = [];
|
||||
for (const key of this._resources.keys()) {
|
||||
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
|
||||
if (key.endsWith(mdNeedle) || key.endsWith(needle)) {
|
||||
resources.push(this._resources.get(normalize(key)));
|
||||
}
|
||||
}
|
||||
return resources.sort((a, b) => a.uri.path.localeCompare(b.uri.path));
|
||||
return resources.sort(Resource.sortByPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +112,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
forResource.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
identifier = changeExtension(identifier, '.md', '');
|
||||
identifier = changeExtension(identifier, this.defaultExtension, '');
|
||||
if (forResource.fragment) {
|
||||
identifier += `#${forResource.fragment}`;
|
||||
}
|
||||
@@ -121,7 +128,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
if (FoamWorkspace.isIdentifier(path)) {
|
||||
resource = this.listByIdentifier(path)[0];
|
||||
} else {
|
||||
const candidates = [path, path + '.md'];
|
||||
const candidates = [path, path + this.defaultExtension];
|
||||
for (const candidate of candidates) {
|
||||
const searchKey = isAbsolute(candidate)
|
||||
? candidate
|
||||
@@ -141,7 +148,6 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
// TODO add tests
|
||||
for (const provider of this.providers) {
|
||||
if (provider.supports(resource.uri)) {
|
||||
return provider.resolveLink(this, resource, link);
|
||||
@@ -237,9 +243,10 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
static async fromProviders(
|
||||
providers: ResourceProvider[],
|
||||
dataStore: IDataStore
|
||||
dataStore: IDataStore,
|
||||
defaultExtension: string = '.md'
|
||||
): Promise<FoamWorkspace> {
|
||||
const workspace = new FoamWorkspace();
|
||||
const workspace = new FoamWorkspace(defaultExtension);
|
||||
await Promise.all(providers.map(p => workspace.registerProvider(p)));
|
||||
const files = await dataStore.list();
|
||||
await Promise.all(files.map(f => workspace.fetchAndSet(f)));
|
||||
|
||||
@@ -3,17 +3,15 @@ import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
|
||||
const attachmentExtConfig = getFoamVsCodeConfig(
|
||||
'files.attachmentExtensions',
|
||||
''
|
||||
)
|
||||
.split(' ')
|
||||
.map(ext => '.' + ext.trim());
|
||||
|
||||
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
|
||||
const attachmentExtensions = [...attachmentExtConfig, ...imageExtensions];
|
||||
export const imageExtensions = [
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.svg',
|
||||
'.webp',
|
||||
];
|
||||
|
||||
const asResource = (uri: URI): Resource => {
|
||||
const type = imageExtensions.includes(uri.getExtension())
|
||||
@@ -34,9 +32,14 @@ const asResource = (uri: URI): Resource => {
|
||||
|
||||
export class AttachmentResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
public readonly attachmentExtensions: string[];
|
||||
|
||||
constructor(attachmentExtensions: string[] = []) {
|
||||
this.attachmentExtensions = [...imageExtensions, ...attachmentExtensions];
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return attachmentExtensions.includes(
|
||||
return this.attachmentExtensions.includes(
|
||||
uri.getExtension().toLocaleLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class GenericDataStore implements IDataStore {
|
||||
/**
|
||||
* A matcher that instead of using globs uses a list of files to
|
||||
* check the matches.
|
||||
* The {@link refresh} function has been added to the interface to accomodate
|
||||
* The {@link refresh} function has been added to the interface to accommodate
|
||||
* this matcher, far from ideal but to be refactored later
|
||||
*/
|
||||
export class FileListBasedMatcher implements IMatcher {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { Range } from '../model/range';
|
||||
import { getRandomURI } from '../../test/test-utils';
|
||||
import { Position } from '../model/position';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -464,7 +465,7 @@ But with some content.
|
||||
});
|
||||
});
|
||||
|
||||
describe('Block detection', () => {
|
||||
describe('Block detection for lists', () => {
|
||||
const md = `
|
||||
- this is block 1
|
||||
- this is [[block]] 2
|
||||
@@ -508,3 +509,76 @@ this is another simple line
|
||||
- this is block 2.1`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('block detection for sections', () => {
|
||||
const markdown = `
|
||||
# Section 1
|
||||
- this is block 1
|
||||
- this is [[block]] 2
|
||||
- this is block 2.1
|
||||
|
||||
# Section 2
|
||||
this is a simple line
|
||||
this is another simple line
|
||||
|
||||
## Section 2.1
|
||||
- this is block 3.1
|
||||
- this is block 3.1.1
|
||||
- this is block 3.2
|
||||
|
||||
# Section 3
|
||||
# Section 4
|
||||
some text
|
||||
some text
|
||||
`;
|
||||
|
||||
it('should return correct block for valid markdown string with line number', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, 1);
|
||||
expect(block).toEqual(`# Section 1
|
||||
- this is block 1
|
||||
- this is [[block]] 2
|
||||
- this is block 2.1
|
||||
`);
|
||||
expect(nLines).toEqual(5);
|
||||
});
|
||||
|
||||
it('should return correct block for valid markdown string with position', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, 6);
|
||||
expect(block).toEqual(`# Section 2
|
||||
this is a simple line
|
||||
this is another simple line
|
||||
|
||||
## Section 2.1
|
||||
- this is block 3.1
|
||||
- this is block 3.1.1
|
||||
- this is block 3.2
|
||||
`);
|
||||
expect(nLines).toEqual(9);
|
||||
});
|
||||
|
||||
it('should return single line for section with no content', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, 15);
|
||||
expect(block).toEqual('# Section 3');
|
||||
expect(nLines).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return till end of file for last section', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, 16);
|
||||
expect(block).toEqual(`# Section 4
|
||||
some text
|
||||
some text`);
|
||||
expect(nLines).toEqual(3);
|
||||
});
|
||||
|
||||
it('should return single line for non-existing line number', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, 100);
|
||||
expect(block).toEqual('');
|
||||
expect(nLines).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return single line for non-existing position', () => {
|
||||
const { block, nLines } = getBlockFor(markdown, Position.create(100, 2));
|
||||
expect(block).toEqual('');
|
||||
expect(nLines).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,7 +241,7 @@ const sectionsPlugin: ParserPlugin = {
|
||||
astPointToFoamPosition(tree.position.end).line + 1,
|
||||
0
|
||||
);
|
||||
// Close all the remainig sections
|
||||
// Close all the remaining sections
|
||||
while (sectionStack.length > 0) {
|
||||
const section = sectionStack.pop();
|
||||
note.sections.push({
|
||||
@@ -268,7 +268,7 @@ const titlePlugin: ParserPlugin = {
|
||||
}
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
// Give precedence to the title from the frontmatter if it exists
|
||||
note.title = props.title?.toString() ?? note.title;
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
@@ -433,19 +433,37 @@ export const getBlockFor = (
|
||||
const searchLine = typeof line === 'number' ? line : line.line;
|
||||
const tree = blockParser.parse(markdown);
|
||||
const lines = markdown.split('\n');
|
||||
let block = null;
|
||||
let nLines = 0;
|
||||
let startLine = -1;
|
||||
let endLine = -1;
|
||||
|
||||
// For list items, we also include the sub-lists
|
||||
visit(tree, ['listItem'], (node: any) => {
|
||||
if (node.position.start.line === searchLine + 1) {
|
||||
block = lines
|
||||
.slice(node.position.start.line - 1, node.position.end.line)
|
||||
.join('\n');
|
||||
nLines = node.position.end.line - node.position.start.line;
|
||||
startLine = node.position.start.line - 1;
|
||||
endLine = node.position.end.line;
|
||||
return visit.EXIT;
|
||||
}
|
||||
});
|
||||
if (block == null) {
|
||||
block = lines[searchLine];
|
||||
}
|
||||
|
||||
// For headings, we also include the sub-sections
|
||||
let headingLevel = -1;
|
||||
visit(tree, ['heading'], (node: any) => {
|
||||
if (startLine > -1 && node.depth <= headingLevel) {
|
||||
endLine = node.position.start.line - 1;
|
||||
return visit.EXIT;
|
||||
}
|
||||
if (node.position.start.line === searchLine + 1) {
|
||||
headingLevel = node.depth;
|
||||
startLine = node.position.start.line - 1;
|
||||
endLine = lines.length - 1; // in case it's the last section
|
||||
}
|
||||
});
|
||||
|
||||
let nLines = startLine == -1 ? 1 : endLine - startLine;
|
||||
let block =
|
||||
startLine == -1
|
||||
? lines[searchLine] ?? ''
|
||||
: lines.slice(startLine, endLine).join('\n');
|
||||
|
||||
return { block, nLines };
|
||||
};
|
||||
|
||||
@@ -316,4 +316,40 @@ describe('Generation of markdown references', () => {
|
||||
'../dir3/page-c.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate links for embedded notes that are formatted properly', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'Link to ![[page-b]] and [[page-c]]',
|
||||
'/dir1/page-a.md'
|
||||
);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => [r.url, r.label])).toEqual([
|
||||
['../dir2/page-b.md', 'page-b'],
|
||||
['../dir3/page-c.md', 'page-c'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not generate links for placeholders', () => {
|
||||
const workspace = createTestWorkspace();
|
||||
const noteA = createNoteFromMarkdown(
|
||||
'Link to ![[page-b]] and [[page-c]] and [[does-not-exist]] and ![[does-not-exist-either]]',
|
||||
'/dir1/page-a.md'
|
||||
);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
|
||||
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
|
||||
|
||||
const references = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(references.map(r => r.url)).toEqual([
|
||||
'../dir2/page-b.md',
|
||||
'../dir3/page-c.md',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,17 +12,19 @@ import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { MarkdownLink } from './markdown-link';
|
||||
import { IDataStore } from './datastore';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly parser: ResourceParser
|
||||
private readonly parser: ResourceParser,
|
||||
public readonly noteExtensions: string[] = ['.md']
|
||||
) {}
|
||||
|
||||
supports(uri: URI) {
|
||||
return uri.isMarkdown();
|
||||
return this.noteExtensions.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
@@ -106,27 +108,19 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
export function createMarkdownReferences(
|
||||
workspace: FoamWorkspace,
|
||||
noteUri: URI,
|
||||
source: Resource | URI,
|
||||
includeExtension: boolean
|
||||
): NoteLinkDefinition[] {
|
||||
const source = workspace.find(noteUri);
|
||||
// Should never occur since we're already in a file,
|
||||
if (source?.type !== 'note') {
|
||||
console.warn(
|
||||
`Note ${noteUri.toString()} note found in workspace when attempting \
|
||||
to generate markdown reference list`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const resource = source instanceof URI ? workspace.find(source) : source;
|
||||
|
||||
return source.links
|
||||
const definitions = resource.links
|
||||
.filter(link => link.type === 'wikilink')
|
||||
.map(link => {
|
||||
const targetUri = workspace.resolveLink(source, link);
|
||||
const targetUri = workspace.resolveLink(resource, link);
|
||||
const target = workspace.find(targetUri);
|
||||
if (isNone(target)) {
|
||||
Logger.warn(
|
||||
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
|
||||
`Link ${targetUri.toString()} in ${resource.uri.toString()} is not valid.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -135,34 +129,27 @@ to generate markdown reference list`
|
||||
return null;
|
||||
}
|
||||
|
||||
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
|
||||
if (!includeExtension && relativeUri.path.endsWith('.md')) {
|
||||
let relativeUri = target.uri.relativeTo(resource.uri.getDirectory());
|
||||
if (
|
||||
!includeExtension &&
|
||||
relativeUri.path.endsWith(workspace.defaultExtension)
|
||||
) {
|
||||
relativeUri = relativeUri.changeExtension('*', '');
|
||||
}
|
||||
|
||||
// [wikilink-text]: path/to/file.md "Page title"
|
||||
return {
|
||||
label:
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText,
|
||||
// embedded looks like ![[note-a]]
|
||||
// regular note looks like [[note-a]]
|
||||
label: link.rawText.substring(
|
||||
link.isEmbed ? 3 : 2,
|
||||
link.rawText.length - 2
|
||||
),
|
||||
url: relativeUri.path,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
.filter(isSome)
|
||||
.sort();
|
||||
}
|
||||
|
||||
export function stringifyMarkdownLinkReferenceDefinition(
|
||||
definition: NoteLinkDefinition
|
||||
) {
|
||||
const url =
|
||||
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
|
||||
let text = `[${definition.label}]: ${url}`;
|
||||
if (definition.title) {
|
||||
text = `${text} "${definition.title}"`;
|
||||
}
|
||||
|
||||
return text;
|
||||
return uniqBy(definitions, def => NoteLinkDefinition.format(def));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,22 @@ Logger.setLevel('error');
|
||||
|
||||
describe('Resource Filter', () => {
|
||||
describe('Filter parameters', () => {
|
||||
it('should support the path regex', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/foo.md',
|
||||
type: 'type-1',
|
||||
});
|
||||
const noteB = createTestNote({
|
||||
uri: 'note-b.md',
|
||||
type: '/path/to/bar.md',
|
||||
});
|
||||
|
||||
const filter = createFilter({ path: 'foo' }, false);
|
||||
|
||||
expect(filter(noteA)).toBeTruthy();
|
||||
expect(filter(noteB)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support expressions when code execution is enabled', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
|
||||
@@ -53,6 +53,9 @@ export function createFilter(
|
||||
if (expressionFn && !expressionFn(resource)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.path && !resource.uri.path.match(filter.path)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type && resource.type !== filter.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { workspace } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { toVsCodeUri } from './utils/vsc-utils';
|
||||
import { NoteFactory } from './services/templates';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
import { asAbsoluteWorkspaceUri } from './services/editor';
|
||||
@@ -76,7 +74,7 @@ export function getDailyNoteFileName(date: Date): string {
|
||||
* this function will create all folders in the path.
|
||||
*
|
||||
* @param currentDate The current date, to be used as a title.
|
||||
* @returns Wether the file was created.
|
||||
* @returns Whether the file was created.
|
||||
*/
|
||||
export async function createDailyNoteIfNotExists(targetDate: Date) {
|
||||
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
|
||||
|
||||
@@ -7,12 +7,17 @@ import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import {
|
||||
getAttachmentsExtensions,
|
||||
getIgnoredFilesSetting,
|
||||
getNotesExtensions,
|
||||
} from './settings';
|
||||
import { AttachmentResourceProvider } from './core/services/attachment-provider';
|
||||
import { VsCodeWatcher } from './services/watcher';
|
||||
import { createMarkdownParser } from './core/services/markdown-parser';
|
||||
import VsCodeBasedParserCache from './services/cache';
|
||||
import { createMatcherAndDataStore } from './services/editor';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
@@ -45,16 +50,30 @@ export async function activate(context: ExtensionContext) {
|
||||
const parserCache = new VsCodeBasedParserCache(context);
|
||||
const parser = createMarkdownParser([], parserCache);
|
||||
|
||||
const markdownProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
const attachmentProvider = new AttachmentResourceProvider();
|
||||
const { notesExtensions, defaultExtension } = getNotesExtensions();
|
||||
|
||||
const foamPromise = bootstrap(matcher, watcher, dataStore, parser, [
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
]);
|
||||
const markdownProvider = new MarkdownResourceProvider(
|
||||
dataStore,
|
||||
parser,
|
||||
notesExtensions
|
||||
);
|
||||
|
||||
const attachmentExtConfig = getAttachmentsExtensions();
|
||||
const attachmentProvider = new AttachmentResourceProvider(
|
||||
attachmentExtConfig
|
||||
);
|
||||
|
||||
const foamPromise = bootstrap(
|
||||
matcher,
|
||||
watcher,
|
||||
dataStore,
|
||||
parser,
|
||||
[markdownProvider, attachmentProvider],
|
||||
defaultExtension
|
||||
);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
const resPromises = features.map(feature => feature(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
|
||||
@@ -66,7 +85,21 @@ export async function activate(context: ExtensionContext) {
|
||||
attachmentProvider,
|
||||
commands.registerCommand('foam-vscode.clear-cache', () =>
|
||||
parserCache.clear()
|
||||
)
|
||||
),
|
||||
workspace.onDidChangeConfiguration(e => {
|
||||
if (
|
||||
[
|
||||
'foam.files.ignore',
|
||||
'foam.files.attachmentExtensions',
|
||||
'foam.files.noteExtensions',
|
||||
'foam.files.defaultNoteExtension',
|
||||
].some(setting => e.affectsConfiguration(setting))
|
||||
) {
|
||||
window.showInformationMessage(
|
||||
'Foam: Reload the window to use the updated settings'
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { window, env, ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { removeBrackets } from '../../utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.copy-without-brackets',
|
||||
copyWithoutBrackets
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.copy-without-brackets',
|
||||
copyWithoutBrackets
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function copyWithoutBrackets() {
|
||||
// Get the active text editor
|
||||
@@ -34,5 +31,3 @@ async function copyWithoutBrackets() {
|
||||
window.showInformationMessage('Successfully copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { commands, window } from 'vscode';
|
||||
import * as editor from '../../services/editor';
|
||||
|
||||
describe('create-note-from-default-template command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,8 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { createTemplate } from '../../services/templates';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-new-template',
|
||||
createTemplate
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.create-new-template', createTemplate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { commands, window } from 'vscode';
|
||||
import * as editor from '../../services/editor';
|
||||
|
||||
describe('create-note-from-default-template command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can be cancelled while resolving FOAM_TITLE', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const docCreatorSpy = jest.spyOn(editor, 'createDocAndFocus');
|
||||
|
||||
await commands.executeCommand(
|
||||
'foam-vscode.create-note-from-default-template'
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
prompt: `Enter a title for the new note`,
|
||||
value: 'Title of my New Note',
|
||||
validateInput: expect.anything(),
|
||||
});
|
||||
|
||||
expect(docCreatorSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { commands, window, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getDefaultTemplateUri, NoteFactory } from '../../services/templates';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
|
||||
/**
|
||||
* Create a new note from the default template.
|
||||
*
|
||||
* @deprecated use 'foam-vscode.create-note' instead
|
||||
*/
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-default-template',
|
||||
() => {
|
||||
window.showWarningMessage(
|
||||
"This command is deprecated, use 'Foam: Create Note' (foam-vscode.create-note) instead"
|
||||
);
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
return NoteFactory.createFromTemplate(
|
||||
getDefaultTemplateUri(),
|
||||
resolver,
|
||||
undefined,
|
||||
`---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}
|
||||
`
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -1,25 +1,20 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { askUserForTemplate, NoteFactory } from '../../services/templates';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
async () => {
|
||||
const templateUri = await askUserForTemplate();
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
async () => {
|
||||
const templateUri = await askUserForTemplate();
|
||||
|
||||
if (templateUri) {
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
if (templateUri) {
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
await NoteFactory.createFromTemplate(templateUri, resolver);
|
||||
}
|
||||
await NoteFactory.createFromTemplate(templateUri, resolver);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,10 @@ describe('factories', () => {
|
||||
describe('forPlaceholder', () => {
|
||||
it('adds the .md extension to notes created for placeholders', async () => {
|
||||
await closeEditors();
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder('my-placeholder');
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
'my-placeholder',
|
||||
'.md'
|
||||
);
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
const doc = window.activeTextEditor.document;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import {
|
||||
askUserForTemplate,
|
||||
@@ -7,12 +6,17 @@ import {
|
||||
getPathFromTitle,
|
||||
NoteFactory,
|
||||
} from '../../services/templates';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
|
||||
import { isSome } from '../../core/utils';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
|
||||
export default async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, createNote)
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateNoteArgs {
|
||||
/**
|
||||
* The path of the note to create.
|
||||
@@ -110,16 +114,27 @@ async function createNote(args: CreateNoteArgs) {
|
||||
export const CREATE_NOTE_COMMAND = {
|
||||
command: 'foam-vscode.create-note',
|
||||
|
||||
/**
|
||||
* Creates a command descriptor to create a note from the given placeholder.
|
||||
*
|
||||
* @param placeholder the placeholder
|
||||
* @param defaultExtension the default extension (e.g. '.md')
|
||||
* @param extra extra command arguments
|
||||
* @returns the command descriptor
|
||||
*/
|
||||
forPlaceholder: (
|
||||
placeholder: string,
|
||||
defaultExtension: string,
|
||||
extra: Partial<CreateNoteArgs> = {}
|
||||
): CommandDescriptor<CreateNoteArgs> => {
|
||||
const title = placeholder.endsWith('.md')
|
||||
? placeholder.replace(/\.md$/, '')
|
||||
const endsWithDefaultExtension = new RegExp(defaultExtension + '$');
|
||||
|
||||
const title = placeholder.endsWith(defaultExtension)
|
||||
? placeholder.replace(endsWithDefaultExtension, '')
|
||||
: placeholder;
|
||||
const notePath = placeholder.endsWith('.md')
|
||||
const notePath = placeholder.endsWith(defaultExtension)
|
||||
? placeholder
|
||||
: placeholder + '.md';
|
||||
: placeholder + defaultExtension;
|
||||
return {
|
||||
name: CREATE_NOTE_COMMAND.command,
|
||||
params: {
|
||||
@@ -130,13 +145,3 @@ export const CREATE_NOTE_COMMAND = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, createNote)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { default as copyWithoutBracketsCommand } from './copy-without-brackets';
|
||||
export { default as createFromDefaultTemplateCommand } from './create-note-from-default-template';
|
||||
export { default as createFromTemplateCommand } from './create-note-from-template';
|
||||
export { default as createNewTemplate } from './create-new-template';
|
||||
export { default as janitorCommand } from './janitor';
|
||||
|
||||
@@ -5,12 +5,7 @@ import {
|
||||
commands,
|
||||
ProgressLocation,
|
||||
} from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../../settings';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import {
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
@@ -23,15 +18,16 @@ import { Range } from '../../core/model/range';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.janitor', async () =>
|
||||
janitor(await foamPromise)
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.janitor', async () =>
|
||||
janitor(await foamPromise)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function janitor(foam: Foam) {
|
||||
try {
|
||||
@@ -109,14 +105,14 @@ async function runJanitor(foam: Foam) {
|
||||
}
|
||||
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
wikilinkSetting === 'off'
|
||||
? null
|
||||
: await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
noteEol,
|
||||
foam.workspace,
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
|
||||
wikilinkSetting === 'withExtensions'
|
||||
);
|
||||
if (definitions) {
|
||||
updatedDefinitionListCount += 1;
|
||||
@@ -151,14 +147,14 @@ async function runJanitor(foam: Foam) {
|
||||
// Get edits
|
||||
const heading = await generateHeading(note, noteText, eol);
|
||||
const definitions =
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.off
|
||||
wikilinkSetting === 'off'
|
||||
? null
|
||||
: await generateLinkReferences(
|
||||
note,
|
||||
noteText,
|
||||
eol,
|
||||
foam.workspace,
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
|
||||
wikilinkSetting === 'withExtensions'
|
||||
);
|
||||
|
||||
if (heading || definitions) {
|
||||
@@ -192,5 +188,3 @@ async function runJanitor(foam: Foam) {
|
||||
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount,
|
||||
};
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import { ExtensionContext, commands, window, QuickPickItem } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { openDailyNoteFor } from '../../dated-notes';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { range } from 'lodash';
|
||||
import dateFormat from 'dateformat';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.open-daily-note-for-date',
|
||||
async () => {
|
||||
const ws = (await foamPromise).workspace;
|
||||
const date = await window
|
||||
.showQuickPick<DateItem>(generateDateItems(ws), {
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
})
|
||||
.then(item => {
|
||||
return item?.date;
|
||||
});
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.open-daily-note-for-date',
|
||||
async () => {
|
||||
const ws = (await foamPromise).workspace;
|
||||
const date = await window
|
||||
.showQuickPick<DateItem>(generateDateItems(ws), {
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
})
|
||||
.then(item => {
|
||||
return item?.date;
|
||||
});
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
class DateItem implements QuickPickItem {
|
||||
public label: string;
|
||||
@@ -68,5 +69,3 @@ function generateDateItems(ws: FoamWorkspace): DateItem[] {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import { openDailyNoteFor } from '../../dated-notes';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', () =>
|
||||
openDailyNoteFor(new Date())
|
||||
)
|
||||
);
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', () =>
|
||||
openDailyNoteFor(new Date())
|
||||
)
|
||||
);
|
||||
|
||||
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
|
||||
commands.executeCommand('foam-vscode.open-daily-note');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
|
||||
commands.executeCommand('foam-vscode.open-daily-note');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import {
|
||||
createDailyNoteIfNotExists,
|
||||
openDailyNoteFor,
|
||||
} from '../../dated-notes';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-dated-note', date => {
|
||||
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
|
||||
case 'navigateToNote':
|
||||
return openDailyNoteFor(date);
|
||||
case 'createNote':
|
||||
return createDailyNoteIfNotExists(date);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-dated-note', date => {
|
||||
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
|
||||
case 'navigateToNote':
|
||||
return openDailyNoteFor(date);
|
||||
case 'createNote':
|
||||
return createDailyNoteIfNotExists(date);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { focusNote } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-random-note', async () => {
|
||||
const foam = await foamPromise;
|
||||
const currentFile = window.activeTextEditor?.document.uri.path;
|
||||
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
|
||||
if (notes.length <= 1) {
|
||||
window.showInformationMessage(
|
||||
'Could not find another note to open. If you believe this is a bug, please file an issue.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-random-note', async () => {
|
||||
const foam = await foamPromise;
|
||||
const currentFile = window.activeTextEditor?.document.uri.path;
|
||||
const notes = foam.workspace.list().filter(r => r.uri.isMarkdown());
|
||||
if (notes.length <= 1) {
|
||||
window.showInformationMessage(
|
||||
'Could not find another note to open. If you believe this is a bug, please file an issue.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let randomNoteIndex = Math.floor(Math.random() * notes.length);
|
||||
if (notes[randomNoteIndex].uri.path === currentFile) {
|
||||
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
|
||||
}
|
||||
let randomNoteIndex = Math.floor(Math.random() * notes.length);
|
||||
if (notes[randomNoteIndex].uri.path === currentFile) {
|
||||
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
|
||||
}
|
||||
|
||||
focusNote(notes[randomNoteIndex].uri, false);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
focusNote(notes[randomNoteIndex].uri, false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
@@ -11,7 +10,18 @@ import { CommandDescriptor } from '../../utils/commands';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { isSome, isNone } from '../../core/utils';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(OPEN_COMMAND.command, args => {
|
||||
return openResource(foam.workspace, args);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export interface OpenResourceArgs {
|
||||
/**
|
||||
@@ -81,20 +91,6 @@ async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(OPEN_COMMAND.command, args => {
|
||||
return openResource(foam.workspace, args);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
interface ResourceItem extends vscode.QuickPickItem {
|
||||
label: string;
|
||||
description: string;
|
||||
@@ -115,5 +111,3 @@ const createQuickPickItemForResource = (resource: Resource): ResourceItem => {
|
||||
detail: detail,
|
||||
};
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
export const UPDATE_GRAPH_COMMAND_NAME = 'foam-vscode.update-graph';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(UPDATE_GRAPH_COMMAND_NAME, async () => {
|
||||
const foam = await foamPromise;
|
||||
return foam.graph.update();
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(UPDATE_GRAPH_COMMAND_NAME, async () => {
|
||||
const foam = await foamPromise;
|
||||
return foam.graph.update();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { uniq } from 'lodash';
|
||||
import {
|
||||
CancellationToken,
|
||||
CodeLens,
|
||||
@@ -12,209 +11,164 @@ import {
|
||||
workspace,
|
||||
Position,
|
||||
} from 'vscode';
|
||||
import {
|
||||
hasEmptyTrailing,
|
||||
docConfig,
|
||||
loadDocConfig,
|
||||
isMdEditor,
|
||||
mdDocSelector,
|
||||
getText,
|
||||
} from '../../utils';
|
||||
import { FoamFeature } from '../../types';
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../../settings';
|
||||
import { isMdEditor, mdDocSelector } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../../core/services/markdown-provider';
|
||||
import {
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
generateLinkReferences,
|
||||
} from '../../core/janitor/generate-link-references';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
|
||||
import { getEditorEOL } from '../../services/editor';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
const foam = await foamPromise;
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.update-wikilinks', () =>
|
||||
updateReferenceList(foam.workspace)
|
||||
),
|
||||
workspace.onWillSaveTextDocument(e => {
|
||||
if (
|
||||
e.document.languageId === 'markdown' &&
|
||||
foam.services.matcher.isMatch(fromVsCodeUri(e.document.uri))
|
||||
) {
|
||||
e.waitUntil(updateReferenceList(foam.workspace));
|
||||
}
|
||||
}),
|
||||
languages.registerCodeLensProvider(
|
||||
mdDocSelector,
|
||||
new WikilinkReferenceCodeLensProvider(foam.workspace)
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.update-wikilink-definitions', () => {
|
||||
return updateWikilinkDefinitions(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher
|
||||
);
|
||||
}),
|
||||
workspace.onWillSaveTextDocument(e => {
|
||||
e.waitUntil(
|
||||
updateWikilinkDefinitions(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher
|
||||
)
|
||||
);
|
||||
}),
|
||||
languages.registerCodeLensProvider(
|
||||
mdDocSelector,
|
||||
new WikilinkReferenceCodeLensProvider(
|
||||
foam.workspace,
|
||||
foam.services.parser
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
async function createReferenceList(foam: FoamWorkspace) {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refs = await generateReferenceList(foam, editor.document);
|
||||
if (refs && refs.length) {
|
||||
await editor.edit(function (editBuilder) {
|
||||
if (editor) {
|
||||
const spacing = hasEmptyTrailing(editor.document)
|
||||
? docConfig.eol
|
||||
: docConfig.eol + docConfig.eol;
|
||||
|
||||
editBuilder.insert(
|
||||
new Position(editor.document.lineCount, 0),
|
||||
spacing + refs.join(docConfig.eol)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function updateReferenceList(foam: FoamWorkspace) {
|
||||
async function updateWikilinkDefinitions(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher
|
||||
) {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
if (!editor || !isMdEditor(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadDocConfig();
|
||||
|
||||
const doc = editor.document;
|
||||
const range = detectReferenceListRange(doc);
|
||||
|
||||
if (!range) {
|
||||
await createReferenceList(foam);
|
||||
} else {
|
||||
const refs = generateReferenceList(foam, doc);
|
||||
|
||||
// references must always be preceded by an empty line
|
||||
const spacing = doc.lineAt(range.start.line - 1).isEmptyOrWhitespace
|
||||
? ''
|
||||
: docConfig.eol;
|
||||
|
||||
await editor.edit(editBuilder => {
|
||||
editBuilder.replace(range, spacing + refs.join(docConfig.eol));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function generateReferenceList(
|
||||
foam: FoamWorkspace,
|
||||
doc: TextDocument
|
||||
): string[] {
|
||||
const wikilinkSetting = getWikilinkDefinitionSetting();
|
||||
|
||||
if (wikilinkSetting === LinkReferenceDefinitionsSetting.off) {
|
||||
return [];
|
||||
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const note = foam.get(fromVsCodeUri(doc.uri));
|
||||
const setting = getWikilinkDefinitionSetting();
|
||||
const eol = getEditorEOL();
|
||||
const text = doc.getText();
|
||||
|
||||
// Should never happen as `doc` is usually given by `editor.document`, which
|
||||
// binds to an opened note.
|
||||
if (!note) {
|
||||
console.warn(
|
||||
`Can't find note for URI ${doc.uri.path} before attempting to generate its markdown reference list`
|
||||
);
|
||||
return [];
|
||||
if (setting === 'off') {
|
||||
const { range } = detectDocumentWikilinkDefinitions(text, eol);
|
||||
if (range) {
|
||||
await editor.edit(editBuilder => {
|
||||
editBuilder.delete(toVsCodeRange(range));
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const references = uniq(
|
||||
createMarkdownReferences(
|
||||
foam,
|
||||
note.uri,
|
||||
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
|
||||
).map(stringifyMarkdownLinkReferenceDefinition)
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
const update = await generateLinkReferences(
|
||||
resource,
|
||||
text,
|
||||
eol,
|
||||
fWorkspace,
|
||||
setting === 'withExtensions'
|
||||
);
|
||||
|
||||
if (references.length) {
|
||||
return [
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
...references,
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
];
|
||||
if (update) {
|
||||
await editor.edit(editBuilder => {
|
||||
const gap = doc.lineAt(update.range.start.line - 1).isEmptyOrWhitespace
|
||||
? ''
|
||||
: eol;
|
||||
editBuilder.replace(toVsCodeRange(update.range), gap + update.newText);
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the range of existing reference list
|
||||
* @param doc
|
||||
* Detects the range of the wikilink definitions in the document.
|
||||
*/
|
||||
function detectReferenceListRange(doc: TextDocument): Range | null {
|
||||
const fullText = doc.getText();
|
||||
function detectDocumentWikilinkDefinitions(text: string, eol: string) {
|
||||
const lines = text.split(eol);
|
||||
|
||||
const headerIndex = fullText.indexOf(LINK_REFERENCE_DEFINITION_HEADER);
|
||||
const footerIndex = fullText.lastIndexOf(LINK_REFERENCE_DEFINITION_FOOTER);
|
||||
|
||||
if (headerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headerLine =
|
||||
fullText.substring(0, headerIndex).split(docConfig.eol).length - 1;
|
||||
|
||||
const footerLine =
|
||||
fullText.substring(0, footerIndex).split(docConfig.eol).length - 1;
|
||||
|
||||
if (headerLine >= footerLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Range(
|
||||
new Position(headerLine, 0),
|
||||
new Position(footerLine, LINK_REFERENCE_DEFINITION_FOOTER.length)
|
||||
const headerLine = lines.findIndex(
|
||||
line => line === LINK_REFERENCE_DEFINITION_HEADER
|
||||
);
|
||||
const footerLine = lines.findIndex(
|
||||
line => line === LINK_REFERENCE_DEFINITION_FOOTER
|
||||
);
|
||||
|
||||
if (headerLine < 0 || footerLine < 0 || headerLine >= footerLine) {
|
||||
return { range: null, definitions: null };
|
||||
}
|
||||
|
||||
const range = new Range(
|
||||
new Position(headerLine, 0),
|
||||
new Position(footerLine, lines[footerLine].length)
|
||||
);
|
||||
const definitions = lines.slice(headerLine, footerLine).join(eol);
|
||||
|
||||
return { range, definitions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a code lens to update the wikilink definitions in the document.
|
||||
*/
|
||||
class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
|
||||
private foam: FoamWorkspace;
|
||||
constructor(
|
||||
private fWorkspace: FoamWorkspace,
|
||||
private fParser: ResourceParser
|
||||
) {}
|
||||
|
||||
constructor(foam: FoamWorkspace) {
|
||||
this.foam = foam;
|
||||
}
|
||||
|
||||
public provideCodeLenses(
|
||||
public async provideCodeLenses(
|
||||
document: TextDocument,
|
||||
_: CancellationToken
|
||||
): CodeLens[] | Promise<CodeLens[]> {
|
||||
loadDocConfig();
|
||||
): Promise<CodeLens[]> {
|
||||
const eol = getEditorEOL();
|
||||
const text = document.getText();
|
||||
|
||||
const range = detectReferenceListRange(document);
|
||||
const { range } = detectDocumentWikilinkDefinitions(text, eol);
|
||||
if (!range) {
|
||||
return [];
|
||||
}
|
||||
const setting = getWikilinkDefinitionSetting();
|
||||
|
||||
const refs = generateReferenceList(this.foam, document);
|
||||
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
|
||||
const newRefs = refs.join(docConfig.eol);
|
||||
const resource = this.fParser.parse(fromVsCodeUri(document.uri), text);
|
||||
const update = await generateLinkReferences(
|
||||
resource,
|
||||
text,
|
||||
eol,
|
||||
this.fWorkspace,
|
||||
setting === 'withExtensions'
|
||||
);
|
||||
|
||||
const status = oldRefs === newRefs ? 'up to date' : 'out of date';
|
||||
const status = update == null ? 'up to date' : 'out of date';
|
||||
|
||||
return [
|
||||
new CodeLens(range, {
|
||||
command:
|
||||
update == null ? '' : 'foam-vscode.update-wikilink-definitions',
|
||||
title: `Wikilink definitions (${status})`,
|
||||
arguments: [],
|
||||
title: `Link references (${status})`,
|
||||
command: '',
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -9,7 +9,17 @@ import {
|
||||
} from 'vscode';
|
||||
import { getDailyNoteFileName } from '../dated-notes';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
languages.registerCompletionItemProvider('markdown', completions, '/'),
|
||||
languages.registerCompletionItemProvider(
|
||||
'markdown',
|
||||
datesCompletionProvider,
|
||||
'/'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface DateSnippet {
|
||||
snippet: string;
|
||||
@@ -28,21 +38,42 @@ const daysOfWeek = [
|
||||
];
|
||||
|
||||
const generateDayOfWeekSnippets = (): DateSnippet[] => {
|
||||
const getTarget = (day: number) => {
|
||||
const getFutureTarget = (day: number) => {
|
||||
const target = new Date();
|
||||
const currentDay = target.getDay();
|
||||
const distance = (day + 7 - currentDay) % 7;
|
||||
target.setDate(target.getDate() + distance);
|
||||
return target;
|
||||
};
|
||||
// needs work
|
||||
const getPastTarget = (day: number) => {
|
||||
const target = new Date();
|
||||
const currentDay = target.getDay();
|
||||
const distance = currentDay === day ? 7 : (7 + currentDay - day) % 7;
|
||||
target.setDate(target.getDate() - distance);
|
||||
return target;
|
||||
};
|
||||
|
||||
const snippets = daysOfWeek.map(({ day, index }) => {
|
||||
const target = getTarget(index);
|
||||
const target = getFutureTarget(index);
|
||||
return {
|
||||
date: target,
|
||||
detail: `Get a daily note link for ${day}`,
|
||||
snippet: `/${day}`,
|
||||
};
|
||||
});
|
||||
|
||||
// append snippets previous days
|
||||
snippets.push(
|
||||
...daysOfWeek.map(({ day, index }) => {
|
||||
const target = getPastTarget(index);
|
||||
return {
|
||||
date: target,
|
||||
detail: `Get a daily note link for last ${day}`,
|
||||
snippet: `/-${day}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
return snippets;
|
||||
};
|
||||
|
||||
@@ -198,18 +229,3 @@ export const datesCompletionProvider: CompletionItemProvider = {
|
||||
return new CompletionList(completionItems, true);
|
||||
},
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
languages.registerCompletionItemProvider('markdown', completions, '/'),
|
||||
languages.registerCompletionItemProvider(
|
||||
'markdown',
|
||||
datesCompletionProvider,
|
||||
'/'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
@@ -41,39 +40,35 @@ const updateDecorations =
|
||||
editor.setDecorations(placeholderDecoration, placeholderRanges);
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
let activeEditor = vscode.window.activeTextEditor;
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
let activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
const immediatelyUpdateDecorations = updateDecorations(
|
||||
foam.services.parser,
|
||||
foam.workspace
|
||||
);
|
||||
const immediatelyUpdateDecorations = updateDecorations(
|
||||
foam.services.parser,
|
||||
foam.workspace
|
||||
);
|
||||
|
||||
const debouncedUpdateDecorations = debounce(
|
||||
immediatelyUpdateDecorations,
|
||||
500
|
||||
);
|
||||
const debouncedUpdateDecorations = debounce(
|
||||
immediatelyUpdateDecorations,
|
||||
500
|
||||
);
|
||||
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
|
||||
context.subscriptions.push(
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (activeEditor && event.document === activeEditor.document) {
|
||||
debouncedUpdateDecorations(activeEditor);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default feature;
|
||||
context.subscriptions.push(
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (activeEditor && event.document === activeEditor.document) {
|
||||
debouncedUpdateDecorations(activeEditor);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import {
|
||||
@@ -18,30 +17,28 @@ import { commandAsURI } from '../utils/commands';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const isHoverEnabled: ConfigurationMonitor<boolean> =
|
||||
monitorFoamVsCodeConfig(CONFIG_KEY);
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const isHoverEnabled: ConfigurationMonitor<boolean> =
|
||||
monitorFoamVsCodeConfig(CONFIG_KEY);
|
||||
|
||||
const foam = await foamPromise;
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
isHoverEnabled,
|
||||
vscode.languages.registerHoverProvider(
|
||||
mdDocSelector,
|
||||
new HoverProvider(
|
||||
isHoverEnabled,
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
foam.services.parser
|
||||
)
|
||||
context.subscriptions.push(
|
||||
isHoverEnabled,
|
||||
vscode.languages.registerHoverProvider(
|
||||
mdDocSelector,
|
||||
new HoverProvider(
|
||||
isHoverEnabled,
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
foam.services.parser
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class HoverProvider implements vscode.HoverProvider {
|
||||
constructor(
|
||||
@@ -109,12 +106,16 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
: this.workspace.get(targetUri).title;
|
||||
}
|
||||
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(targetUri.path, {
|
||||
askForTemplate: true,
|
||||
onFileExists: 'open',
|
||||
});
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
targetUri.path,
|
||||
this.workspace.defaultExtension,
|
||||
{
|
||||
askForTemplate: true,
|
||||
onFileExists: 'open',
|
||||
}
|
||||
);
|
||||
const newNoteFromTemplate = new vscode.MarkdownString(
|
||||
`[Create note from template for '${targetUri.getName()}'](${commandAsURI(
|
||||
`[Create note from template for '${targetUri.getBasename()}'](${commandAsURI(
|
||||
command
|
||||
).toString()})`
|
||||
);
|
||||
@@ -131,5 +132,3 @@ export class HoverProvider implements vscode.HoverProvider {
|
||||
return hover;
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as panels from './panels';
|
||||
import dateSnippets from './date-snippets';
|
||||
import hoverProvider from './hover-provider';
|
||||
import preview from './preview';
|
||||
import completionProvider, { completionCursorMove } from './link-completion';
|
||||
import completionProvider from './link-completion';
|
||||
import tagCompletionProvider from './tag-completion';
|
||||
import linkDecorations from './document-decorator';
|
||||
import navigationProviders from './navigation-provider';
|
||||
@@ -23,5 +23,4 @@ export const features: FoamFeature[] = [
|
||||
preview,
|
||||
completionProvider,
|
||||
tagCompletionProvider,
|
||||
completionCursorMove,
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Resource } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
@@ -21,81 +20,72 @@ const COMPLETION_CURSOR_MOVE = {
|
||||
export const WIKILINK_REGEX = /\[\[[^[\]]*(?!.*\]\])/;
|
||||
export const SECTION_REGEX = /\[\[([^[\]]*#(?!.*\]\]))/;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new WikilinkCompletionProvider(foam.workspace, foam.graph),
|
||||
'['
|
||||
),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new SectionCompletionProvider(foam.workspace),
|
||||
'#'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new WikilinkCompletionProvider(foam.workspace, foam.graph),
|
||||
'['
|
||||
),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new SectionCompletionProvider(foam.workspace),
|
||||
'#'
|
||||
),
|
||||
|
||||
/**
|
||||
* always jump to the closing bracket, but jump back the cursor when commit
|
||||
* by alias divider `|` and section divider `#`
|
||||
* See https://github.com/foambubble/foam/issues/962,
|
||||
*/
|
||||
/**
|
||||
* always jump to the closing bracket, but jump back the cursor when commit
|
||||
* by alias divider `|` and section divider `#`
|
||||
* See https://github.com/foambubble/foam/issues/962,
|
||||
*/
|
||||
vscode.commands.registerCommand(
|
||||
COMPLETION_CURSOR_MOVE.command,
|
||||
async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const document = activeEditor.document;
|
||||
const currentPosition = activeEditor.selection.active;
|
||||
const cursorChange = vscode.window.onDidChangeTextEditorSelection(
|
||||
async e => {
|
||||
const changedPosition = e.selections[0].active;
|
||||
const preChar = document
|
||||
.lineAt(changedPosition.line)
|
||||
.text.charAt(changedPosition.character - 1);
|
||||
|
||||
export const completionCursorMove: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
COMPLETION_CURSOR_MOVE.command,
|
||||
async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const document = activeEditor.document;
|
||||
const currentPosition = activeEditor.selection.active;
|
||||
const cursorChange = vscode.window.onDidChangeTextEditorSelection(
|
||||
async e => {
|
||||
const changedPosition = e.selections[0].active;
|
||||
const preChar = document
|
||||
.lineAt(changedPosition.line)
|
||||
.text.charAt(changedPosition.character - 1);
|
||||
const { character: selectionChar, line: selectionLine } =
|
||||
e.selections[0].active;
|
||||
|
||||
const { character: selectionChar, line: selectionLine } =
|
||||
e.selections[0].active;
|
||||
const { line: completionLine, character: completionChar } =
|
||||
currentPosition;
|
||||
|
||||
const { line: completionLine, character: completionChar } =
|
||||
currentPosition;
|
||||
const inCompleteBySectionDivider =
|
||||
linkCommitCharacters.includes(preChar) &&
|
||||
selectionLine === completionLine &&
|
||||
selectionChar === completionChar + 1;
|
||||
|
||||
const inCompleteBySectionDivider =
|
||||
linkCommitCharacters.includes(preChar) &&
|
||||
selectionLine === completionLine &&
|
||||
selectionChar === completionChar + 1;
|
||||
|
||||
cursorChange.dispose();
|
||||
if (inCompleteBySectionDivider) {
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'left',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
cursorChange.dispose();
|
||||
if (inCompleteBySectionDivider) {
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'left',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'right',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'right',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem>
|
||||
@@ -307,5 +297,3 @@ const normalize = (text: string) => text.toLocaleLowerCase().trim();
|
||||
function wikilinkRequiresAlias(resource: Resource) {
|
||||
return normalize(resource.uri.getName()) !== normalize(resource.title);
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { createTestWorkspace } from '../test/test-utils';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { NavigationProvider } from './navigation-provider';
|
||||
import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
@@ -85,7 +83,7 @@ describe('Document navigation', () => {
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(
|
||||
commandAsURI(
|
||||
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', {
|
||||
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', '.md', {
|
||||
onFileExists: 'open',
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
@@ -12,35 +11,33 @@ import { Position } from '../core/model/position';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const navigationProvider = new NavigationProvider(
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
foam.services.parser
|
||||
);
|
||||
const navigationProvider = new NavigationProvider(
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
foam.services.parser
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDefinitionProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerReferenceProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDefinitionProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerReferenceProvider(
|
||||
mdDocSelector,
|
||||
navigationProvider
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides navigation and references for Foam links.
|
||||
@@ -67,7 +64,7 @@ export class NavigationProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Provide references for links and placholders
|
||||
* Provide references for links and placeholders
|
||||
*/
|
||||
public provideReferences(
|
||||
document: vscode.TextDocument,
|
||||
@@ -164,9 +161,13 @@ export class NavigationProvider
|
||||
return targets
|
||||
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
|
||||
.map(o => {
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(o.target.path, {
|
||||
onFileExists: 'open',
|
||||
});
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
o.target.path,
|
||||
this.workspace.defaultExtension,
|
||||
{
|
||||
onFileExists: 'open',
|
||||
}
|
||||
);
|
||||
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
@@ -182,5 +183,3 @@ export class NavigationProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
groupRangesByResource,
|
||||
} from '../../utils/tree-view-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(foam.workspace, foam.graph);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
provider.target = vscode.window.activeTextEditor
|
||||
? fromVsCodeUri(vscode.window.activeTextEditor?.document.uri)
|
||||
: undefined;
|
||||
await provider.refresh();
|
||||
});
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.backlinks', provider),
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
);
|
||||
},
|
||||
};
|
||||
export default feature;
|
||||
|
||||
export class BacklinksTreeDataProvider
|
||||
implements vscode.TreeDataProvider<vscode.TreeItem>
|
||||
{
|
||||
public target?: URI = undefined;
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeDataEmitter = new vscode.EventEmitter<BacklinkPanelTreeItem | undefined | void>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
|
||||
|
||||
constructor(private workspace: FoamWorkspace, private graph: FoamGraph) {}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeDataEmitter.fire();
|
||||
}
|
||||
|
||||
getTreeItem(item: BacklinkPanelTreeItem): vscode.TreeItem {
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(item?: BacklinkPanelTreeItem): Promise<vscode.TreeItem[]> {
|
||||
const uri = this.target;
|
||||
if (item && item instanceof ResourceTreeItem) {
|
||||
return item.getChildren();
|
||||
}
|
||||
|
||||
if (isNone(uri) || isNone(this.workspace.find(uri))) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const connections = this.graph
|
||||
.getConnections(uri)
|
||||
.filter(c => c.target.asPlain().isEqual(uri));
|
||||
|
||||
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> {
|
||||
return item.resolveTreeItem();
|
||||
}
|
||||
}
|
||||
|
||||
type BacklinkPanelTreeItem = ResourceTreeItem | ResourceRangeTreeItem;
|
||||
@@ -6,15 +6,13 @@ import {
|
||||
createNote,
|
||||
getUriInWorkspace,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { BacklinksTreeDataProvider } from './backlinks';
|
||||
import { OPEN_COMMAND } from '../commands/open-resource';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { ConnectionsTreeDataProvider } from './connections';
|
||||
import { MapBasedMemento, 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';
|
||||
} from './utils/tree-view-utils';
|
||||
|
||||
describe('Backlinks panel', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -23,11 +21,6 @@ describe('Backlinks panel', () => {
|
||||
await createNote(noteB);
|
||||
await createNote(noteC);
|
||||
});
|
||||
afterAll(async () => {
|
||||
graph.dispose();
|
||||
ws.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
// TODO: this should really just be the workspace folder, use that once #806 is fixed
|
||||
const rootUri = getUriInWorkspace('just-a-ref.md');
|
||||
@@ -50,19 +43,25 @@ describe('Backlinks panel', () => {
|
||||
ws.set(noteA).set(noteB).set(noteC);
|
||||
const graph = FoamGraph.fromWorkspace(ws, true);
|
||||
|
||||
const provider = new BacklinksTreeDataProvider(ws, graph);
|
||||
const provider = new ConnectionsTreeDataProvider(
|
||||
ws,
|
||||
graph,
|
||||
new MapBasedMemento(),
|
||||
false
|
||||
);
|
||||
|
||||
afterAll(async () => {
|
||||
graph.dispose();
|
||||
ws.dispose();
|
||||
provider.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
provider.target = undefined;
|
||||
});
|
||||
|
||||
// Skipping these as still figuring out how to interact with the provider
|
||||
// running in the test instance of VS Code
|
||||
it.skip('does not target excluded files', async () => {
|
||||
provider.target = URI.file('/excluded-file.txt');
|
||||
expect(await provider.getChildren()).toEqual([]);
|
||||
});
|
||||
it.skip('targets active editor', async () => {
|
||||
const docA = await workspace.openTextDocument(toVsCodeUri(noteA.uri));
|
||||
const docB = await workspace.openTextDocument(toVsCodeUri(noteB.uri));
|
||||
@@ -76,6 +75,7 @@ describe('Backlinks panel', () => {
|
||||
|
||||
it('shows linking resources alphaetically by name', async () => {
|
||||
provider.target = noteA.uri;
|
||||
await provider.refresh();
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.map(n => n.resource.uri.path)).toEqual([
|
||||
noteB.uri.path,
|
||||
@@ -84,6 +84,7 @@ describe('Backlinks panel', () => {
|
||||
});
|
||||
it('shows references in range order', async () => {
|
||||
provider.target = noteA.uri;
|
||||
await provider.refresh();
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
@@ -96,14 +97,24 @@ describe('Backlinks panel', () => {
|
||||
});
|
||||
it('navigates to the document if clicking on note', async () => {
|
||||
provider.target = noteA.uri;
|
||||
await provider.refresh();
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes[0].command).toMatchObject({
|
||||
command: 'vscode.open',
|
||||
arguments: [expect.objectContaining({ path: noteB.uri.path })],
|
||||
});
|
||||
const links = (await provider.getChildren(notes[0])) as ResourceTreeItem[];
|
||||
expect(links[0].command).toMatchObject({
|
||||
command: 'vscode.open',
|
||||
arguments: [
|
||||
expect.objectContaining({ path: noteB.uri.path }),
|
||||
expect.objectContaining({ selection: expect.anything() }),
|
||||
],
|
||||
});
|
||||
});
|
||||
it('navigates to document with link selection if clicking on backlink', async () => {
|
||||
provider.target = noteA.uri;
|
||||
await provider.refresh();
|
||||
const notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
const linksFromB = (await provider.getChildren(
|
||||
notes[0]
|
||||
@@ -121,7 +132,7 @@ describe('Backlinks panel', () => {
|
||||
it('refreshes upon changes in the workspace', async () => {
|
||||
let notes: ResourceTreeItem[] = [];
|
||||
provider.target = noteA.uri;
|
||||
|
||||
await provider.refresh();
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(2);
|
||||
|
||||
@@ -130,6 +141,7 @@ describe('Backlinks panel', () => {
|
||||
uri: './note-d.md',
|
||||
});
|
||||
ws.set(noteD);
|
||||
await provider.refresh();
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(2);
|
||||
|
||||
@@ -139,8 +151,8 @@ describe('Backlinks panel', () => {
|
||||
links: [{ slug: 'note-a' }],
|
||||
});
|
||||
ws.set(noteDBis);
|
||||
await provider.refresh();
|
||||
notes = (await provider.getChildren()) as ResourceTreeItem[];
|
||||
expect(notes.length).toEqual(3);
|
||||
expect(notes.map(n => n.resource.uri.path)).toEqual(
|
||||
[noteB.uri, noteC.uri, noteD.uri].map(uri => uri.path)
|
||||
);
|
||||
171
packages/foam-vscode/src/features/panels/connections.ts
Normal file
171
packages/foam-vscode/src/features/panels/connections.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { isNone } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Connection, FoamGraph } from '../../core/model/graph';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { ContextMemento, fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import {
|
||||
BaseTreeItem,
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
createConnectionItemsForResource,
|
||||
} from './utils/tree-view-utils';
|
||||
import { BaseTreeProvider } from './utils/base-tree-provider';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const provider = new ConnectionsTreeDataProvider(
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
context.globalState
|
||||
);
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.connections', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
|
||||
const updateTreeView = async () => {
|
||||
provider.target = vscode.window.activeTextEditor
|
||||
? fromVsCodeUri(vscode.window.activeTextEditor?.document.uri)
|
||||
: undefined;
|
||||
await provider.refresh();
|
||||
};
|
||||
|
||||
updateTreeView();
|
||||
|
||||
context.subscriptions.push(
|
||||
provider,
|
||||
treeView,
|
||||
foam.graph.onDidUpdate(() => updateTreeView()),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => updateTreeView()),
|
||||
provider.onDidChangeTreeData(() => {
|
||||
treeView.title = ` ${provider.show.get()} (${provider.nValues})`;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeItem> {
|
||||
public show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
|
||||
this.state,
|
||||
`foam-vscode.views.connections.show`,
|
||||
'all links',
|
||||
true
|
||||
);
|
||||
public target?: URI = undefined;
|
||||
public nValues = 0;
|
||||
private connectionItems: ResourceRangeTreeItem[] = [];
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
public state: vscode.Memento,
|
||||
registerCommands = true // for testing. don't love it, but will do for now
|
||||
) {
|
||||
super();
|
||||
if (!registerCommands) {
|
||||
return;
|
||||
}
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.connections.show:all-links`,
|
||||
() => {
|
||||
this.show.update('all links');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.connections.show:backlinks`,
|
||||
() => {
|
||||
this.show.update('backlinks');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.connections.show:forward-links`,
|
||||
() => {
|
||||
this.show.update('forward links');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
const uri = this.target;
|
||||
|
||||
const connectionItems =
|
||||
isNone(uri) || isNone(this.workspace.find(uri))
|
||||
? []
|
||||
: await createConnectionItemsForResource(
|
||||
this.workspace,
|
||||
this.graph,
|
||||
uri,
|
||||
(connection: Connection) => {
|
||||
const isBacklink = connection.target
|
||||
.asPlain()
|
||||
.isEqual(this.target);
|
||||
return (
|
||||
this.show.get() === 'all links' ||
|
||||
(isBacklink && this.show.get() === 'backlinks') ||
|
||||
(!isBacklink && this.show.get() === 'forward links')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.connectionItems = connectionItems;
|
||||
this.nValues = connectionItems.length;
|
||||
super.refresh();
|
||||
}
|
||||
|
||||
async getChildren(item?: BacklinkPanelTreeItem): Promise<vscode.TreeItem[]> {
|
||||
if (item && item instanceof BaseTreeItem) {
|
||||
return item.getChildren();
|
||||
}
|
||||
|
||||
const byResource = this.connectionItems.reduce((acc, item) => {
|
||||
const connection = item.value as Connection;
|
||||
const isBacklink = connection.target.asPlain().isEqual(this.target);
|
||||
const uri = isBacklink ? connection.source : connection.target;
|
||||
acc.set(uri.toString(), [...(acc.get(uri.toString()) ?? []), item]);
|
||||
return acc;
|
||||
}, new Map() as Map<string, ResourceRangeTreeItem[]>);
|
||||
|
||||
const resourceItems = [];
|
||||
for (const [uriString, items] of byResource.entries()) {
|
||||
const uri = URI.parse(uriString);
|
||||
const item = uri.isPlaceholder()
|
||||
? new UriTreeItem(uri, {
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Expanded,
|
||||
})
|
||||
: new ResourceTreeItem(this.workspace.get(uri), this.workspace, {
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Expanded,
|
||||
});
|
||||
const children = items.sort((a, b) => {
|
||||
return (
|
||||
a.variant.localeCompare(b.variant) || Range.isBefore(a.range, b.range)
|
||||
);
|
||||
});
|
||||
item.getChildren = () => Promise.resolve(children);
|
||||
item.description = `(${items.length}) ${item.description}`;
|
||||
// item.iconPath = children.every(c => c.variant === children[0].variant)
|
||||
// ? children[0].iconPath
|
||||
// : new vscode.ThemeIcon(
|
||||
// 'arrow-swap',
|
||||
// new vscode.ThemeColor('charts.purple')
|
||||
// );
|
||||
resourceItems.push(item);
|
||||
}
|
||||
resourceItems.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return resourceItems;
|
||||
}
|
||||
}
|
||||
|
||||
type BacklinkPanelTreeItem = ResourceTreeItem | ResourceRangeTreeItem;
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../../settings';
|
||||
import { isSome } from '../../utils';
|
||||
@@ -7,53 +6,54 @@ import { Foam } from '../../core/model/foam';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
let panel: vscode.WebviewPanel | undefined = undefined;
|
||||
vscode.workspace.onDidChangeConfiguration(event => {
|
||||
if (event.affectsConfiguration('foam.graph.style')) {
|
||||
const style = getGraphStyle();
|
||||
panel.webview.postMessage({
|
||||
type: 'didUpdateStyle',
|
||||
payload: style,
|
||||
});
|
||||
}
|
||||
});
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
let panel: vscode.WebviewPanel | undefined = undefined;
|
||||
vscode.workspace.onDidChangeConfiguration(event => {
|
||||
if (event.affectsConfiguration('foam.graph.style')) {
|
||||
const style = getGraphStyle();
|
||||
panel.webview.postMessage({
|
||||
type: 'didUpdateStyle',
|
||||
payload: style,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('foam-vscode.show-graph', async () => {
|
||||
if (panel) {
|
||||
const columnToShowIn = vscode.window.activeTextEditor
|
||||
? vscode.window.activeTextEditor.viewColumn
|
||||
: undefined;
|
||||
panel.reveal(columnToShowIn);
|
||||
} else {
|
||||
const foam = await foamPromise;
|
||||
panel = await createGraphPanel(foam, context);
|
||||
const onFoamChanged = _ => {
|
||||
updateGraph(panel, foam);
|
||||
};
|
||||
vscode.commands.registerCommand('foam-vscode.show-graph', async () => {
|
||||
if (panel) {
|
||||
const columnToShowIn = vscode.window.activeTextEditor
|
||||
? vscode.window.activeTextEditor.viewColumn
|
||||
: undefined;
|
||||
panel.reveal(columnToShowIn);
|
||||
} else {
|
||||
const foam = await foamPromise;
|
||||
panel = await createGraphPanel(foam, context);
|
||||
const onFoamChanged = _ => {
|
||||
updateGraph(panel, foam);
|
||||
};
|
||||
|
||||
const noteUpdatedListener = foam.graph.onDidUpdate(onFoamChanged);
|
||||
panel.onDidDispose(() => {
|
||||
noteUpdatedListener.dispose();
|
||||
panel = undefined;
|
||||
});
|
||||
const noteUpdatedListener = foam.graph.onDidUpdate(onFoamChanged);
|
||||
panel.onDidDispose(() => {
|
||||
noteUpdatedListener.dispose();
|
||||
panel = undefined;
|
||||
});
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e?.document?.uri?.scheme === 'file') {
|
||||
const note = foam.workspace.get(fromVsCodeUri(e.document.uri));
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
type: 'didSelectNote',
|
||||
payload: note.uri.path,
|
||||
});
|
||||
}
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e?.document?.uri?.scheme === 'file') {
|
||||
const note = foam.workspace.get(fromVsCodeUri(e.document.uri));
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
type: 'didSelectNote',
|
||||
payload: note.uri.path,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateGraph(panel: vscode.WebviewPanel, foam: Foam) {
|
||||
const graph = generateGraphData(foam);
|
||||
@@ -191,5 +191,3 @@ async function getWebviewContent(
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as backlinks } from './backlinks';
|
||||
export { default as backlinks } from './connections';
|
||||
export { default as dataviz } from './dataviz';
|
||||
export { default as orphans } from './orphans';
|
||||
export { default as placeholders } from './placeholders';
|
||||
export { default as tags } from './tags-explorer';
|
||||
export { default as notes } from './notes-explorer';
|
||||
|
||||
174
packages/foam-vscode/src/features/panels/notes-explorer.ts
Normal file
174
packages/foam-vscode/src/features/panels/notes-explorer.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
createBacklinkItemsForResource as createBacklinkTreeItemsForResource,
|
||||
expandAll,
|
||||
} from './utils/tree-view-utils';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { ContextMemento } from '../../utils/vsc-utils';
|
||||
import {
|
||||
FolderTreeItem,
|
||||
FolderTreeProvider,
|
||||
} from './utils/folder-tree-provider';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
const provider = new NotesProvider(
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
context.globalState
|
||||
);
|
||||
provider.refresh();
|
||||
const treeView = vscode.window.createTreeView<NotesTreeItems>(
|
||||
'foam-vscode.notes-explorer',
|
||||
{
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
canSelectMany: true,
|
||||
}
|
||||
);
|
||||
const revealTextEditorItem = async () => {
|
||||
const target = vscode.window.activeTextEditor?.document.uri;
|
||||
if (treeView.visible) {
|
||||
if (target) {
|
||||
const item = await findTreeItemByUri(provider, target);
|
||||
// Check if the item is already selected.
|
||||
// This check is needed because always calling reveal() will
|
||||
// cause the tree view to take the focus from the item when
|
||||
// browsing the notes explorer
|
||||
if (
|
||||
item &&
|
||||
!treeView.selection.find(
|
||||
i => i.resourceUri?.path === item.resourceUri.path
|
||||
)
|
||||
) {
|
||||
treeView.reveal(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
provider,
|
||||
foam.graph.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.expand-all`,
|
||||
(...args) =>
|
||||
expandAll(treeView, provider, node => node.contextValue === 'folder')
|
||||
),
|
||||
vscode.window.onDidChangeActiveTextEditor(revealTextEditorItem),
|
||||
treeView.onDidChangeVisibility(revealTextEditorItem)
|
||||
);
|
||||
}
|
||||
|
||||
export function findTreeItemByUri<I, T>(
|
||||
provider: FolderTreeProvider<I, T>,
|
||||
target: vscode.Uri
|
||||
) {
|
||||
const path = vscode.workspace.asRelativePath(
|
||||
target,
|
||||
vscode.workspace.workspaceFolders.length > 1
|
||||
);
|
||||
return provider.findTreeItemByPath(path.split('/'));
|
||||
}
|
||||
|
||||
export type NotesTreeItems =
|
||||
| ResourceTreeItem
|
||||
| FolderTreeItem<Resource>
|
||||
| ResourceRangeTreeItem;
|
||||
|
||||
export class NotesProvider extends FolderTreeProvider<
|
||||
NotesTreeItems,
|
||||
Resource
|
||||
> {
|
||||
public show = new ContextMemento<'all' | 'notes-only'>(
|
||||
this.state,
|
||||
`foam-vscode.views.notes-explorer.show`,
|
||||
'all'
|
||||
);
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
private state: vscode.Memento
|
||||
) {
|
||||
super();
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.show:all`,
|
||||
() => {
|
||||
this.show.update('all');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.show:notes`,
|
||||
() => {
|
||||
this.show.update('notes-only');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getValues() {
|
||||
return this.workspace.list();
|
||||
}
|
||||
|
||||
getFilterFn() {
|
||||
return this.show.get() === 'notes-only'
|
||||
? res => res.type !== 'image' && res.type !== 'attachment'
|
||||
: () => true;
|
||||
}
|
||||
|
||||
valueToPath(value: Resource) {
|
||||
const path = vscode.workspace.asRelativePath(
|
||||
value.uri.path,
|
||||
vscode.workspace.workspaceFolders.length > 1
|
||||
);
|
||||
const parts = path.split('/');
|
||||
return parts;
|
||||
}
|
||||
|
||||
createValueTreeItem(
|
||||
value: Resource,
|
||||
parent: FolderTreeItem<Resource>
|
||||
): NotesTreeItems {
|
||||
const item = new ResourceTreeItem(value, this.workspace, {
|
||||
parent,
|
||||
collapsibleState:
|
||||
this.graph.getBacklinks(value.uri).length > 0
|
||||
? vscode.TreeItemCollapsibleState.Collapsed
|
||||
: vscode.TreeItemCollapsibleState.None,
|
||||
});
|
||||
item.id = value.uri.toString();
|
||||
item.getChildren = async () => {
|
||||
const backlinks = await createBacklinkTreeItemsForResource(
|
||||
this.workspace,
|
||||
this.graph,
|
||||
item.uri
|
||||
);
|
||||
backlinks.forEach(item => {
|
||||
item.description = item.label;
|
||||
item.label = item.resource.title;
|
||||
});
|
||||
return backlinks;
|
||||
};
|
||||
item.description =
|
||||
value.uri.getName().toLocaleLowerCase() ===
|
||||
value.title.toLocaleLowerCase()
|
||||
? undefined
|
||||
: value.uri.getBasename();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,80 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getOrphansConfig } from '../../settings';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { GroupedResourcesTreeDataProvider } from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from '../../utils/tree-view-utils';
|
||||
import { getAttachmentsExtensions, getOrphansConfig } from '../../settings';
|
||||
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from './utils/tree-view-utils';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { imageExtensions } from '../../core/services/attachment-provider';
|
||||
|
||||
const EXCLUDE_TYPES = ['image', 'attachment'];
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getOrphansConfig().exclude
|
||||
);
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'orphans',
|
||||
'orphan',
|
||||
context.globalState,
|
||||
matcher,
|
||||
() =>
|
||||
foam.graph
|
||||
.getAllNodes()
|
||||
.filter(
|
||||
uri =>
|
||||
!EXCLUDE_TYPES.includes(foam.workspace.find(uri)?.type) &&
|
||||
foam.graph.getConnections(uri).length === 0
|
||||
),
|
||||
uri => {
|
||||
return uri.isPlaceholder()
|
||||
? new UriTreeItem(uri)
|
||||
: new ResourceTreeItem(foam.workspace.find(uri), foam.workspace);
|
||||
}
|
||||
);
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getOrphansConfig().exclude
|
||||
);
|
||||
const provider = new OrphanTreeView(
|
||||
context.globalState,
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
matcher
|
||||
);
|
||||
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.orphans', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
provider.refresh();
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.orphans', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
provider.refresh();
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.nValues})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
|
||||
provider,
|
||||
foam.graph.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
|
||||
provider,
|
||||
treeView,
|
||||
foam.graph.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
treeView.title = baseTitle + ` (${provider.nValues})`;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default feature;
|
||||
export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
|
||||
constructor(
|
||||
state: vscode.Memento,
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
matcher: IMatcher
|
||||
) {
|
||||
super('orphans', state, matcher);
|
||||
}
|
||||
|
||||
createValueTreeItem = uri => {
|
||||
return uri.isPlaceholder()
|
||||
? new UriTreeItem(uri)
|
||||
: new ResourceTreeItem(this.workspace.find(uri), this.workspace);
|
||||
};
|
||||
|
||||
getUris = () =>
|
||||
this.graph
|
||||
.getAllNodes()
|
||||
.filter(
|
||||
uri =>
|
||||
!EXCLUDE_TYPES.includes(this.workspace.find(uri)?.type) &&
|
||||
this.graph.getBacklinks(uri).length === 0 &&
|
||||
this.graph.getLinks(uri).filter(c => !isAttachment(c.target))
|
||||
.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
function isAttachment(uri: URI) {
|
||||
const ext = [...getAttachmentsExtensions(), ...imageExtensions];
|
||||
return ext.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
@@ -2,96 +2,85 @@ import * as vscode from 'vscode';
|
||||
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 } from './utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
UriTreeItem,
|
||||
createBacklinkItemsForResource,
|
||||
expandAll,
|
||||
groupRangesByResource,
|
||||
} from '../../utils/tree-view-utils';
|
||||
} from './utils/tree-view-utils';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { ContextMemento, fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FolderTreeItem } from './utils/folder-tree-provider';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getPlaceholdersConfig().exclude
|
||||
);
|
||||
const provider = new PlaceholderTreeView(
|
||||
context.globalState,
|
||||
foam,
|
||||
matcher
|
||||
);
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getPlaceholdersConfig().exclude
|
||||
);
|
||||
const provider = new PlaceholderTreeView(
|
||||
context.globalState,
|
||||
foam.workspace,
|
||||
foam.graph,
|
||||
matcher
|
||||
);
|
||||
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
provider.refresh();
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
provider.refresh();
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.nValues})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
provider,
|
||||
foam.graph.onDidUpdate(() => {
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
provider,
|
||||
foam.graph.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
}),
|
||||
provider.onDidChangeTreeData(() => {
|
||||
treeView.title = baseTitle + ` (${provider.nValues})`;
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.placeholders.expand-all`,
|
||||
() =>
|
||||
expandAll(
|
||||
treeView,
|
||||
provider,
|
||||
node =>
|
||||
node.contextValue === 'placeholder' ||
|
||||
node.contextValue === 'folder'
|
||||
)
|
||||
),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
if (provider.show.get() === 'for-current-file') {
|
||||
provider.refresh();
|
||||
}),
|
||||
provider.onDidChangeTreeData(() => {
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
if (provider.show.get() === 'for-current-file') {
|
||||
provider.refresh();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
|
||||
private graph: FoamGraph;
|
||||
public show = new ContextMemento<'all' | 'for-current-file'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.show`,
|
||||
'all'
|
||||
);
|
||||
|
||||
public constructor(state: vscode.Memento, foam: Foam, matcher: IMatcher) {
|
||||
super(
|
||||
'placeholders',
|
||||
'placeholder',
|
||||
state,
|
||||
matcher,
|
||||
() => {
|
||||
// we override computeResources below (as we can't use "this" here)
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
uri => {
|
||||
return new UriTreeItem(uri, {
|
||||
icon: 'link',
|
||||
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
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
this.graph = foam.graph;
|
||||
public constructor(
|
||||
state: vscode.Memento,
|
||||
private workspace: FoamWorkspace,
|
||||
private graph: FoamGraph,
|
||||
matcher: IMatcher
|
||||
) {
|
||||
super('placeholders', state, matcher);
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.show:all`,
|
||||
@@ -110,7 +99,28 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
|
||||
);
|
||||
}
|
||||
|
||||
computeResources = (): URI[] => {
|
||||
createValueTreeItem(uri: URI, parent: FolderTreeItem<URI>): UriTreeItem {
|
||||
const item = new UriTreeItem(uri, {
|
||||
parent,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
});
|
||||
item.contextValue = 'placeholder';
|
||||
item.id = uri.toString();
|
||||
item.getChildren = async () => {
|
||||
return groupRangesByResource(
|
||||
this.workspace,
|
||||
await createBacklinkItemsForResource(
|
||||
this.workspace,
|
||||
this.graph,
|
||||
uri,
|
||||
'link'
|
||||
)
|
||||
);
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
getUris(): URI[] {
|
||||
if (this.show.get() === 'for-current-file') {
|
||||
const currentFile = vscode.window.activeTextEditor?.document.uri;
|
||||
return currentFile
|
||||
@@ -121,7 +131,5 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
|
||||
: [];
|
||||
}
|
||||
return this.graph.getAllNodes().filter(uri => uri.isPlaceholder());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagsProvider } from './tags-explorer';
|
||||
import { FoamTags } from '../../core/model/tags';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { ResourceTreeItem } from '../../utils/tree-view-utils';
|
||||
import { ResourceTreeItem } from './utils/tree-view-utils';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
beforeAll(async () => {
|
||||
@@ -25,7 +25,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const treeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -43,12 +43,12 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
@@ -57,7 +57,7 @@ describe('Tags tree panel', () => {
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).toEqual('child');
|
||||
expect(child.label).toEqual('child');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
@@ -81,7 +81,7 @@ describe('Tags tree panel', () => {
|
||||
item => item instanceof TagItem
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).toEqual('parent');
|
||||
expect(parentTreeItems).toHaveLength(1);
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
@@ -91,12 +91,12 @@ describe('Tags tree panel', () => {
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof TagItem) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(['child', 'subchild']).toContain(child.title);
|
||||
expect(['child', 'subchild']).toContain(child.label);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(child.title).not.toEqual('parent');
|
||||
expect(child.label).not.toEqual('parent');
|
||||
}
|
||||
});
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles a parent and child tag in the same note', async () => {
|
||||
@@ -107,7 +107,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteC);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Tags tree panel', () => {
|
||||
item => item instanceof TagItem
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('main');
|
||||
expect(parentTagItem.label).toEqual('main');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(
|
||||
parentTagItem
|
||||
@@ -132,10 +132,10 @@ describe('Tags tree panel', () => {
|
||||
.filter(item => item instanceof TagItem)
|
||||
.forEach(item => {
|
||||
expect(['main/subtopic']).toContain(item.tag);
|
||||
expect(item.title).toEqual('subtopic');
|
||||
expect(item.label).toEqual('subtopic');
|
||||
});
|
||||
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
expect(childTreeItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles a tag with multiple levels of hierarchy - #1134', async () => {
|
||||
@@ -145,28 +145,26 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
const workspace = new FoamWorkspace().set(noteA);
|
||||
const foamTags = FoamTags.fromWorkspace(workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace);
|
||||
const provider = new TagsProvider(foamTags, workspace, false);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as TagItem[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTagItem.label).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');
|
||||
expect(childTreeItems).toHaveLength(1);
|
||||
expect(childTreeItems[0].label).toEqual('child');
|
||||
|
||||
const grandchildTreeItems = (await provider.getChildren(
|
||||
childTreeItems[1]
|
||||
childTreeItems[0]
|
||||
)) as TagItem[];
|
||||
|
||||
expect(grandchildTreeItems).toHaveLength(2);
|
||||
expect(grandchildTreeItems[0].label).toMatch(/^Search.*/);
|
||||
expect(grandchildTreeItems[1].label).toEqual('second');
|
||||
expect(grandchildTreeItems).toHaveLength(1);
|
||||
expect(grandchildTreeItems[0].label).toEqual('second');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,79 +1,167 @@
|
||||
import { URI } from '../../core/model/uri';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getNoteTooltip, isSome } from '../../utils';
|
||||
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';
|
||||
import {
|
||||
ResourceRangeTreeItem,
|
||||
ResourceTreeItem,
|
||||
expandAll,
|
||||
groupRangesByResource,
|
||||
} from '../../utils/tree-view-utils';
|
||||
} from './utils/tree-view-utils';
|
||||
import {
|
||||
Folder,
|
||||
FolderTreeItem,
|
||||
FolderTreeProvider,
|
||||
walk,
|
||||
} from './utils/folder-tree-provider';
|
||||
import {
|
||||
ContextMemento,
|
||||
MapBasedMemento,
|
||||
fromVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
|
||||
const TAG_SEPARATOR = '/';
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const provider = new TagsProvider(foam.tags, foam.workspace);
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.tags-explorer', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
foam.tags.onDidUpdate(() => {
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
const provider = new TagsProvider(foam.tags, foam.workspace);
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.tags-explorer', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
provider.refresh();
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
context.subscriptions.push(
|
||||
treeView,
|
||||
foam.tags.onDidUpdate(() => {
|
||||
provider.refresh();
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
if (provider.show.get() === 'for-current-file') {
|
||||
provider.refresh();
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${provider.providerId}.expand-all`,
|
||||
() =>
|
||||
expandAll(
|
||||
treeView,
|
||||
provider,
|
||||
node => node.contextValue === 'tag' || node.contextValue === 'folder'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<
|
||||
TagTreeItem | undefined | void
|
||||
> = new vscode.EventEmitter<TagTreeItem | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> =
|
||||
this._onDidChangeTreeData.event;
|
||||
export class TagsProvider extends FolderTreeProvider<TagTreeItem, string> {
|
||||
public providerId = 'tags-explorer';
|
||||
public show = new ContextMemento<'all' | 'for-current-file'>(
|
||||
new MapBasedMemento(),
|
||||
`foam-vscode.views.${this.providerId}.show`,
|
||||
'all'
|
||||
);
|
||||
public groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
new MapBasedMemento(),
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
|
||||
private tags: {
|
||||
tag: string;
|
||||
notes: URI[];
|
||||
}[];
|
||||
|
||||
private foamTags: FoamTags;
|
||||
|
||||
constructor(tags: FoamTags, private workspace: FoamWorkspace) {
|
||||
this.foamTags = tags;
|
||||
this.computeTags();
|
||||
constructor(
|
||||
private foamTags: FoamTags,
|
||||
private workspace: FoamWorkspace,
|
||||
registerCommands: boolean = true
|
||||
) {
|
||||
super();
|
||||
if (!registerCommands) {
|
||||
return;
|
||||
}
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.show:all`,
|
||||
() => {
|
||||
this.show.update('all');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.show:for-current-file`,
|
||||
() => {
|
||||
this.show.update('for-current-file');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:folder`,
|
||||
() => {
|
||||
this.groupBy.update('folder');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:off`,
|
||||
() => {
|
||||
this.groupBy.update('off');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.computeTags();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private computeTags() {
|
||||
this.tags = [...this.foamTags.tags]
|
||||
.map(([tag, notes]) => ({ tag, notes }))
|
||||
.sort((a, b) => a.tag.localeCompare(b.tag));
|
||||
super.refresh();
|
||||
}
|
||||
|
||||
getTreeItem(element: TagTreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
getValues(): string[] {
|
||||
if (this.show.get() === 'for-current-file') {
|
||||
const uriInEditor = vscode.window.activeTextEditor?.document.uri;
|
||||
const currentResource = this.workspace.find(fromVsCodeUri(uriInEditor));
|
||||
return currentResource?.tags.map(t => t.label) ?? [];
|
||||
}
|
||||
return Array.from(this.tags.values()).map(tag => tag.tag);
|
||||
}
|
||||
|
||||
valueToPath(value: string) {
|
||||
return this.groupBy.get() === 'off' ? [value] : value.split(TAG_SEPARATOR);
|
||||
}
|
||||
|
||||
private countResourcesInSubtree(node: Folder<string>) {
|
||||
const nChildren = walk(
|
||||
node,
|
||||
tag => this.foamTags.tags.get(tag)?.length ?? 0
|
||||
).reduce((acc, nResources) => acc + nResources, 0);
|
||||
return nChildren;
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
node: Folder<string>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<string>
|
||||
): FolderTreeItem<string> {
|
||||
const nChildren = this.countResourcesInSubtree(node);
|
||||
return new TagItem(node, nChildren, [], parent);
|
||||
}
|
||||
|
||||
createValueTreeItem(
|
||||
value: string,
|
||||
parent: FolderTreeItem<string>,
|
||||
node: Folder<string>
|
||||
): TagItem {
|
||||
const nChildren = this.countResourcesInSubtree(node);
|
||||
const resources = this.foamTags.tags.get(value) ?? [];
|
||||
return new TagItem(node, nChildren, resources, parent);
|
||||
}
|
||||
|
||||
async getChildren(element?: TagItem): Promise<TagTreeItem[]> {
|
||||
@@ -81,37 +169,11 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
const children = await (element as any).getChildren();
|
||||
return children;
|
||||
}
|
||||
const parentTag = element ? element.tag : '';
|
||||
const parentPrefix = element ? parentTag + TAG_SEPARATOR : '';
|
||||
|
||||
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 }>);
|
||||
|
||||
const subtags = Array.from(tagsAtThisLevel.values())
|
||||
.map(({ label, tagId, nResources }) => {
|
||||
const resources = this.foamTags.tags.get(tagId) ?? [];
|
||||
return new TagItem(tagId, label, nResources, resources);
|
||||
})
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
// Subtags are managed by the FolderTreeProvider
|
||||
const subtags = await super.getChildren(element);
|
||||
|
||||
// Compute the resources children
|
||||
const resourceTags: ResourceRangeTreeItem[] = (element?.notes ?? [])
|
||||
.map(uri => this.workspace.get(uri))
|
||||
.reduce((acc, note) => {
|
||||
@@ -120,46 +182,37 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
ResourceRangeTreeItem.createStandardItem(
|
||||
this.workspace,
|
||||
note,
|
||||
t.range
|
||||
t.range,
|
||||
'tag'
|
||||
)
|
||||
);
|
||||
return [...acc, ...items];
|
||||
}, []);
|
||||
const resources = (
|
||||
await groupRangesByResource(this.workspace, resourceTags)
|
||||
).map(item => {
|
||||
item.id = element.tag + ' / ' + item.uri.toString();
|
||||
return item;
|
||||
});
|
||||
|
||||
const resources = await groupRangesByResource(this.workspace, resourceTags);
|
||||
|
||||
return Promise.resolve(
|
||||
[element && new TagSearch(element.tag), ...subtags, ...resources].filter(
|
||||
Boolean
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
|
||||
if (
|
||||
item instanceof ResourceTreeItem ||
|
||||
item instanceof ResourceRangeTreeItem
|
||||
) {
|
||||
return item.resolveTreeItem();
|
||||
}
|
||||
return Promise.resolve(item);
|
||||
return [...subtags, ...resources];
|
||||
}
|
||||
}
|
||||
|
||||
type TagTreeItem =
|
||||
| TagItem
|
||||
| TagSearch
|
||||
| ResourceTreeItem
|
||||
| ResourceRangeTreeItem;
|
||||
type TagTreeItem = TagItem | ResourceTreeItem | ResourceRangeTreeItem;
|
||||
|
||||
export class TagItem extends FolderTreeItem<string> {
|
||||
public readonly tag: string;
|
||||
|
||||
export class TagItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly tag: string,
|
||||
public readonly title: string,
|
||||
public readonly node: Folder<string>,
|
||||
public readonly nResourcesInSubtree: number,
|
||||
public readonly notes: URI[]
|
||||
public readonly notes: URI[],
|
||||
public readonly parentElement?: FolderTreeItem<string>
|
||||
) {
|
||||
super(title, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
super(node, node.path.slice(-1)[0], parentElement);
|
||||
this.tag = node.path.join(TAG_SEPARATOR);
|
||||
this.id = this.tag;
|
||||
this.description = `${nResourcesInSubtree} reference${
|
||||
nResourcesInSubtree !== 1 ? 's' : ''
|
||||
}`;
|
||||
@@ -168,27 +221,3 @@ export class TagItem extends vscode.TreeItem {
|
||||
iconPath = new vscode.ThemeIcon('symbol-number');
|
||||
contextValue = 'tag';
|
||||
}
|
||||
|
||||
export class TagSearch extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: string) {
|
||||
super(`Search #${tag}`, vscode.TreeItemCollapsibleState.None);
|
||||
const searchString = `#${tag}`;
|
||||
this.tooltip = `Search ${searchString} in workspace`;
|
||||
this.command = {
|
||||
command: 'workbench.action.findInFiles',
|
||||
arguments: [
|
||||
{
|
||||
query: searchString,
|
||||
triggerSearch: true,
|
||||
matchWholeWord: true,
|
||||
isCaseSensitive: true,
|
||||
},
|
||||
],
|
||||
title: 'Search',
|
||||
};
|
||||
}
|
||||
|
||||
iconPath = new vscode.ThemeIcon('search');
|
||||
contextValue = 'tag-search';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { IDisposable } from '../../../core/common/lifecycle';
|
||||
|
||||
/**
|
||||
* This class is a wrapper around vscode.TreeDataProvider that adds a few
|
||||
* features:
|
||||
* - It adds a `refresh()` method that can be called to refresh the tree view
|
||||
* - It adds a `resolveTreeItem()` method that can be used to resolve the
|
||||
* tree item asynchronously. This is useful when the tree item needs to
|
||||
* fetch data from the file system or from the network.
|
||||
* - It adds a `dispose()` method that can be used to dispose of any resources
|
||||
* that the tree provider might be holding on to.
|
||||
*/
|
||||
export abstract class BaseTreeProvider<T>
|
||||
implements vscode.TreeDataProvider<T>, IDisposable
|
||||
{
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
|
||||
// prettier-ignore
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<T | undefined | void> = new vscode.EventEmitter<T | undefined | void>();
|
||||
// prettier-ignore
|
||||
readonly onDidChangeTreeData: vscode.Event<T | undefined | void> = this._onDidChangeTreeData.event;
|
||||
|
||||
abstract getChildren(element?: T): vscode.ProviderResult<T[]>;
|
||||
|
||||
getTreeItem(element: T) {
|
||||
return element;
|
||||
}
|
||||
|
||||
async resolveTreeItem(item: T): Promise<T> {
|
||||
if ((item as any)?.resolveTreeItem) {
|
||||
return (item as any).resolveTreeItem();
|
||||
}
|
||||
return Promise.resolve(item);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseTreeProvider } from './base-tree-provider';
|
||||
import { BaseTreeItem, ResourceTreeItem } from './tree-view-utils';
|
||||
|
||||
/**
|
||||
* A folder is a map of basenames to either folders or values (e.g. resources).
|
||||
*/
|
||||
export interface Folder<T> {
|
||||
children: {
|
||||
[basename: string]: Folder<T>;
|
||||
};
|
||||
value?: T;
|
||||
path: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A TreeItem that represents a folder.
|
||||
*/
|
||||
export class FolderTreeItem<T> extends vscode.TreeItem {
|
||||
collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
contextValue = 'folder';
|
||||
iconPath = new vscode.ThemeIcon('folder');
|
||||
|
||||
constructor(
|
||||
public node: Folder<T>,
|
||||
public name: string,
|
||||
public parentElement?: FolderTreeItem<T>
|
||||
) {
|
||||
super(name, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract class that can be used to create a tree view from a Folder object.
|
||||
* Its abstract methods must be implemented by the subclass to define the type of
|
||||
* the values in the folder, and how to filter them.
|
||||
*/
|
||||
export abstract class FolderTreeProvider<I, T> extends BaseTreeProvider<I> {
|
||||
private root: Folder<T>;
|
||||
public nValues = 0;
|
||||
|
||||
refresh(): void {
|
||||
const values = this.getValues();
|
||||
this.nValues = values.length;
|
||||
this.createTree(values, this.getFilterFn());
|
||||
super.refresh();
|
||||
}
|
||||
|
||||
getParent(element: I | FolderTreeItem<T>): vscode.ProviderResult<I> {
|
||||
if (element instanceof ResourceTreeItem) {
|
||||
return Promise.resolve(element.parent as I);
|
||||
}
|
||||
if (element instanceof FolderTreeItem) {
|
||||
return Promise.resolve(element.parentElement as any);
|
||||
}
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
node: Folder<T>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<T>
|
||||
) {
|
||||
return new FolderTreeItem<T>(node, name, parent);
|
||||
}
|
||||
|
||||
async getChildren(item?: I): Promise<I[]> {
|
||||
if (item instanceof BaseTreeItem) {
|
||||
return item.getChildren() as Promise<I[]>;
|
||||
}
|
||||
|
||||
const parent: Folder<T> = (item as any)?.node ?? this.root;
|
||||
|
||||
const children: vscode.TreeItem[] = Object.keys(parent?.children ?? []).map(
|
||||
name => {
|
||||
const node = parent.children[name];
|
||||
if (node.value != null) {
|
||||
return this.createValueTreeItem(node.value, undefined, node);
|
||||
} else {
|
||||
return this.createFolderTreeItem(
|
||||
node,
|
||||
name,
|
||||
item as unknown as FolderTreeItem<T>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return children.sort((a, b) => sortFolderTreeItems(a, b)) as any;
|
||||
}
|
||||
|
||||
createTree(values: T[], filterFn: (value: T) => boolean): Folder<T> {
|
||||
const root: Folder<T> = {
|
||||
children: {},
|
||||
path: [],
|
||||
};
|
||||
|
||||
for (const r of values) {
|
||||
const parts = this.valueToPath(r);
|
||||
let currentNode: Folder<T> = root;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!currentNode.children[part]) {
|
||||
if (index < parts.length - 1) {
|
||||
currentNode.children[part] = {
|
||||
children: {},
|
||||
path: parts.slice(0, index + 1),
|
||||
};
|
||||
} else if (filterFn(r)) {
|
||||
currentNode.children[part] = {
|
||||
children: {},
|
||||
path: parts.slice(0, index + 1),
|
||||
value: r,
|
||||
};
|
||||
}
|
||||
}
|
||||
currentNode = currentNode.children[part];
|
||||
});
|
||||
}
|
||||
|
||||
this.root = root;
|
||||
return root;
|
||||
}
|
||||
|
||||
getTreeItemsHierarchy(path: string[]): vscode.TreeItem[] {
|
||||
const treeItemsHierarchy: vscode.TreeItem[] = [];
|
||||
let currentNode: Folder<T> = this.root;
|
||||
|
||||
for (const part of path) {
|
||||
if (currentNode.children[part] !== undefined) {
|
||||
currentNode = currentNode.children[part] as Folder<T>;
|
||||
if (currentNode.value) {
|
||||
treeItemsHierarchy.push(
|
||||
this.createValueTreeItem(
|
||||
currentNode.value,
|
||||
treeItemsHierarchy[
|
||||
treeItemsHierarchy.length - 1
|
||||
] as FolderTreeItem<T>,
|
||||
currentNode
|
||||
)
|
||||
);
|
||||
} else {
|
||||
treeItemsHierarchy.push(
|
||||
new FolderTreeItem(
|
||||
currentNode,
|
||||
part,
|
||||
treeItemsHierarchy[
|
||||
treeItemsHierarchy.length - 1
|
||||
] as FolderTreeItem<T>
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If a part is not found in the tree structure, the given URI is not valid.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return treeItemsHierarchy;
|
||||
}
|
||||
|
||||
findTreeItemByPath(path: string[]): Promise<I> {
|
||||
const hierarchy = this.getTreeItemsHierarchy(path);
|
||||
return hierarchy.length > 0
|
||||
? Promise.resolve(hierarchy.pop())
|
||||
: Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that can be used to filter the values.
|
||||
* The difference between using this function vs not including the values
|
||||
* is that in this case, the tree will be created with all the folders
|
||||
* and subfolders, but the values will only be displayed if they pass
|
||||
* the filter.
|
||||
* By default it doesn't filter anything.
|
||||
*/
|
||||
getFilterFn(): (value: T) => boolean {
|
||||
return () => true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value to a path of strings that can be used to create a tree.
|
||||
*/
|
||||
abstract valueToPath(value: T);
|
||||
|
||||
/**
|
||||
* Returns all the values that should be displayed in the tree.
|
||||
*/
|
||||
abstract getValues(): T[];
|
||||
|
||||
/**
|
||||
* Creates a tree item for the given value.
|
||||
*/
|
||||
abstract createValueTreeItem(
|
||||
value: T,
|
||||
parent: FolderTreeItem<T>,
|
||||
node: Folder<T>
|
||||
): I;
|
||||
}
|
||||
|
||||
/**
|
||||
* walks the node and performs an action on each value
|
||||
* @returns
|
||||
*/
|
||||
export function walk<T, R>(node: Folder<T>, fn: (value: T) => R): R[] {
|
||||
const results: R[] = [];
|
||||
|
||||
function traverse(node: Folder<T>) {
|
||||
if (node.value) {
|
||||
results.push(fn(node.value));
|
||||
}
|
||||
|
||||
Object.values(node.children).forEach(child => {
|
||||
traverse(child);
|
||||
});
|
||||
}
|
||||
|
||||
traverse(node);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function sortFolderTreeItems(a: vscode.TreeItem, b: vscode.TreeItem): number {
|
||||
// Both a and b are FolderTreeItem instances
|
||||
if (a instanceof FolderTreeItem && b instanceof FolderTreeItem) {
|
||||
return a.label.toString().localeCompare(b.label.toString());
|
||||
}
|
||||
|
||||
// Only a is a FolderTreeItem instance
|
||||
if (a instanceof FolderTreeItem) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Only b is a FolderTreeItem instance
|
||||
if (b instanceof FolderTreeItem) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.label.toString().localeCompare(b.label.toString());
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { FoamWorkspace } from '../../../core/model/workspace';
|
||||
import {
|
||||
AlwaysIncludeMatcher,
|
||||
IMatcher,
|
||||
SubstringExcludeMatcher,
|
||||
} from '../../../core/services/datastore';
|
||||
import { createTestNote } from '../../../test/test-utils';
|
||||
import { ResourceTreeItem, UriTreeItem } from './tree-view-utils';
|
||||
import { randomString } from '../../../test/test-utils';
|
||||
import { MapBasedMemento } from '../../../utils/vsc-utils';
|
||||
import { URI } from '../../../core/model/uri';
|
||||
import { TreeItem } from 'vscode';
|
||||
import { GroupedResourcesTreeDataProvider } from './grouped-resources-tree-data-provider';
|
||||
|
||||
const testMatcher = new SubstringExcludeMatcher('path-exclude');
|
||||
|
||||
class TestProvider extends GroupedResourcesTreeDataProvider {
|
||||
constructor(
|
||||
matcher: IMatcher,
|
||||
private list: () => URI[],
|
||||
private create: (uri: URI) => TreeItem
|
||||
) {
|
||||
super(randomString(), new MapBasedMemento(), matcher);
|
||||
}
|
||||
getUris(): URI[] {
|
||||
return this.list();
|
||||
}
|
||||
createValueTreeItem(value: URI) {
|
||||
return this.create(value) as any;
|
||||
}
|
||||
}
|
||||
|
||||
describe('TestProvider', () => {
|
||||
const note1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
|
||||
const note2 = createTestNote({
|
||||
uri: '/path-bis/XYZ.md',
|
||||
title: 'XYZ',
|
||||
});
|
||||
const note3 = createTestNote({
|
||||
uri: '/path-bis/ABCDEFG.md',
|
||||
title: 'ABCDEFG',
|
||||
});
|
||||
const excludedNote = createTestNote({
|
||||
uri: '/path-exclude/HIJ.m',
|
||||
title: 'HIJ',
|
||||
});
|
||||
|
||||
const workspace = new FoamWorkspace()
|
||||
.set(note1)
|
||||
.set(note2)
|
||||
.set(note3)
|
||||
.set(excludedNote);
|
||||
|
||||
it('should return the grouped resources as a folder tree', async () => {
|
||||
const provider = new TestProvider(
|
||||
testMatcher,
|
||||
() => workspace.list().map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
);
|
||||
provider.groupBy.update('folder');
|
||||
provider.refresh();
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path',
|
||||
description: '(1)',
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-bis',
|
||||
description: '(2)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should return the grouped resources in a directory', async () => {
|
||||
const provider = new TestProvider(
|
||||
testMatcher,
|
||||
() => workspace.list().map(r => r.uri),
|
||||
uri => new ResourceTreeItem(workspace.get(uri), workspace)
|
||||
);
|
||||
provider.groupBy.update('folder');
|
||||
provider.refresh();
|
||||
const paths = await provider.getChildren();
|
||||
const directory = paths[0];
|
||||
expect(directory).toMatchObject({
|
||||
label: '/path',
|
||||
});
|
||||
const result = await provider.getChildren(directory);
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: 'ABC',
|
||||
description: '/path/ABC.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should return the flattened resources', async () => {
|
||||
const provider = new TestProvider(
|
||||
testMatcher,
|
||||
() => workspace.list().map(r => r.uri),
|
||||
uri => new ResourceTreeItem(workspace.get(uri), workspace)
|
||||
);
|
||||
provider.groupBy.update('off');
|
||||
provider.refresh();
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: note1.title,
|
||||
description: '/path/ABC.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: note3.title,
|
||||
description: '/path-bis/ABCDEFG.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
{
|
||||
collapsibleState: 0,
|
||||
label: note2.title,
|
||||
description: '/path-bis/XYZ.md',
|
||||
command: { command: 'vscode.open' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should return the grouped resources without exclusion', async () => {
|
||||
const provider = new TestProvider(
|
||||
new AlwaysIncludeMatcher(),
|
||||
() => workspace.list().map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
);
|
||||
provider.groupBy.update('folder');
|
||||
provider.refresh();
|
||||
const result = await provider.getChildren();
|
||||
expect(result.length).toEqual(3);
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path',
|
||||
description: '(1)',
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-bis',
|
||||
description: '(2)',
|
||||
},
|
||||
{
|
||||
collapsibleState: 1,
|
||||
label: '/path-exclude',
|
||||
description: '(1)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../../../core/model/uri';
|
||||
import { IMatcher } from '../../../core/services/datastore';
|
||||
import { UriTreeItem } from './tree-view-utils';
|
||||
import { ContextMemento } from '../../../utils/vsc-utils';
|
||||
import {
|
||||
FolderTreeItem,
|
||||
FolderTreeProvider,
|
||||
Folder,
|
||||
} from './folder-tree-provider';
|
||||
|
||||
type GroupedResourceTreeItem = UriTreeItem | FolderTreeItem<URI>;
|
||||
|
||||
/**
|
||||
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
|
||||
* iterate over each Resource in the FoamWorkspace, call the provided filter predicate, and
|
||||
* display the Resources.
|
||||
*
|
||||
* **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file:
|
||||
* ```
|
||||
* foam-vscode.views.${providerId}.group-by-folder
|
||||
* foam-vscode.views.${providerId}.group-off
|
||||
* ```
|
||||
* Where `providerId` is the same string provided to the constructor.
|
||||
* @export
|
||||
* @class GroupedResourcesTreeDataProvider
|
||||
* @implements {vscode.TreeDataProvider<GroupedResourceTreeItem>}
|
||||
*/
|
||||
export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvider<
|
||||
GroupedResourceTreeItem,
|
||||
URI
|
||||
> {
|
||||
public groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates an instance of GroupedResourcesTreeDataProvider.
|
||||
* **NOTE**: In order for this provider to correctly function, you must define the following command in the package.json file:
|
||||
* ```
|
||||
* foam-vscode.views.${this.providerId}.group-by:folder
|
||||
* foam-vscode.views.${this.providerId}.group-by:off
|
||||
* ```
|
||||
* Where `providerId` is the same string provided to this constructor.
|
||||
*
|
||||
* @param {string} providerId A **unique** providerId, this will be used to generate necessary commands within the provider.
|
||||
* @param {vscode.Memento} state The state to use for persisting the panel settings.
|
||||
* @param {IMatcher} matcher The matcher to use for filtering the uris.
|
||||
* @memberof GroupedResourcesTreeDataProvider
|
||||
*/
|
||||
constructor(
|
||||
protected providerId: string,
|
||||
protected state: vscode.Memento,
|
||||
private matcher: IMatcher
|
||||
) {
|
||||
super();
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:folder`,
|
||||
() => {
|
||||
this.groupBy.update('folder');
|
||||
this.refresh();
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:off`,
|
||||
() => {
|
||||
this.groupBy.update('off');
|
||||
this.refresh();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
valueToPath(value: URI) {
|
||||
const p = vscode.workspace.asRelativePath(
|
||||
value.path,
|
||||
vscode.workspace.workspaceFolders.length > 1
|
||||
);
|
||||
if (this.groupBy.get() === 'folder') {
|
||||
const { dir, base } = path.parse(p);
|
||||
return [dir, base];
|
||||
}
|
||||
return [p];
|
||||
}
|
||||
|
||||
getValues(): URI[] {
|
||||
const uris = this.getUris();
|
||||
return uris.filter(uri => this.matcher.isMatch(uri));
|
||||
}
|
||||
|
||||
createFolderTreeItem(
|
||||
node: Folder<URI>,
|
||||
name: string,
|
||||
parent: FolderTreeItem<URI>
|
||||
) {
|
||||
const item = super.createFolderTreeItem(node, name, parent);
|
||||
item.label = item.label || '(Not Created)';
|
||||
item.description = `(${Object.keys(node.children).length})`;
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URIs before the filtering by the matcher is applied
|
||||
*/
|
||||
abstract getUris(): URI[];
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { groupBy } from 'lodash';
|
||||
import { Resource } from '../../../core/model/note';
|
||||
import { toVsCodeUri } from '../../../utils/vsc-utils';
|
||||
import { Range } from '../../../core/model/range';
|
||||
import { URI } from '../../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../../core/model/workspace';
|
||||
import { getNoteTooltip } from '../../../utils';
|
||||
import { isSome } from '../../../core/utils';
|
||||
import { getBlockFor } from '../../../core/services/markdown-parser';
|
||||
import { Connection, FoamGraph } from '../../../core/model/graph';
|
||||
import { Logger } from '../../../core/utils/log';
|
||||
|
||||
export class BaseTreeItem extends vscode.TreeItem {
|
||||
resolveTreeItem(): Promise<vscode.TreeItem> {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
getChildren(): Promise<vscode.TreeItem[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
export class UriTreeItem extends BaseTreeItem {
|
||||
public parent?: vscode.TreeItem;
|
||||
|
||||
constructor(
|
||||
public readonly uri: URI,
|
||||
options: {
|
||||
collapsibleState?: vscode.TreeItemCollapsibleState;
|
||||
title?: string;
|
||||
parent?: vscode.TreeItem;
|
||||
} = {}
|
||||
) {
|
||||
super(options?.title ?? uri.getName(), options.collapsibleState);
|
||||
this.parent = options.parent;
|
||||
this.description = uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(uri))?.uri.path,
|
||||
''
|
||||
);
|
||||
this.iconPath = new vscode.ThemeIcon('link');
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceTreeItem extends UriTreeItem {
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
private readonly workspace: FoamWorkspace,
|
||||
options: {
|
||||
collapsibleState?: vscode.TreeItemCollapsibleState;
|
||||
parent?: vscode.TreeItem;
|
||||
} = {}
|
||||
) {
|
||||
super(resource.uri, {
|
||||
title: resource.title,
|
||||
collapsibleState: options.collapsibleState,
|
||||
parent: options.parent,
|
||||
});
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [toVsCodeUri(resource.uri)],
|
||||
title: 'Go to location',
|
||||
};
|
||||
this.resourceUri = toVsCodeUri(resource.uri);
|
||||
this.iconPath = vscode.ThemeIcon.File;
|
||||
this.contextValue = 'foam.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 BaseTreeItem {
|
||||
public value: any;
|
||||
constructor(
|
||||
public label: string,
|
||||
public variant: string,
|
||||
public readonly resource: Resource,
|
||||
public readonly range: Range,
|
||||
public readonly workspace: FoamWorkspace
|
||||
) {
|
||||
super(label, vscode.TreeItemCollapsibleState.None);
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [toVsCodeUri(resource.uri), { selection: range }],
|
||||
title: 'Go to location',
|
||||
};
|
||||
}
|
||||
|
||||
async resolveTreeItem(): Promise<ResourceRangeTreeItem> {
|
||||
const markdown =
|
||||
(await this.workspace.readAsMarkdown(this.resource.uri)) ?? '';
|
||||
let { block, nLines } = getBlockFor(markdown, this.range.start);
|
||||
// Long blocks need to be interrupted or they won't display in hover preview
|
||||
// We keep the extra lines so that the count in the preview is correct
|
||||
if (nLines > 15) {
|
||||
let tmp = block.split('\n');
|
||||
tmp.splice(15, 1, '\n'); // replace a line with a blank line to interrupt the block
|
||||
block = tmp.join('\n');
|
||||
}
|
||||
const tooltip = getNoteTooltip(block ?? this.label ?? '');
|
||||
this.tooltip = tooltip;
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
static icons = {
|
||||
backlink: new vscode.ThemeIcon(
|
||||
'arrow-left',
|
||||
new vscode.ThemeColor('charts.purple')
|
||||
),
|
||||
link: new vscode.ThemeIcon(
|
||||
'arrow-right',
|
||||
new vscode.ThemeColor('charts.purple')
|
||||
),
|
||||
tag: new vscode.ThemeIcon(
|
||||
'symbol-number',
|
||||
new vscode.ThemeColor('charts.purple')
|
||||
),
|
||||
};
|
||||
static async createStandardItem(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
range: Range,
|
||||
variant: 'backlink' | 'tag' | 'link'
|
||||
): Promise<ResourceRangeTreeItem> {
|
||||
const markdown = (await workspace.readAsMarkdown(resource.uri)) ?? '';
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
const line = lines[range.start.line];
|
||||
const start = Math.max(0, range.start.character - 15);
|
||||
const ellipsis = start === 0 ? '' : '...';
|
||||
|
||||
const label = line
|
||||
? `${range.start.line + 1}: ${ellipsis}${line.slice(start, start + 300)}`
|
||||
: Range.toString(range);
|
||||
|
||||
const item = new ResourceRangeTreeItem(
|
||||
label,
|
||||
variant,
|
||||
resource,
|
||||
range,
|
||||
workspace
|
||||
);
|
||||
item.iconPath = ResourceRangeTreeItem.icons[variant];
|
||||
|
||||
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,
|
||||
});
|
||||
const children = items.sort((a, b) => Range.isBefore(a.range, b.range));
|
||||
resourceItem.getChildren = () => Promise.resolve(children);
|
||||
resourceItem.description = `(${items.length}) ${resourceItem.description}`;
|
||||
resourceItem.command = children[0].command;
|
||||
return resourceItem;
|
||||
});
|
||||
resourceItems.sort((a, b) => Resource.sortByTitle(a.resource, b.resource));
|
||||
return resourceItems;
|
||||
};
|
||||
|
||||
export function createBacklinkItemsForResource(
|
||||
workspace: FoamWorkspace,
|
||||
graph: FoamGraph,
|
||||
uri: URI,
|
||||
variant: 'backlink' | 'link' = 'backlink'
|
||||
) {
|
||||
const connections = graph
|
||||
.getConnections(uri)
|
||||
.filter(c => c.target.asPlain().isEqual(uri));
|
||||
|
||||
const backlinkItems = connections.map(async c =>
|
||||
ResourceRangeTreeItem.createStandardItem(
|
||||
workspace,
|
||||
workspace.get(c.source),
|
||||
c.link.range,
|
||||
variant
|
||||
)
|
||||
);
|
||||
return Promise.all(backlinkItems);
|
||||
}
|
||||
|
||||
export function createConnectionItemsForResource(
|
||||
workspace: FoamWorkspace,
|
||||
graph: FoamGraph,
|
||||
uri: URI,
|
||||
filter: (c: Connection) => boolean = () => true
|
||||
) {
|
||||
const connections = graph.getConnections(uri).filter(c => filter(c));
|
||||
|
||||
const backlinkItems = connections.map(async c => {
|
||||
const item = await ResourceRangeTreeItem.createStandardItem(
|
||||
workspace,
|
||||
workspace.get(c.source),
|
||||
c.link.range,
|
||||
c.source.asPlain().isEqual(uri) ? 'link' : 'backlink'
|
||||
);
|
||||
item.value = c;
|
||||
return item;
|
||||
});
|
||||
return Promise.all(backlinkItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a node and its children in a tree view that match a given predicate
|
||||
*
|
||||
* @param treeView - The tree view to expand nodes in
|
||||
* @param provider - The tree data provider for the view
|
||||
* @param element - The element to expand
|
||||
* @param when - A function that returns true if the node should be expanded
|
||||
*/
|
||||
export async function expandNode<T>(
|
||||
treeView: vscode.TreeView<any>,
|
||||
provider: vscode.TreeDataProvider<T>,
|
||||
element: T,
|
||||
when: (element: T) => boolean
|
||||
) {
|
||||
try {
|
||||
if (when(element)) {
|
||||
await treeView.reveal(element, {
|
||||
select: false,
|
||||
focus: false,
|
||||
expand: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const obj = element as any;
|
||||
const label = obj.label ?? obj.toString();
|
||||
Logger.warn(
|
||||
`Could not expand element: ${label}. Try setting the ID property of the TreeItem`
|
||||
);
|
||||
}
|
||||
|
||||
const children = await provider.getChildren(element);
|
||||
for (const child of children) {
|
||||
await expandNode(treeView, provider, child, when);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all items in a tree view that match a given predicate
|
||||
*
|
||||
* @param treeView - The tree view to expand items in
|
||||
* @param provider - The tree data provider for the view
|
||||
* @param when - A function that returns true if the node should be expanded
|
||||
*/
|
||||
export async function expandAll<T>(
|
||||
treeView: vscode.TreeView<T>,
|
||||
provider: vscode.TreeDataProvider<T>,
|
||||
when: (element: T) => boolean = () => true
|
||||
) {
|
||||
const elements = await provider.getChildren(undefined);
|
||||
for (const element of elements) {
|
||||
await expandNode(treeView, provider, element, when);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,30 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { default as markdownItFoamTags } from './tag-highlight';
|
||||
import { default as markdownItWikilinkNavigation } from './wikilink-navigation';
|
||||
import { default as markdownItRemoveLinkReferences } from './remove-wikilink-references';
|
||||
import { default as markdownItWikilinkEmbed } from './wikilink-embed';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
_context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) => {
|
||||
return [
|
||||
markdownItWikilinkEmbed,
|
||||
markdownItFoamTags,
|
||||
markdownItWikilinkNavigation,
|
||||
markdownItRemoveLinkReferences,
|
||||
].reduce(
|
||||
(acc, extension) =>
|
||||
extension(acc, foam.workspace, foam.services.parser),
|
||||
md
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
export default feature;
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) => {
|
||||
return [
|
||||
markdownItWikilinkEmbed,
|
||||
markdownItFoamTags,
|
||||
markdownItWikilinkNavigation,
|
||||
markdownItRemoveLinkReferences,
|
||||
].reduce(
|
||||
(acc, extension) =>
|
||||
extension(acc, foam.workspace, foam.services.parser),
|
||||
md
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,21 +8,21 @@ import {
|
||||
} from '../../test/test-utils-vscode';
|
||||
import {
|
||||
default as markdownItWikilinkEmbed,
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
} from './wikilink-embed';
|
||||
|
||||
const parser = createMarkdownParser();
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
it('should render an included note in flat mode', async () => {
|
||||
it('should render an included note in full inline mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -40,16 +40,16 @@ describe('Displaying included notes in preview', () => {
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included note in container mode', async () => {
|
||||
it('should render an included note in full card mode', async () => {
|
||||
const note = await createFile('This is the text of note A', [
|
||||
'preview',
|
||||
'note-a.md',
|
||||
]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('Displaying included notes in preview', () => {
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section', async () => {
|
||||
it('should render an included section in full inline mode', async () => {
|
||||
// here we use createFile as the test note doesn't fill in
|
||||
// all the metadata we need
|
||||
const note = await createFile(
|
||||
@@ -83,8 +83,8 @@ This is the third section of note E
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
@@ -102,7 +102,7 @@ This is the third section of note E
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should render an included section in container mode', async () => {
|
||||
it('should render an included section in full card mode', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
@@ -120,8 +120,8 @@ This is the third section of note E
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
@@ -138,6 +138,220 @@ This is the third section of note E
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the title of a note in content inline mode', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Title
|
||||
## Section 1
|
||||
|
||||
This is the first section of note E`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><h2>Section 1</h2>
|
||||
<p>This is the first section of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the title of a note in content card mode', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
## Section 1
|
||||
|
||||
This is the first section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-e.md]]`);
|
||||
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('Section 1');
|
||||
expect(res).toContain('This is the first section of note E');
|
||||
expect(res).not.toContain('Title');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the section title, but still render subsection titles in content inline mode', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
|
||||
|
||||
## Section 1
|
||||
This is the first section of note E
|
||||
|
||||
### Subsection a
|
||||
This is the first subsection of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 1]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the first section of note E</p>
|
||||
<h3>Subsection a</h3>
|
||||
<p>This is the first subsection of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should not render the subsection title in content mode if you link to it and regardless of its level', async () => {
|
||||
const note = await createFile(
|
||||
`# Title
|
||||
## Section 1
|
||||
This is the first section of note E
|
||||
|
||||
### Subsection a
|
||||
This is the first subsection of note E`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'content-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Subsection a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the first subsection of note E</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should allow a note embedding type to be overridden if a modifier is passed in', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note E
|
||||
|
||||
# Section 3
|
||||
This is the third section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
content![[note-e#Section 2]]
|
||||
|
||||
full![[note-e#Section 3]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the second section of note E</p>
|
||||
</p>
|
||||
<p><h1>Section 3</h1>
|
||||
<p>This is the third section of note E</p>
|
||||
</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should allow a note embedding type to be overridden if two modifiers are passed in', async () => {
|
||||
const note = await createFile(
|
||||
`
|
||||
# Section 1
|
||||
This is the first section of note E
|
||||
|
||||
# Section 2
|
||||
This is the second section of note E
|
||||
`,
|
||||
['note-e.md']
|
||||
);
|
||||
const parser = createMarkdownParser([]);
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const res = md.render(`This is the root node.
|
||||
|
||||
content-card![[note-e#Section 2]]`);
|
||||
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('This is the second section of note E');
|
||||
expect(res).not.toContain('Section 2');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
});
|
||||
|
||||
it('should fallback to the bare text when the note is not found', () => {
|
||||
const md = markdownItWikilinkEmbed(
|
||||
MarkdownIt(),
|
||||
@@ -150,6 +364,27 @@ This is the third section of note E
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the bare text for an embedded note that is embedding a note that is not found', async () => {
|
||||
const note = await createFile(
|
||||
'This is the text of note A which includes ![[does-not-exist]]',
|
||||
['note.md']
|
||||
);
|
||||
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-inline',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
expect(md.render(`This is the root node. ![[note]]`)).toMatch(
|
||||
`<p>This is the root node. <p>This is the text of note A which includes ![[does-not-exist]]</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should display a warning in case of cyclical inclusions', async () => {
|
||||
const noteA = await createFile(
|
||||
'This is the text of note A which includes ![[note-b]]',
|
||||
@@ -162,14 +397,21 @@ 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, 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');
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_TYPE,
|
||||
'full-card',
|
||||
() => {
|
||||
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
|
||||
const res = md.render(noteBText);
|
||||
|
||||
deleteFile(noteA);
|
||||
deleteFile(noteB);
|
||||
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');
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(noteA);
|
||||
await deleteFile(noteB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
WIKILINK_EMBED_REGEX,
|
||||
WIKILINK_EMBED_REGEX_GROUPS,
|
||||
retrieveNoteConfig,
|
||||
} from './wikilink-embed';
|
||||
import * as config from '../../services/config';
|
||||
|
||||
describe('Wikilink Note Embedding', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Wikilink Parsing', () => {
|
||||
it('should match a wikilink item including a modifier and wikilink', () => {
|
||||
// no configuration
|
||||
expect('![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
|
||||
// one of the configurations
|
||||
expect('full![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
|
||||
// any combination of configurations
|
||||
expect('full-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('full-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
expect('content-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
|
||||
});
|
||||
|
||||
it('should only match the wikilink if there are unrecognized keywords', () => {
|
||||
const match1 = 'random-word![[note-a]]'.match(WIKILINK_EMBED_REGEX);
|
||||
expect(match1[0]).toEqual('![[note-a]]');
|
||||
expect(match1[1]).toEqual('![[note-a]]');
|
||||
|
||||
const match2 = 'foo![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX);
|
||||
expect(match2[0]).toEqual('![[note-a#section 1]]');
|
||||
expect(match2[1]).toEqual('![[note-a#section 1]]');
|
||||
});
|
||||
|
||||
it('should group the wikilink into modifier and wikilink', () => {
|
||||
const match1 = 'content![[note-a]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
|
||||
expect(match1[0]).toEqual('content![[note-a]]');
|
||||
expect(match1[1]).toEqual('content');
|
||||
expect(match1[2]).toEqual('note-a');
|
||||
|
||||
const match2 = 'full-inline![[note-a#section 1]]'.match(
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
expect(match2[0]).toEqual('full-inline![[note-a#section 1]]');
|
||||
expect(match2[1]).toEqual('full-inline');
|
||||
expect(match2[2]).toEqual('note-a#section 1');
|
||||
|
||||
const match3 = '![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
|
||||
expect(match3[0]).toEqual('![[note-a#section 1]]');
|
||||
expect(match3[1]).toEqual(undefined);
|
||||
expect(match3[2]).toEqual('note-a#section 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Parsing', () => {
|
||||
it('should use preview.embedNoteType if an explicit modifier is not passed in', () => {
|
||||
jest
|
||||
.spyOn(config, 'getFoamVsCodeConfig')
|
||||
.mockReturnValueOnce('full-card');
|
||||
|
||||
const { noteScope, noteStyle } = retrieveNoteConfig(undefined);
|
||||
expect(noteScope).toEqual('full');
|
||||
expect(noteStyle).toEqual('card');
|
||||
});
|
||||
|
||||
it('should use explicit modifier over user settings if passed in', () => {
|
||||
jest
|
||||
.spyOn(config, 'getFoamVsCodeConfig')
|
||||
.mockReturnValueOnce('full-inline')
|
||||
.mockReturnValueOnce('full-inline')
|
||||
.mockReturnValueOnce('full-inline');
|
||||
|
||||
let { noteScope, noteStyle } = retrieveNoteConfig('content-card');
|
||||
expect(noteScope).toEqual('content');
|
||||
expect(noteStyle).toEqual('card');
|
||||
|
||||
({ noteScope, noteStyle } = retrieveNoteConfig('content'));
|
||||
expect(noteScope).toEqual('content');
|
||||
expect(noteStyle).toEqual('inline');
|
||||
|
||||
({ noteScope, noteStyle } = retrieveNoteConfig('card'));
|
||||
expect(noteScope).toEqual('full');
|
||||
expect(noteStyle).toEqual('card');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { workspace as vsWorkspace } from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isSome } from '../../utils';
|
||||
import { isSome, isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { Resource, ResourceParser } from '../../core/model/note';
|
||||
@@ -14,7 +14,14 @@ 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';
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
// we need another regex because md.use(regex, replace) only permits capturing one group
|
||||
// so we capture the entire possible wikilink item (ex. content-card![[note]]) using WIKILINK_EMBED_REGEX and then
|
||||
// use WIKILINK_EMBED_REGEX_GROUPER to parse it into the modifier(content-card) and the wikilink(note)
|
||||
export const WIKILINK_EMBED_REGEX_GROUPS =
|
||||
/((?:\w+)|(?:(?:\w+)-(?:\w+)))?!\[\[([^[\]]+?)\]\]/;
|
||||
export const CONFIG_EMBED_NOTE_TYPE = 'preview.embedNoteType';
|
||||
const refsStack: string[] = [];
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
@@ -24,9 +31,13 @@ export const markdownItWikilinkEmbed = (
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
regex: /!\[\[([^[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
try {
|
||||
const [_, noteEmbedModifier, wikilink] = wikilinkItem.match(
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
|
||||
const includedNote = workspace.find(wikilink);
|
||||
|
||||
if (!includedNote) {
|
||||
@@ -45,27 +56,29 @@ export const markdownItWikilinkEmbed = (
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
}
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
let html: string;
|
||||
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
let noteText = readFileSync(includedNote.uri.toFsPath()).toString();
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = noteText.split('\n');
|
||||
noteText = rows
|
||||
.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;
|
||||
const { noteScope, noteStyle } =
|
||||
retrieveNoteConfig(noteEmbedModifier);
|
||||
|
||||
const extractor: EmbedNoteExtractor =
|
||||
noteScope === 'full'
|
||||
? fullExtractor
|
||||
: noteScope === 'content'
|
||||
? contentExtractor
|
||||
: fullExtractor;
|
||||
|
||||
const formatter: EmbedNoteFormatter =
|
||||
noteStyle === 'card'
|
||||
? cardFormatter
|
||||
: noteStyle === 'inline'
|
||||
? inlineFormatter
|
||||
: cardFormatter;
|
||||
|
||||
content = extractor(includedNote, parser, workspace);
|
||||
html = formatter(content, md);
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
@@ -74,19 +87,20 @@ export const markdownItWikilinkEmbed = (
|
||||
${md.renderInline('[[' + wikilink + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`})`
|
||||
)}</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
`Error while including ${wikilinkItem} into the current document of the Preview panel`,
|
||||
e
|
||||
);
|
||||
return '';
|
||||
@@ -108,6 +122,11 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
.map(link => {
|
||||
const info = MarkdownLink.analyzeLink(link);
|
||||
const resource = workspace.find(info.target);
|
||||
// embedded notes that aren't created are still collected
|
||||
// return null so it can be filtered in the next step
|
||||
if (isNone(resource)) {
|
||||
return null;
|
||||
}
|
||||
const pathFromRoot = vsWorkspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
);
|
||||
@@ -115,6 +134,7 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
target: pathFromRoot,
|
||||
});
|
||||
})
|
||||
.filter(linkEdits => !isNone(linkEdits))
|
||||
.sort((a, b) => Position.compareTo(b.range.start, a.range.start));
|
||||
const text = edits.reduce(
|
||||
(text, edit) => TextEdit.apply(text, edit),
|
||||
@@ -123,4 +143,91 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
return text;
|
||||
}
|
||||
|
||||
export function retrieveNoteConfig(explicitModifier: string | undefined): {
|
||||
noteScope: string;
|
||||
noteStyle: string;
|
||||
} {
|
||||
let config = getFoamVsCodeConfig<string>(CONFIG_EMBED_NOTE_TYPE); // ex. full-inline
|
||||
let [noteScope, noteStyle] = config.split('-');
|
||||
|
||||
// an explicit modifier will always override corresponding user setting
|
||||
if (explicitModifier !== undefined) {
|
||||
if (['full', 'content'].includes(explicitModifier)) {
|
||||
noteScope = explicitModifier;
|
||||
} else if (['card', 'inline'].includes(explicitModifier)) {
|
||||
noteStyle = explicitModifier;
|
||||
} else {
|
||||
[noteScope, noteStyle] = explicitModifier.split('-');
|
||||
}
|
||||
}
|
||||
return { noteScope, noteStyle };
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of function that gets the desired content of the note
|
||||
*/
|
||||
export type EmbedNoteExtractor = (
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => string;
|
||||
|
||||
function fullExtractor(
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
): string {
|
||||
let noteText = readFileSync(note.uri.toFsPath()).toString();
|
||||
const section = Resource.findSection(note, note.uri.fragment);
|
||||
if (isSome(section)) {
|
||||
const rows = noteText.split('\n');
|
||||
noteText = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
function contentExtractor(
|
||||
note: Resource,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
): string {
|
||||
let noteText = readFileSync(note.uri.toFsPath()).toString();
|
||||
let section = Resource.findSection(note, note.uri.fragment);
|
||||
if (!note.uri.fragment) {
|
||||
// if there's no fragment(section), the wikilink is linking to the entire note,
|
||||
// in which case we need to remove the title. We could just use rows.shift()
|
||||
// but should the note start with blank lines, it will only remove the first blank line
|
||||
// leaving the title
|
||||
// A better way is to find where the actual title starts by assuming it's at section[0]
|
||||
// then we treat it as the same case as link to a section
|
||||
section = note.sections.length ? note.sections[0] : null;
|
||||
}
|
||||
let rows = noteText.split('\n');
|
||||
if (isSome(section)) {
|
||||
rows = rows.slice(section.range.start.line, section.range.end.line);
|
||||
}
|
||||
rows.shift();
|
||||
noteText = rows.join('\n');
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of function that renders note content with the desired style in html
|
||||
*/
|
||||
export type EmbedNoteFormatter = (content: string, md: markdownit) => string;
|
||||
|
||||
function cardFormatter(content: string, md: markdownit): string {
|
||||
return md.render(
|
||||
`<div class="embed-container-note">${md.render(content)}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function inlineFormatter(content: string, md: markdownit): string {
|
||||
return md.render(content);
|
||||
}
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
|
||||
@@ -52,6 +52,15 @@ describe('Link generation in preview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a link to a section within the note', () => {
|
||||
expect(md.render(`[[#sec]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='sec' href='#sec' data-href='#sec'>#sec</a></p>\n`
|
||||
);
|
||||
expect(md.render(`[[#Section Name]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='Section Name' href='#section-name' data-href='#section-name'>#Section Name</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a link to a note with a specific section', () => {
|
||||
expect(md.render(`[[note-b#sec2]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='My second note#sec2' href='/path2/to/note-b.md#sec2' data-href='/path2/to/note-b.md#sec2'>note-b#sec2</a></p>\n`
|
||||
|
||||
@@ -9,6 +9,7 @@ import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { toSlug } from '../../utils/slug';
|
||||
|
||||
export const markdownItWikilinkNavigation = (
|
||||
md: markdownit,
|
||||
@@ -26,18 +27,29 @@ export const markdownItWikilinkNavigation = (
|
||||
isEmbed: false,
|
||||
});
|
||||
const formattedSection = section ? `#${section}` : '';
|
||||
const linkSection = section ? `#${toSlug(section)}` : '';
|
||||
const label = isEmpty(alias) ? `${target}${formattedSection}` : alias;
|
||||
|
||||
// [[#section]] links
|
||||
if (target.length === 0) {
|
||||
// we don't have a good way to check if the section exists within the
|
||||
// open file, so we just create a regular link for it
|
||||
return getResourceLink(section, linkSection, label);
|
||||
}
|
||||
|
||||
const resource = workspace.find(target);
|
||||
if (isNone(resource)) {
|
||||
return getPlaceholderLink(label);
|
||||
}
|
||||
|
||||
const link = `${vscode.workspace.asRelativePath(
|
||||
const resourceLink = `/${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
)}${formattedSection}`;
|
||||
const title = `${resource.title}${formattedSection}`;
|
||||
return `<a class='foam-note-link' title='${title}' href='/${link}' data-href='/${link}'>${label}</a>`;
|
||||
)}`;
|
||||
return getResourceLink(
|
||||
`${resource.title}${formattedSection}`,
|
||||
`${resourceLink}${linkSection}`,
|
||||
label
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
@@ -52,4 +64,7 @@ export const markdownItWikilinkNavigation = (
|
||||
const getPlaceholderLink = (content: string) =>
|
||||
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
|
||||
|
||||
const getResourceLink = (title: string, link: string, label: string) =>
|
||||
`<a class='foam-note-link' title='${title}' href='${link}' data-href='${link}'>${label}</a>`;
|
||||
|
||||
export default markdownItWikilinkNavigation;
|
||||
|
||||
@@ -4,114 +4,108 @@ import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { isAbsolute } from '../core/utils/path';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { FoamFeature } from '../types';
|
||||
import { fromVsCodeUri, toVsCodeRange, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onWillRenameFiles(async e => {
|
||||
if (!getFoamVsCodeConfig<boolean>('links.sync.enable')) {
|
||||
return;
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onWillRenameFiles(async e => {
|
||||
if (!getFoamVsCodeConfig<boolean>('links.sync.enable')) {
|
||||
return;
|
||||
}
|
||||
const renameEdits = new vscode.WorkspaceEdit();
|
||||
for (const { oldUri, newUri } of e.files) {
|
||||
if (
|
||||
(await vscode.workspace.fs.stat(oldUri)).type ===
|
||||
vscode.FileType.Directory
|
||||
) {
|
||||
vscode.window.showWarningMessage(
|
||||
'Foam: Updating links on directory rename is not supported.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const renameEdits = new vscode.WorkspaceEdit();
|
||||
for (const { oldUri, newUri } of e.files) {
|
||||
if (
|
||||
(await vscode.workspace.fs.stat(oldUri)).type ===
|
||||
vscode.FileType.Directory
|
||||
) {
|
||||
vscode.window.showWarningMessage(
|
||||
'Foam: Updating links on directory rename is not supported.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const connections = foam.graph.getBacklinks(fromVsCodeUri(oldUri));
|
||||
connections.forEach(async connection => {
|
||||
const { target } = MarkdownLink.analyzeLink(connection.link);
|
||||
switch (connection.link.type) {
|
||||
case 'wikilink': {
|
||||
const identifier = foam.workspace.getIdentifier(
|
||||
fromVsCodeUri(newUri),
|
||||
[fromVsCodeUri(oldUri)]
|
||||
);
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(
|
||||
connection.link,
|
||||
{ target: identifier }
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'link': {
|
||||
const path = isAbsolute(target)
|
||||
? '/' + vscode.workspace.asRelativePath(newUri)
|
||||
: fromVsCodeUri(newUri).relativeTo(
|
||||
connection.source.getDirectory()
|
||||
).path;
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(
|
||||
connection.link,
|
||||
{ target: path }
|
||||
);
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
const connections = foam.graph.getBacklinks(fromVsCodeUri(oldUri));
|
||||
connections.forEach(async connection => {
|
||||
const { target } = MarkdownLink.analyzeLink(connection.link);
|
||||
switch (connection.link.type) {
|
||||
case 'wikilink': {
|
||||
const identifier = foam.workspace.getIdentifier(
|
||||
fromVsCodeUri(newUri),
|
||||
[fromVsCodeUri(oldUri)]
|
||||
);
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(connection.link, {
|
||||
target: identifier,
|
||||
});
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (renameEdits.size > 0) {
|
||||
// We break the update by file because applying it at once was causing
|
||||
// dirty state and editors not always saving or closing
|
||||
for (const renameEditForUri of renameEdits.entries()) {
|
||||
const [uri, edits] = renameEditForUri;
|
||||
const fileEdits = new vscode.WorkspaceEdit();
|
||||
fileEdits.set(uri, edits);
|
||||
await vscode.workspace.applyEdit(fileEdits);
|
||||
const editor = await vscode.workspace.openTextDocument(uri);
|
||||
// Because the save happens within 50ms of opening the doc, it will be then closed
|
||||
editor.save();
|
||||
case 'link': {
|
||||
const path = isAbsolute(target)
|
||||
? '/' + vscode.workspace.asRelativePath(newUri)
|
||||
: fromVsCodeUri(newUri).relativeTo(
|
||||
connection.source.getDirectory()
|
||||
).path;
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(connection.link, {
|
||||
target: path,
|
||||
});
|
||||
renameEdits.replace(
|
||||
toVsCodeUri(connection.source),
|
||||
toVsCodeRange(edit.range),
|
||||
edit.newText
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reporting
|
||||
const nUpdates = renameEdits.entries().reduce((acc, entry) => {
|
||||
return (acc += entry[1].length);
|
||||
}, 0);
|
||||
const links = nUpdates > 1 ? 'links' : 'link';
|
||||
const nFiles = renameEdits.size;
|
||||
const files = nFiles > 1 ? 'files' : 'file';
|
||||
Logger.info(
|
||||
`Updated links in the following files:`,
|
||||
...renameEdits
|
||||
.entries()
|
||||
.map(e => vscode.workspace.asRelativePath(e[0]))
|
||||
);
|
||||
vscode.window.showInformationMessage(
|
||||
`Updated ${nUpdates} ${links} across ${nFiles} ${files}.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error while updating references to file', e);
|
||||
vscode.window.showErrorMessage(
|
||||
`Foam couldn't update the links to ${vscode.workspace.asRelativePath(
|
||||
e.newUri
|
||||
)}. Check the logs for error details.`
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (renameEdits.size > 0) {
|
||||
// We break the update by file because applying it at once was causing
|
||||
// dirty state and editors not always saving or closing
|
||||
for (const renameEditForUri of renameEdits.entries()) {
|
||||
const [uri, edits] = renameEditForUri;
|
||||
const fileEdits = new vscode.WorkspaceEdit();
|
||||
fileEdits.set(uri, edits);
|
||||
await vscode.workspace.applyEdit(fileEdits);
|
||||
const editor = await vscode.workspace.openTextDocument(uri);
|
||||
// Because the save happens within 50ms of opening the doc, it will be then closed
|
||||
editor.save();
|
||||
}
|
||||
|
||||
// Reporting
|
||||
const nUpdates = renameEdits.entries().reduce((acc, entry) => {
|
||||
return (acc += entry[1].length);
|
||||
}, 0);
|
||||
const links = nUpdates > 1 ? 'links' : 'link';
|
||||
const nFiles = renameEdits.size;
|
||||
const files = nFiles > 1 ? 'files' : 'file';
|
||||
Logger.info(
|
||||
`Updated links in the following files:`,
|
||||
...renameEdits
|
||||
.entries()
|
||||
.map(e => vscode.workspace.asRelativePath(e[0]))
|
||||
);
|
||||
vscode.window.showInformationMessage(
|
||||
`Updated ${nUpdates} ${links} across ${nFiles} ${files}.`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
export default feature;
|
||||
} catch (e) {
|
||||
Logger.error('Error while updating references to file', e);
|
||||
vscode.window.showErrorMessage(
|
||||
`Foam couldn't update the links to ${vscode.workspace.asRelativePath(
|
||||
e.newUri
|
||||
)}. Check the logs for error details.`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,4 +213,154 @@ more text
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('works inside front-matter #1184', () => {
|
||||
it('should provide suggestions when on `tags:` in the front-matter', async () => {
|
||||
const { uri } = await createFile(`---
|
||||
created: 2023-01-01
|
||||
tags: prim`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 10)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions when on `tags:` in the front-matter with leading `[`', async () => {
|
||||
const { uri } = await createFile('---\ntags: [');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(1, 7)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions when on `tags:` in the front-matter with `#`', async () => {
|
||||
const { uri } = await createFile('---\ntags: #');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(1, 7)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions when on `tags:` in the front-matter when tags are comma separated', async () => {
|
||||
const { uri } = await createFile(
|
||||
'---\ncreated: 2023-01-01\ntags: secondary, prim'
|
||||
);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 21)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions when on `tags:` in the front-matter in middle of comma separated', async () => {
|
||||
const { uri } = await createFile(
|
||||
'---\ncreated: 2023-01-01\ntags: second, prim'
|
||||
);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 12)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('secondary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions in `tags:` on separate line with leading space', async () => {
|
||||
const { uri } = await createFile('---\ntags: second, prim\n ');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 1)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('secondary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide suggestions in `tags:` on separate line with leading ` - `', async () => {
|
||||
const { uri } = await createFile('---\ntags:\n - ');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 3)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('secondary')).toBeTruthy();
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not provide suggestions when on non-`tags:` in the front-matter', async () => {
|
||||
const { uri } = await createFile('---\ntags: prim\ntitle: prim');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 11)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide suggestions when outside the front-matter without `#` key', async () => {
|
||||
const { uri } = await createFile(
|
||||
'---\ncreated: 2023-01-01\ntags: prim\n---\ncontent\ntags: prim'
|
||||
);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(5, 10)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide suggestions in `tags:` on separate line with leading ` -`', async () => {
|
||||
const { uri } = await createFile('---\ntags:\n -');
|
||||
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('secondary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine, 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 =
|
||||
const HASH_REGEX =
|
||||
/(?<=^|\s)#(?![ \t#])([0-9]*[\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/dgu;
|
||||
const MAX_LINES_FOR_FRONT_MATTER = 50;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new TagCompletionProvider(foam.tags),
|
||||
'#'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
new TagCompletionProvider(foam.tags),
|
||||
'#'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class TagCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem>
|
||||
@@ -38,23 +36,96 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(TAG_REGEX);
|
||||
if (!requiresAutocomplete) {
|
||||
const beginningOfFileText = document.getText(
|
||||
new vscode.Range(
|
||||
new vscode.Position(0, 0),
|
||||
new vscode.Position(
|
||||
position.line < MAX_LINES_FOR_FRONT_MATTER
|
||||
? position.line
|
||||
: MAX_LINES_FOR_FRONT_MATTER,
|
||||
position.character
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const isHashMatch = cursorPrefix.match(HASH_REGEX) !== null;
|
||||
const inFrontMatter = isInFrontMatter(beginningOfFileText, position.line);
|
||||
|
||||
if (!isHashMatch && !inFrontMatter) {
|
||||
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;
|
||||
return inFrontMatter
|
||||
? this.createTagsForFrontMatter(beginningOfFileText, position)
|
||||
: this.createTagsForContent(cursorPrefix, position);
|
||||
}
|
||||
|
||||
private createTagsForFrontMatter(
|
||||
content: string,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.CompletionList<vscode.CompletionItem>> {
|
||||
const FRONT_MATTER_PREVIOUS_CHARACTER = /[#[\s\w]/g;
|
||||
|
||||
const lines = content.split('\n');
|
||||
if (position.line >= lines.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cursorPrefix = lines[position.line].substring(0, position.character);
|
||||
|
||||
const isTagsMatch =
|
||||
isOnYAMLKeywordLine(content, 'tags') &&
|
||||
cursorPrefix
|
||||
.charAt(position.character - 1)
|
||||
.match(FRONT_MATTER_PREVIOUS_CHARACTER);
|
||||
|
||||
if (!isTagsMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [lastMatchStartIndex, lastMatchEndIndex] = this.tagMatchIndices(
|
||||
cursorPrefix,
|
||||
HASH_REGEX
|
||||
);
|
||||
|
||||
const isHashMatch = cursorPrefix.match(HASH_REGEX) !== null;
|
||||
if (isHashMatch && lastMatchEndIndex !== position.character) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completionTags = this.createCompletionTagItems();
|
||||
// We are in the front matter and we typed #, remove the `#`
|
||||
if (isHashMatch) {
|
||||
completionTags.forEach(item => {
|
||||
item.additionalTextEdits = [
|
||||
vscode.TextEdit.delete(
|
||||
new vscode.Range(
|
||||
position.line,
|
||||
lastMatchStartIndex,
|
||||
position.line,
|
||||
lastMatchStartIndex + 1
|
||||
)
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return new vscode.CompletionList(completionTags);
|
||||
}
|
||||
|
||||
private createTagsForContent(
|
||||
content: string,
|
||||
position: vscode.Position
|
||||
): vscode.ProviderResult<vscode.CompletionList<vscode.CompletionItem>> {
|
||||
const [, lastMatchEndIndex] = this.tagMatchIndices(content, HASH_REGEX);
|
||||
if (lastMatchEndIndex !== position.character) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new vscode.CompletionList(this.createCompletionTagItems());
|
||||
}
|
||||
|
||||
private createCompletionTagItems(): vscode.CompletionItem[] {
|
||||
const completionTags = [];
|
||||
[...this.foamTags.tags].forEach(([tag]) => {
|
||||
const item = new vscode.CompletionItem(
|
||||
@@ -67,9 +138,24 @@ export class TagCompletionProvider
|
||||
|
||||
completionTags.push(item);
|
||||
});
|
||||
return completionTags;
|
||||
}
|
||||
|
||||
return new vscode.CompletionList(completionTags);
|
||||
private tagMatchIndices(content: string, match: RegExp): number[] {
|
||||
// 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(content.matchAll(match));
|
||||
if (matches.length === 0) {
|
||||
return [-1, -1];
|
||||
}
|
||||
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
const lastMatchStartIndex = lastMatch.index;
|
||||
const lastMatchEndIndex = lastMatch[0].length + lastMatchStartIndex;
|
||||
|
||||
return [lastMatchStartIndex, lastMatchEndIndex];
|
||||
}
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Resource, ResourceParser } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { FoamFeature } from '../types';
|
||||
import { isNone } from '../utils';
|
||||
import {
|
||||
fromVsCodeUri,
|
||||
@@ -25,20 +24,21 @@ interface FoamCommand<T> {
|
||||
interface FindIdentifierCommandArgs {
|
||||
range: vscode.Range;
|
||||
target: vscode.Uri;
|
||||
defaultExtension: string;
|
||||
amongst: vscode.Uri[];
|
||||
}
|
||||
|
||||
const FIND_IDENTIFER_COMMAND: FoamCommand<FindIdentifierCommandArgs> = {
|
||||
const FIND_IDENTIFIER_COMMAND: FoamCommand<FindIdentifierCommandArgs> = {
|
||||
name: 'foam:compute-identifier',
|
||||
execute: async ({ target, amongst, range }) => {
|
||||
execute: async ({ target, amongst, range, defaultExtension }) => {
|
||||
if (vscode.window.activeTextEditor) {
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
target.path,
|
||||
amongst.map(uri => uri.path)
|
||||
);
|
||||
|
||||
identifier = identifier.endsWith('.md')
|
||||
? identifier.slice(0, -3)
|
||||
identifier = identifier.endsWith(defaultExtension)
|
||||
? identifier.slice(0, defaultExtension.length * -1)
|
||||
: identifier;
|
||||
|
||||
await vscode.window.activeTextEditor.edit(builder => {
|
||||
@@ -62,59 +62,57 @@ const REPLACE_TEXT_COMMAND: FoamCommand<ReplaceTextCommandArgs> = {
|
||||
},
|
||||
};
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const collection = vscode.languages.createDiagnosticCollection('foam');
|
||||
const debouncedUpdateDiagnostics = debounce(updateDiagnostics, 500);
|
||||
const foam = await foamPromise;
|
||||
if (vscode.window.activeTextEditor) {
|
||||
updateDiagnostics(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
vscode.window.activeTextEditor.document,
|
||||
collection
|
||||
);
|
||||
}
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) {
|
||||
updateDiagnostics(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
editor.document,
|
||||
collection
|
||||
);
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
debouncedUpdateDiagnostics(
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const collection = vscode.languages.createDiagnosticCollection('foam');
|
||||
const debouncedUpdateDiagnostics = debounce(updateDiagnostics, 500);
|
||||
const foam = await foamPromise;
|
||||
if (vscode.window.activeTextEditor) {
|
||||
updateDiagnostics(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
vscode.window.activeTextEditor.document,
|
||||
collection
|
||||
);
|
||||
}
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) {
|
||||
updateDiagnostics(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
event.document,
|
||||
editor.document,
|
||||
collection
|
||||
);
|
||||
}),
|
||||
vscode.languages.registerCodeActionsProvider(
|
||||
'markdown',
|
||||
new IdentifierResolver(),
|
||||
{
|
||||
providedCodeActionKinds: IdentifierResolver.providedCodeActionKinds,
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
FIND_IDENTIFER_COMMAND.name,
|
||||
FIND_IDENTIFER_COMMAND.execute
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
REPLACE_TEXT_COMMAND.name,
|
||||
REPLACE_TEXT_COMMAND.execute
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
debouncedUpdateDiagnostics(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
event.document,
|
||||
collection
|
||||
);
|
||||
}),
|
||||
vscode.languages.registerCodeActionsProvider(
|
||||
'markdown',
|
||||
new IdentifierResolver(foam.workspace.defaultExtension),
|
||||
{
|
||||
providedCodeActionKinds: IdentifierResolver.providedCodeActionKinds,
|
||||
}
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
FIND_IDENTIFIER_COMMAND.name,
|
||||
FIND_IDENTIFIER_COMMAND.execute
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
REPLACE_TEXT_COMMAND.name,
|
||||
REPLACE_TEXT_COMMAND.execute
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDiagnostics(
|
||||
workspace: FoamWorkspace,
|
||||
@@ -196,6 +194,8 @@ export class IdentifierResolver implements vscode.CodeActionProvider {
|
||||
vscode.CodeActionKind.QuickFix,
|
||||
];
|
||||
|
||||
constructor(private defaultExtension: string) {}
|
||||
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
@@ -210,7 +210,12 @@ export class IdentifierResolver implements vscode.CodeActionProvider {
|
||||
);
|
||||
for (const item of diagnostic.relatedInformation) {
|
||||
res.push(
|
||||
createFindIdentifierCommand(diagnostic, item.location.uri, uris)
|
||||
createFindIdentifierCommand(
|
||||
diagnostic,
|
||||
item.location.uri,
|
||||
this.defaultExtension,
|
||||
uris
|
||||
)
|
||||
);
|
||||
}
|
||||
return [...acc, ...res];
|
||||
@@ -260,6 +265,7 @@ const createReplaceSectionCommand = (
|
||||
const createFindIdentifierCommand = (
|
||||
diagnostic: vscode.Diagnostic,
|
||||
target: vscode.Uri,
|
||||
defaultExtension: string,
|
||||
possibleTargets: vscode.Uri[]
|
||||
): vscode.CodeAction => {
|
||||
const action = new vscode.CodeAction(
|
||||
@@ -267,12 +273,13 @@ const createFindIdentifierCommand = (
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
command: FIND_IDENTIFER_COMMAND.name,
|
||||
command: FIND_IDENTIFIER_COMMAND.name,
|
||||
title: 'Link to this resource',
|
||||
arguments: [
|
||||
{
|
||||
target: target,
|
||||
amongst: possibleTargets,
|
||||
defaultExtension: defaultExtension,
|
||||
range: new vscode.Range(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character + 2,
|
||||
@@ -285,5 +292,3 @@ const createFindIdentifierCommand = (
|
||||
action.diagnostics = [diagnostic];
|
||||
return action;
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isEmpty } from 'lodash';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
EndOfLine,
|
||||
FileType,
|
||||
RelativePattern,
|
||||
Selection,
|
||||
@@ -74,6 +75,15 @@ export async function replaceSelection(
|
||||
await workspace.applyEdit(originatingFileEdit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the EOL character for the currently open editor.
|
||||
*/
|
||||
export function getEditorEOL(): string {
|
||||
return window.activeTextEditor.document.eol === EndOfLine.CRLF
|
||||
? '\r\n'
|
||||
: '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory of the file currently open in the editor.
|
||||
* If no file is open in the editor it will return the first folder
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from './editor';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import dateFormat from 'dateformat';
|
||||
import { isSome } from '../core/utils';
|
||||
import { getFoamVsCodeConfig } from './config';
|
||||
|
||||
/**
|
||||
@@ -476,30 +475,7 @@ async function askUserForFilepathConfirmation(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Common chars that is better to avoid in file names.
|
||||
* Inspired by:
|
||||
* https://www.mtu.edu/umc/services/websites/writing/characters-avoid/
|
||||
* https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
|
||||
* Even if some might be allowed in Win or Linux, to keep things more compatible and less error prone
|
||||
* we don't allow them
|
||||
* Also see https://github.com/foambubble/foam/issues/1042
|
||||
*/
|
||||
const UNALLOWED_CHARS = '/\\#%&{}<>?*$!\'":@+`|=';
|
||||
|
||||
/**
|
||||
* Uses the title to generate a file path.
|
||||
* It sanitizes the title to remove special characters and spaces.
|
||||
*
|
||||
* @param resolver the resolver to use
|
||||
* @returns the string path of the new note
|
||||
*/
|
||||
export const getPathFromTitle = async (resolver: Resolver) => {
|
||||
let defaultName = await resolver.resolveFromName('FOAM_TITLE');
|
||||
UNALLOWED_CHARS.split('').forEach(char => {
|
||||
defaultName = defaultName.split(char).join('');
|
||||
});
|
||||
|
||||
const defaultFilepath = URI.file(`${defaultName}.md`);
|
||||
return defaultFilepath;
|
||||
const defaultName = await resolver.resolveFromName('FOAM_TITLE_SAFE');
|
||||
return URI.file(`${defaultName}.md`);
|
||||
};
|
||||
|
||||
@@ -102,6 +102,23 @@ describe('variable-resolver, variable resolution', () => {
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_TITLE_SAFE', async () => {
|
||||
const foamTitle = 'My/note#title';
|
||||
const variables = [
|
||||
new Variable('FOAM_TITLE'),
|
||||
new Variable('FOAM_TITLE_SAFE'),
|
||||
];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
expected.set('FOAM_TITLE_SAFE', 'My-note-title');
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', foamTitle);
|
||||
const resolver = new Resolver(givenValues, new Date());
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_DATE_* properties with current day by default', async () => {
|
||||
const variables = [
|
||||
new Variable('FOAM_DATE_YEAR'),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user