mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7400c20f | ||
|
|
d2feee8c75 | ||
|
|
2113705fb6 | ||
|
|
4758923188 | ||
|
|
275028ffa5 | ||
|
|
8b425ff420 | ||
|
|
64638868c0 | ||
|
|
7a2756eb1d | ||
|
|
540371bf96 | ||
|
|
42b32103f5 | ||
|
|
1d264dcb22 | ||
|
|
7f8c58084b | ||
|
|
6b2dda4a71 | ||
|
|
a6cb50f3d6 | ||
|
|
09c2198928 | ||
|
|
431da1b385 | ||
|
|
57e0621c24 | ||
|
|
ebd1008215 | ||
|
|
0df958c3a4 | ||
|
|
14709313ae | ||
|
|
ff2dd23918 | ||
|
|
66e74966ee | ||
|
|
9b022d0c61 | ||
|
|
e1b301814e | ||
|
|
2010d8d51e | ||
|
|
22fa977c9b | ||
|
|
285293ec23 | ||
|
|
6497f527ca | ||
|
|
689c167384 |
@@ -950,6 +950,51 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "elgirafo",
|
||||
"name": "luca",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/80516439?v=4",
|
||||
"profile": "http://elgirafo.xyz",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Lloyd-Jackman-UKPL",
|
||||
"name": "Lloyd Jackman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55206370?v=4",
|
||||
"profile": "https://github.com/Lloyd-Jackman-UKPL",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sn3akiwhizper",
|
||||
"name": "sn3akiwhizper",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/102705294?v=4",
|
||||
"profile": "http://sn3akiwhizper.github.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jonathanpberger",
|
||||
"name": "jonathan berger",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/41085?v=4",
|
||||
"profile": "http://jonathanpberger.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "badsketch",
|
||||
"name": "Daniel Wang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8953212?v=4",
|
||||
"profile": "https://github.com/badsketch",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -28,5 +28,8 @@
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn jest",
|
||||
"gitdoc.enabled": false,
|
||||
"search.mode": "reuseEditor"
|
||||
"search.mode": "reuseEditor",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
||||
4
docs/.vscode/custom-tag-style.css
vendored
Normal file
4
docs/.vscode/custom-tag-style.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.foam-tag{
|
||||
color:#ffffff;
|
||||
background-color: #000000;
|
||||
}
|
||||
5
docs/.vscode/settings.json
vendored
5
docs/.vscode/settings.json
vendored
@@ -26,5 +26,8 @@
|
||||
"files.exclude": {
|
||||
"_site/**": true
|
||||
},
|
||||
"files.insertFinalNewline": true
|
||||
"files.insertFinalNewline": true,
|
||||
"markdown.styles": [
|
||||
".vscode/custom-tag-style.css"
|
||||
]
|
||||
}
|
||||
|
||||
29
docs/Achieving Greater Privacy and Security.md
Normal file
29
docs/Achieving Greater Privacy and Security.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Achieving Greater Privacy and Security
|
||||
|
||||
Foam, at its heart and committed to in its [Principles](https://foambubble.github.io/foam/principles), allows the user to control their content in a flexible and non-prescriptive manner. This extends to user preferences, or requirements depending on application and context, around both privacy and security. One way that these use cases can be met is through the use of open-source and not-for-profit mechanisms in the user's workflow to provide a functional equivalence.
|
||||
|
||||
Here are a few suggestions on increasing privacy and security when using Foam.
|
||||
## VS Codium: The Open Source build of VS Code
|
||||
|
||||
Foam is built upon VS Code, itself a Microsoft product built on top of an open source project.
|
||||
|
||||
As can be found [here](https://github.com/Microsoft/vscode/issues/60#issuecomment-161792005) the **VS Code product itself is not fully open source**. This means that its inner workings are not fully transparent, facilitating the collection and distribution of your data, as specified in its [Privacy Statement](https://devblogs.microsoft.com/visualstudio/privacy/).
|
||||
|
||||
If you prefer a fully open source editor based on the same core of VS Code (and for most intents and purposes equivalent to it), you can try [VSCodium](https://github.com/VSCodium).
|
||||
In its own introduction it is described as, "Binary releases of VS Code without MS branding/telemetry/licensing". Installation packages are easily available across Windows, Unix and Linux (or you can build it from source!).
|
||||
Access to the VS Code marketplace of add-ons remains in place, including the Foam extension.
|
||||
|
||||
The change you will notice in using VS Code versus VS Codium - simply speaking, none. It is, in just about every way you will think of, the same IDE, just without the Microsoft proprietary licence and telemetry. Your Foam experience will remain as smooth and productive as before the change.
|
||||
|
||||
## Version Control and Replication
|
||||
|
||||
In Foam's [Getting Started](https://foambubble.github.io/foam/#getting-started) section, the set up describes how to set up your notes with a GitHub repository in using the template provided. Doing so provides the user with the ability to see commits made and therefore versions of their notes, allows the user to work across devices or collaborate effectively with other users, and makes publishing to GitHub pages easy.
|
||||
It's important at the same time to point out the closed-source nature of GitHub, being owned by Microsoft.
|
||||
|
||||
One alternative approach could be to use [GitLab](https://gitlab.com/), an open source alternative to GitHub. Whilst it improves on the aspect of transparency, it does also collect usage details and sends your content across the internet.
|
||||
And of course data is still stored in clear in the cloud, making it susceptible to hacks of the service.
|
||||
|
||||
A more private approach would manage replication between devices and users with a serverless mechanism like [Syncthing](https://syncthing.net). Its continuous synchronisation means that changes in files are seen almost instantly and offers the choice of using only local network connections or securely using public relays when a local network connection is unavailable. This means that having two connected devices online will have them synchronised, but it is worth noting that the continuous synchronisation could result in corruption if two users worked on the same file simultaneously and it doesn't offer the same kind of version control that git does (though versioning support can be found and is described [here](https://docs.syncthing.net/users/versioning.html)). It is also not advisable to attempt to use a continuous synchronisation tool to sync local git repositories as the risk of corruption on the git files is high (see [here](https://forum.syncthing.net/t/can-syncthing-reliably-sync-local-git-repos-not-github/8404/18)).
|
||||
|
||||
If you need the version control and collaboration, but do not want to compromise on your privacy, the best course of action is to host the open source GitLab server software yourself. The steps (well described [here](https://www.techrepublic.com/article/how-to-set-up-a-gitlab-server-and-host-your-own-git-repositories/)) are not especially complex by any means and can be used exclusively on the local network, if required, offering a rich experience of "built-in version control, issue tracking, code review, CI/CD, and more", according to its website, [GitLab / GitLab Community Edition · GitLab](https://gitlab.com/rluna-gitlab/gitlab-ce).
|
||||
|
||||
BIN
docs/assets/images/custom-tag-style.png
Normal file
BIN
docs/assets/images/custom-tag-style.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/assets/images/graph-filter.gif
Executable file
BIN
docs/assets/images/graph-filter.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 MiB |
@@ -1,7 +1,7 @@
|
||||
# Developing documentation
|
||||
|
||||
The best way to develop docs for the Foam repo is to directly open the `$foam-repo/docs/` as the root folder in a new vscode window.
|
||||
This automatically configures vscode with the necessary settings enabled (like [[link-reference-definitions]]) to effiniently write this documentation.
|
||||
This automatically configures vscode with the necessary settings enabled (like [[link-reference-definitions]]) to efficiently write this documentation.
|
||||
|
||||
## Organization
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ tags: todo, good-first-task
|
||||
# Contribution Guide
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase. For a comprehensive guide about contributing to open-source projects in general, [see here](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/).
|
||||
|
||||
## Getting Up To Speed
|
||||
|
||||
@@ -23,17 +23,18 @@ Finally, the easiest way to help, is to use it and provide feedback by [submitti
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing, this short guide will help you get things set up locally.
|
||||
If you're interested in contributing, this short guide will help you get things set up locally (assuming [node.js](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
|
||||
|
||||
1. Clone the repo locally:
|
||||
1. Fork the project to your Github account by clicking the "Fork" button on the top right hand corner of the project's [home repository page](https://github.com/foambubble/foam).
|
||||
2. Clone your newly forked repo locally:
|
||||
|
||||
`git clone https://github.com/foambubble/foam.git`
|
||||
`git clone https://github.com/your_username/foam.git`
|
||||
|
||||
2. Install the necessary dependencies by running this command from the root:
|
||||
3. Install the necessary dependencies by running this command from the root of the cloned repository:
|
||||
|
||||
`yarn install`
|
||||
|
||||
3. From the root, run the command:
|
||||
4. From the repository root, run the command:
|
||||
|
||||
`yarn build`
|
||||
|
||||
@@ -86,6 +87,18 @@ This guide assumes you read the previous instructions and you're set up to work
|
||||
|
||||
3. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
|
||||
|
||||
### Submitting a Pull Request (PR)
|
||||
|
||||
After you have made your changes to your copy of the project, it is time to try and merge those changes into the public community project.
|
||||
|
||||
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.
|
||||
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!
|
||||
7. Finally rest and watch the sun rise on a grateful universe... Or start tackling the other open issues ;)
|
||||
|
||||
---
|
||||
|
||||
Feel free to modify and submit a PR if this guide is out-of-date or contains errors!
|
||||
|
||||
@@ -239,6 +239,13 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center"><a href="https://www.readingsnail.pe.kr"><img src="https://avatars.githubusercontent.com/u/1904967?v=4?s=60" width="60px;" alt="Woosuk Park"/><br /><sub><b>Woosuk Park</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=readingsnail" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.dmurph.com"><img src="https://avatars.githubusercontent.com/u/294026?v=4?s=60" width="60px;" alt="Daniel Murphy"/><br /><sub><b>Daniel Murphy</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dmurph" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Dominic-DallOsto"><img src="https://avatars.githubusercontent.com/u/26859884?v=4?s=60" width="60px;" alt="Dominic D"/><br /><sub><b>Dominic D</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Dominic-DallOsto" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://elgirafo.xyz"><img src="https://avatars.githubusercontent.com/u/80516439?v=4?s=60" width="60px;" alt="luca"/><br /><sub><b>luca</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elgirafo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Lloyd-Jackman-UKPL"><img src="https://avatars.githubusercontent.com/u/55206370?v=4?s=60" width="60px;" alt="Lloyd Jackman"/><br /><sub><b>Lloyd Jackman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lloyd-Jackman-UKPL" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://sn3akiwhizper.github.io"><img src="https://avatars.githubusercontent.com/u/102705294?v=4?s=60" width="60px;" alt="sn3akiwhizper"/><br /><sub><b>sn3akiwhizper</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sn3akiwhizper" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -45,11 +45,15 @@ The following properties can be used:
|
||||
|
||||
The above configuration would create a file `journal/daily-note-2020-07-25.mdx`, with the heading `Journal Entry, Sunday, July 25`.
|
||||
|
||||
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
|
||||
|
||||
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
|
||||
|
||||
Please see [[note-macros]]
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[Note Templates]: note-templates.md "Note Templates"
|
||||
[note-properties]: note-properties.md "Note Properties"
|
||||
[note-templates#Example of date-based|Example of date-based filepath]: note-templates.md "Note Templates"
|
||||
[note-macros]: ../recipes/note-macros.md "Custom Note Macros"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -3,15 +3,29 @@
|
||||
Foam comes with a graph visualization of your notes.
|
||||
To see the graph execute the `Foam: Show Graph` command.
|
||||
|
||||
Your files, such as notes and documents, are shown as the nodes of the graph along with the tags defined in your notes. The edges of the graph represent either a link between two files or a file that contains a certain tag. A node in the graph will grow in size with the number of connections it has, representing stronger or more defined concepts and topics.
|
||||
|
||||
## Graph Navigation
|
||||
|
||||
With the graph you can:
|
||||
With the Foam graph visualization you can:
|
||||
|
||||
- highlight a node by hovering on it, to quickly see how it's connected to the rest of your notes
|
||||
- select one or more (by keeping `shift` pressed while selecting) nodes by clicking on them, to better understand the structure of your notes
|
||||
- navigate to a note by clicking on it while pressing `ctrl` or `cmd`
|
||||
- navigate to a note by clicking on it's node while pressing `ctrl` or `cmd`
|
||||
- automatically center the graph on the currently edited note, to immediately see its connections
|
||||
|
||||
## Filter View
|
||||
|
||||
If you only wish to view certain types of notes or tags, or want to hide linked attachment nodes then you can apply filters to the graph.
|
||||
|
||||
- Open the graph view using the `Foam: Show Graph` command
|
||||
- Click the button in the top right corner of the graph view that says "Open Controls"
|
||||
- Expand the "Filter By Type" dropdown to view the selection of types that you can filter by
|
||||
- Uncheck the checkbox for any type you want to hide
|
||||
- The types displayed in this dropdown are defined by [[note-properties]] which includes Foam-standard types as well as custom types defined by you!
|
||||
|
||||

|
||||
|
||||
## Custom Graph Styles
|
||||
|
||||
The Foam graph will use the current VS Code theme by default, but it's possible to customize it with the `foam.graph.style` setting.
|
||||
@@ -24,28 +38,41 @@ A sample configuration object is provided below, you can provide as many or as l
|
||||
"foam.graph.style": {
|
||||
"background": "#202020",
|
||||
"fontSize": 12,
|
||||
"lineColor": "#277da1",
|
||||
"lineWidth": 0.2,
|
||||
"particleWidth": 1.0,
|
||||
"highlightedForeground": "#f9c74f",
|
||||
"node": {
|
||||
"note": "#277da1",
|
||||
"placeholder": "#545454",
|
||||
"feature": "green",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `note` defines the color for regular nodes
|
||||
- `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).
|
||||
- `feature` shows an example of how you can use note types to customize the graph. It defines the color for the notes of type `feature`
|
||||
- see [[note-properties]] for details
|
||||
- you can have as many types as you want
|
||||
- `background` background color of the graph, adjust to increase contrast
|
||||
- `fontSize` size of the title font for each node
|
||||
- `lineColor` color of the edges between nodes in the graph
|
||||
- `lineWidth` thickness of the edges between nodes
|
||||
- `particleWidth` size of the particle animation showing link direction when highlighting a node
|
||||
- `highlightedForeground` color of highlighted nodes and edges when hovering over a node
|
||||
- to style individual types of nodes jump to the next section: [Style Nodes By Type](#style-nodes-by-type)
|
||||
|
||||
### Style nodes by type
|
||||
### Style Nodes by Type
|
||||
|
||||
It is possible to customize the style of a node based on the `type` property in the YAML frontmatter of the corresponding document.
|
||||
|
||||
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.
|
||||
- `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.
|
||||
- see [[tags]] for more info
|
||||
- `feature` shows an example of how you can use note types to customize the graph. It defines the color for the notes of type `feature`
|
||||
- see [[note-properties]] for details
|
||||
|
||||
For example the following `backlinking.md` note:
|
||||
|
||||
```
|
||||
```markdown
|
||||
---
|
||||
type: feature
|
||||
---
|
||||
@@ -58,7 +85,11 @@ And the following `settings.json`:
|
||||
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"background": "#202020",
|
||||
"node": {
|
||||
"note": "#277da1",
|
||||
"placeholder": "#545454",
|
||||
"tag": "#f9c74f",
|
||||
"feature": "red",
|
||||
}
|
||||
}
|
||||
@@ -69,6 +100,7 @@ Will result in the following graph:
|
||||

|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[wikilinks]: wikilinks.md "Wikilinks"
|
||||
[note-properties]: note-properties.md "Note Properties"
|
||||
[wikilinks]: wikilinks.md "Wikilinks"
|
||||
[tags]: tags.md "Tags"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -48,9 +48,23 @@ You can override this setting in your Foam workspace's `settings.json`:
|
||||
|
||||
Sometimes, you may want to ignore certain files or folders, so that Foam doesn't generate link reference definitions to them.
|
||||
|
||||
There are three options for excluding files from your Foam project:
|
||||
|
||||
1. `files.exclude` (from VSCode) will prevent the folder from showing in the file explorer.
|
||||
|
||||
> "Configure glob patterns for excluding files and folders. For example, the file explorer decides which files and folders to show or hide based on this setting. Refer to the Search: Exclude setting to define search-specific excludes."
|
||||
|
||||
2. `files.watcherExclude` (from VSCode) prevents VSCode from constantly monitoring files for changes.
|
||||
|
||||
> "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."
|
||||
|
||||
3. `foam.files.ignore` (from Foam) ignores files from being added to the Foam graph.
|
||||
|
||||
> "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`" (requires reloading VSCode to take effect).
|
||||
|
||||
For instance, if you're using a local instance of [Jekyll](https://jekyllrb.com/), you may find that it writes copies of each `.md` file into a `_site` directory, which may lead to Foam generating references to them instead of the original source notes.
|
||||
|
||||
You can ignore the `_site` directory by adding the following to your `.vscode/settings.json`:
|
||||
You can ignore the `_site` directory by adding any of the following settings to your `.vscode/settings.json` file:
|
||||
|
||||
```json
|
||||
"files.exclude": {
|
||||
@@ -59,18 +73,16 @@ You can ignore the `_site` directory by adding the following to your `.vscode/se
|
||||
"files.watcherExclude": {
|
||||
"**/_site": true
|
||||
},
|
||||
"foam.files.ignore": [
|
||||
"_site/**/*"
|
||||
]
|
||||
```
|
||||
|
||||
After changing the setting in your workspace, you can run the [[workspace-janitor]] command to convert all existing definitions.
|
||||
|
||||
## Future improvements
|
||||
|
||||
Implement `foam.exclude`. [[todo]]
|
||||
|
||||
See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[workspace-janitor]: ../tools/workspace-janitor.md "Janitor"
|
||||
[todo]: ../../dev/todo.md "Todo"
|
||||
[link-reference-definition-improvements]: ../../dev/proposals/link-reference-definition-improvements.md "Link Reference Definition Improvements"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,35 +1,56 @@
|
||||
---
|
||||
type: feature
|
||||
keywords: hello world
|
||||
keywords: hello world, bonjour
|
||||
tags: [hello, bonjour]
|
||||
---
|
||||
|
||||
# Note Properties
|
||||
|
||||
At the top of the file you can have a section where you define your properties.
|
||||
At the top of the file you can have a section where you define your properties. This section is known as the [Front-Matter](https://learn.cloudcannon.com/jekyll/introduction-to-jekyll-front-matter/) of the document and uses [YAML formatting](https://www.codeproject.com/Articles/1214409/Learn-YAML-in-five-minutes).
|
||||
|
||||
> Be aware that this section needs to be at the very top of the file to be valid
|
||||
> Be aware that this YAML section needs to be at the very top of the file to be valid.
|
||||
|
||||
For example, for this file, we have:
|
||||
|
||||
```text
|
||||
```markdown
|
||||
---
|
||||
type: feature
|
||||
keywords: hello world
|
||||
keywords: hello world, bonjour
|
||||
---
|
||||
```
|
||||
|
||||
Those are properties.
|
||||
Properties can be used to organize your notes.
|
||||
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`).
|
||||
|
||||
> 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.
|
||||
|
||||
## Special Properties
|
||||
|
||||
Some properties have special meaning for Foam:
|
||||
|
||||
- the `title` property 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]])
|
||||
- the `type` property can be used to style notes differently in the graph (also see [[graph-visualization]])
|
||||
- the `tags` property can be used to add tags to a note (see [[tags-and-tag-explorer]])
|
||||
| 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:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Note Title"
|
||||
type: "daily-note"
|
||||
tags: daily, funny, planning
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Foam Template Properties
|
||||
|
||||
There also exists properties that are even more specific to Foam templates, see [[note-templates#Metadata]] for more info.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[write-notes-in-foam]: ../getting-started/write-notes-in-foam.md "Writing Notes"
|
||||
[graph-visualization]: graph-visualization.md "Graph Visualization"
|
||||
[tags]: tags.md "Tags"
|
||||
[note-templates#Metadata]: note-templates.md "Note Templates"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -25,15 +25,28 @@ To create a note from a template:
|
||||
_Theme: Ayu Light_
|
||||
|
||||
## Special templates
|
||||
|
||||
### Default template
|
||||
|
||||
The `.foam/templates/new-note.md` template is special in that it is the template that will be used by the `Foam: Create New Note` command.
|
||||
Customize this template to contain content that you want included every time you create a note.
|
||||
Customize this template to contain content that you want included every time you create a note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: basic-note
|
||||
---
|
||||
```
|
||||
|
||||
### Default daily note template
|
||||
|
||||
The `.foam/templates/daily-note.md` template is special in that it is the template that will be used when creating daily notes (e.g. by using `Foam: Open Daily Note`).
|
||||
Customize this template to contain content that you want included every time you create a daily note.
|
||||
Customize this template to contain content that you want included every time you create a daily note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: daily-note
|
||||
---
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -41,12 +54,12 @@ Templates can use all the variables available in [VS Code Snippets](https://code
|
||||
|
||||
In addition, you can also use variables provided by Foam:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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 note. |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| `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`, 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. |
|
||||
| Name | Description |
|
||||
| -------------------- | ------------ |
|
||||
| `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_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. |
|
||||
|
||||
### `FOAM_DATE_*` variables
|
||||
|
||||
@@ -56,6 +69,8 @@ For example, `FOAM_DATE_YEAR` has the same behaviour as VS Code's `CURRENT_YEAR`
|
||||
|
||||
By default, prefer using the `FOAM_DATE_` versions. The datetime used to compute the values will be the same for both `FOAM_DATE_` and VS Code's variables, with the exception of the creation notes using the daily note template.
|
||||
|
||||
For more nitty-gritty details about the supported date formats, [see here](https://github.com/foambubble/foam/blob/master/packages/foam-vscode/src/services/variable-resolver.ts).
|
||||
|
||||
#### Relative daily notes
|
||||
|
||||
When referring to daily notes, you can use the relative snippets (`/+1d`, `/tomorrow`, etc.). In these cases, the new notes will be created with the daily note template, but the datetime used should be the relative datetime, not the current datetime.
|
||||
@@ -81,22 +96,19 @@ When creating notes in any other scenario, the `FOAM_DATE_` values are computed
|
||||
|
||||
Templates can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Name | Description |
|
||||
| ------------- | ---------------------- |
|
||||
| `filepath` | The filepath to use when creating the new note. If the filepath is a relative filepath, it is relative to the current workspace. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
|
||||
Foam-specific variables (e.g. `$FOAM_TITLE`) can be used within template metadata. However, VS Code snippet variables are ([currently](https://github.com/foambubble/foam/pull/655)) not supported.
|
||||
|
||||
### `filepath` attribute
|
||||
|
||||
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template.
|
||||
If the filepath is a relative filepath, it is relative to the current workspace.
|
||||
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template. If the filepath is a relative filepath, it is relative to the current workspace.
|
||||
|
||||
**Note:** While you can make use of the `filepath` attribute in [daily note](daily-notes.md) templates (`.foam/templates/daily-note.md`), there is currently no way to have `filepath` vary based on the date. This will be improved in the future. For now, you can customize the location of daily notes using the [`foam.openDailyNote` settings](daily-notes.md).
|
||||
|
||||
#### Example of relative `filepath`
|
||||
#### Example of **relative** `filepath`
|
||||
|
||||
For example, `filepath` can be used to customize `.foam/templates/new-note.md`, overriding the default `Foam: Create New Note` behaviour of opening the file in the same directory as the active file:
|
||||
|
||||
@@ -109,7 +121,7 @@ foam_template:
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of absolute `filepath`
|
||||
#### Example of **absolute** `filepath`
|
||||
|
||||
`filepath` can be an absolute filepath, so that the notes get created in the same location, regardless of which file or workspace the editor currently has open.
|
||||
The format of an absolute filepath may vary depending on the filesystem used.
|
||||
@@ -125,6 +137,22 @@ foam_template:
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of **date-based** `filepath`
|
||||
|
||||
It is possible to vary the `filepath` value based on the current date using the `FOAM_DATE_*` variables. This is especially useful for the [[daily-notes]] template if you wish to organize by years, months, etc. Below is an example of a daily-note template metadata section that will create new daily notes under the `journal/YEAR/MONTH-MONTH_NAME/` filepath. For example, when a note is created on November 15, 2022, a new file will be created at `C:\Users\foam_user\foam_notes\journal\2022\11-Nov\2022-11-15-daily-note.md`. This method also respects the creation of daily notes relative to the current date (i.e. `/+1d`).
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: daily-note
|
||||
foam_template:
|
||||
description: Daily Note for $FOAM_TITLE
|
||||
filepath: "C:\\Users\\foam_user\\foam_notes\\journal\\$FOAM_DATE_YEAR\\$FOAM_DATE_MONTH-$FOAM_DATE_MONTH_NAME_SHORT\\$FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE-daily-note.md"
|
||||
---
|
||||
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE Daily Notes
|
||||
```
|
||||
|
||||
> Note: this method **requires** the use of absolute file paths, and in this example is using Windows path conventions. This method will also override any filename formatting defined in `.vscode/settings.json`
|
||||
|
||||
### `name` and `description` attributes
|
||||
|
||||
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):
|
||||
@@ -185,3 +213,7 @@ existing_frontmatter: "Existing Frontmatter block"
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[daily-notes]: daily-notes.md "Daily Notes"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
tags: my-tag1 my-tag2
|
||||
tags: my-tag1 my-tag2 my-tag3/notes
|
||||
---
|
||||
|
||||
# Tags
|
||||
@@ -11,20 +11,45 @@ You can add tags to your notes to categorize or link notes together.
|
||||
There are two ways of creating a tag:
|
||||
|
||||
- Adding a `#tag` anywhere in the text of the note, for example: #my-tag1
|
||||
- Using the `tags: tag1, tag2` yaml frontmatter [[note property|note-properties]]. Notice `my-tag1` and `my-tag2` tags which are added to this document this way.
|
||||
- Using the `tags: tag1, tag2` yaml frontmatter [[note-properties|note property]]. Notice `my-tag1` and `my-tag2` tags which are added to this document this way.
|
||||
|
||||
Tags can also be hierarchical, so you can have `#parent/child`.
|
||||
Tags can also be hierarchical, so you can have `#parent/child` such as #my-tag3/info.
|
||||
|
||||
## Using *Tag Explorer*
|
||||
|
||||
It's possible to navigate tags via the Tag Explorer panel.
|
||||
In the future it will be possible to explore tags via the graph as well.
|
||||
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.
|
||||
|
||||
Tags can also be visualized in the Foam Graph Explorer. See [[graph-visualization]] for more info including how to change the color of nodes representing tags.
|
||||
|
||||
## Styling tags
|
||||
|
||||
Inline tags can be styled using custom CSS with the selector `.foam-tag`.
|
||||
It is possible to customize the way that tags look in the Markdown Preview panel that renders your Foam notes. This requires some knowledge of the CSS language, which is used to customize the styles of web technologies such as VSCode. A cursory introduction to CSS can be [found here](https://www.freecodecamp.org/news/get-started-with-css-in-5-minutes-e0804813fc3e/).
|
||||
|
||||
1. Create a CSS file within your Foam project, for example in `.foam/css/custom-tag-style.css` or [.vscode/custom-tag-style.css](../../.vscode/custom-tag-style.css)
|
||||
2. Add CSS code that targets the `.foam-tag` class
|
||||
3. Add a rule for each [CSS property](https://www.w3schools.com/cssref/index.php) you would like applied to your tags.
|
||||
4. Open the `.vscode/settings.json` file (or the Settings browser with `ctrl+,`)
|
||||
5. Add the path to your new stylesheet to the `markdown.styles` setting.
|
||||
|
||||
> 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.
|
||||
|
||||
```css
|
||||
.foam-tag{
|
||||
color:#ffffff;
|
||||
background-color: #000000;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Using backlinks in place of tags
|
||||
|
||||
Given the power of backlinks, some people prefer to use them as tags.
|
||||
For example you can tag your notes about books with [[book]].
|
||||
|
||||
[//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"
|
||||
|
||||
@@ -6,11 +6,11 @@ Wikilinks are the internal links that connect the files in your knowledge base.
|
||||
|
||||
To create a wikilink, type `[[` and then start typing the name of another note in your repo. Once the desired note is selected press the `tab` key to autocomplete it. For example: [[graph-visualization]].
|
||||
|
||||
`Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on wikilink to navigate to that note (`F12` also works while your cursor is on the wikilink). If the file doesn't exist it will be created in your workspace based on your default [[note-template]] settings.
|
||||
`Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on wikilink to navigate to that note (`F12` also works while your cursor is on the wikilink). If the file doesn't exist it will be created in your workspace based on your default [[note-templates]] settings.
|
||||
|
||||
## Placeholders
|
||||
|
||||
You can also create a [[placeholder]].
|
||||
You can also create a [[placeholder]]. <!--NOTE: this placeholder link should NOT have an associated file. This is to demonstrate the concept-->
|
||||
A placeholder is a wikilink that doesn't have a target file and a link to a placeholder is styled differently so you can easily tell them apart.
|
||||
They can still be helpful to highlight connections.
|
||||
|
||||
@@ -34,8 +34,8 @@ The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.f
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[graph-visualization]: graph-visualization.md "Graph Visualization"
|
||||
[note-templates]: note-templates.md "Note Templates"
|
||||
[link-reference-definitions]: link-reference-definitions.md "Link Reference Definitions"
|
||||
[foam-file-format]: ../../dev/foam-file-format.md "Foam File Format"
|
||||
[note-templates]: note-templates.md "Note Templates"
|
||||
[link-reference-definition-improvements]: ../../dev/proposals/link-reference-definition-improvements.md "Link Reference Definition Improvements"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
- Check the formatting rules for links on [[foam-file-format]] and [[wikilinks]]
|
||||
|
||||
## I don't want Foam enabled for all my workspaces
|
||||
Any extension you install in Visual Studio Code is enabled by default. Give the philosophy of Foam it works out of the box without doing any configuration upfront. In case you want to disable Foam for a specific workspace, or disable Foam by default and enable it for specific workspaces, it is advised to follow the best practices as [documented by Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-marketplace#_manage-extensions)
|
||||
Any extension you install in Visual Studio Code is enabled by default. Given the philosophy of Foam, it works out of the box without doing any configuration upfront. In case you want to disable Foam for a specific workspace, or disable Foam by default and enable it for specific workspaces, it is advised to follow the best practices as [documented by Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-marketplace#_manage-extensions)
|
||||
|
||||
## I want to publish the graph view to GitHub pages or Vercel
|
||||
If you want a different front-end look to your published foam and a way to see your graph view, we'd recommend checking out these templates:
|
||||
- [foam-gatsby](https://github.com/mathieudutour/foam-gatsby-template) by [Mathieu Dutour](https://github.com/mathieudutour)
|
||||
- [foam-gatsby-kb](https://github.com/hikerpig/foam-template-gatsby-kb) by [hikerpig](https://github.com/hikerpig)
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recommended-extensions]: getting-started/recommended-extensions.md "Recommended Extensions"
|
||||
|
||||
@@ -36,8 +36,12 @@ If you want to learn more about VS Code, check out their [website](https://code.
|
||||
You can see a few panels on the left, including:
|
||||
|
||||
- `Outline`: this panel shows you the structure of the file based on the headings
|
||||
- `Tag Explorer`: This shows you the tags in your workspace, see [[tags-and-tag-explorer]] for more information on tags
|
||||
- `Tag Explorer`: This shows you the tags in your workspace, see [[tags]] for more information on tags
|
||||
|
||||
## Settings
|
||||
|
||||
To view or change the settings in VS Code, press `cmd+,`
|
||||
To view or change the settings in VS Code, press `cmd+,` on Mac and `ctrl+,` on Windows/Linux.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[tags]: ../features/tags.md "Tags"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -218,7 +218,7 @@ Commit the file and push it to gitlab.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- *Could not locate Gemfile* - You didn't follow the steps above to [#Add a Gemlock file]
|
||||
- *Could not locate Gemfile* - You didn't follow the steps above to [Add a Gemlock file](#add-a-gemlock-file)
|
||||
- *Conversion error: Jekyll::Converters::Scss encountered an error while converting* You need to reference a theme.
|
||||
- *Pages are running in CI/CD, but I only ever see `test`, and never deploy* - Perhaps you've renamed the main branch (from master) - check the settings in `.gitlab-ci.yml` and ensure the deploy command is running to the branch you expect it to.
|
||||
- *I deployed, but my .msd files don't seem to be being converted into .html files* - You need a gem that GitHub installs by default - check `gem "jekyll-optional-front-matter"` appears in the `Gemfile`
|
||||
|
||||
@@ -62,7 +62,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
|
||||
- Quick commits with VS Code's built in [[git-integration]]
|
||||
- Store your workspace in an auto-synced GitHub repo with [[write-your-notes-in-github-gist]]
|
||||
- Sync your GitHub repo automatically using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc).
|
||||
- Sync your GitHub repo automatically using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc) [[automatic-git-syncing]].
|
||||
|
||||
## Publish
|
||||
|
||||
@@ -82,8 +82,8 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
|
||||
## Collaborate
|
||||
|
||||
- Give your team push access to your GitHub repo [[todo]]
|
||||
- Real-time collaboration via VS Code Live Share [[todo]]
|
||||
- Give your team push access to your [GitHub repo](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-access-to-your-personal-repositories/inviting-collaborators-to-a-personal-repository)
|
||||
- Real-time collaboration via VS Code Live Share [[real-time-collaboration]]
|
||||
- Accept patches via GitHub PRs [[todo]]
|
||||
|
||||
## Workflow
|
||||
@@ -128,6 +128,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[good-first-task]: ../../dev/good-first-task.md "Good First Task"
|
||||
[including-notes]: ../features/including-notes.md "Including notes in a note"
|
||||
[write-your-notes-in-github-gist]: write-your-notes-in-github-gist.md "Write your notes in GitHub Gist"
|
||||
[automatic-git-syncing]: automatic-git-syncing.md "Automatically Sync with Git"
|
||||
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "GitHub Pages"
|
||||
[publish-to-gitlab-pages]: ../publishing/publish-to-gitlab-pages.md "GitLab Pages"
|
||||
[publish-to-azure-devops-wiki]: ../publishing/publish-to-azure-devops-wiki.md "Publish to Azure DevOps Wiki"
|
||||
@@ -137,6 +138,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[publish-to-github]: ../publishing/publish-to-github.md "Publish to GitHub"
|
||||
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
|
||||
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
|
||||
[real-time-collaboration]: real-time-collaboration.md "Real-time Collaboration"
|
||||
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
|
||||
[capture-notes-with-shortcuts-and-github-actions]: capture-notes-with-shortcuts-and-github-actions.md "Capture Notes With Shortcuts and GitHub Actions"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.20.1"
|
||||
"version": "0.20.4"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/test/**
|
||||
out/**/*.test.*
|
||||
out/**/*.spec.*
|
||||
test-data/**
|
||||
src/**
|
||||
jest.config.js
|
||||
.test-workspace
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
|
||||
@@ -4,9 +4,33 @@ 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.20.4] - 2023-01-04
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for emoji tags (#1125 - thanks @badsketch)
|
||||
|
||||
## [0.20.3] - 2022-12-19
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Show number of entries in title for orphan, placeholder, tag treeviews
|
||||
|
||||
## [0.20.2] - 2022-10-26
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Creating new note uses default template when none is provided (#1094)
|
||||
|
||||
Internal:
|
||||
|
||||
- Changed matcher implementation to remove dependency on micromatch/glob
|
||||
- Removed unnecessary dependencies and assets from extension
|
||||
|
||||
## [0.20.1] - 2022-10-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved support for daily notes in multi root workspace (#1073)
|
||||
- Create note from placeholder using template (#1061 - thanks @Dominic-DallOsto)
|
||||
- Improved support for globs in multi root workspace (#1083)
|
||||
@@ -14,9 +38,11 @@ Fixes and Improvements:
|
||||
## [0.20.0] - 2022-09-30
|
||||
|
||||
New Features:
|
||||
|
||||
- Added `foam-vscode.create-note` command, which can be very customized for several use cases (#1076)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Removed `+` as a trigger char for date snippets
|
||||
- Improved attachment support (#915)
|
||||
- Improved error handling when starting Foam without an open workspace (#908)
|
||||
@@ -27,6 +53,7 @@ Fixes and Improvements:
|
||||
## [0.19.5] - 2022-09-01
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added `FOAM_DATE_WEEK` variable (#1053 - Thanks @dmurph)
|
||||
- Fixed extension inclusion when generating references for attachments
|
||||
- Link completion label can be note title as well as path (#1059)
|
||||
@@ -35,20 +62,24 @@ Fixes and Improvements:
|
||||
## [0.19.4] - 2022-08-07
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed note embed in preview (#1052)
|
||||
|
||||
## [0.19.3] - 2022-08-04
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Image embeds fixed in preview (#1036)
|
||||
|
||||
## [0.19.2] - 2022-08-04
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for angle markdown links (#1044)
|
||||
- Filter out invalid file name chars when creating note (#1042)
|
||||
|
||||
Internal:
|
||||
|
||||
- Reorganized docs (#1031, thanks @infogulch)
|
||||
- Fixed documentation links (#1046)
|
||||
- Preview code refactoring
|
||||
@@ -56,39 +87,46 @@ Internal:
|
||||
## [0.19.1] - 2022-07-11
|
||||
|
||||
Internal:
|
||||
|
||||
- Introduced cache for markdown parser (#1030)
|
||||
- Various code refactorings
|
||||
|
||||
## [0.19.0] - 2022-07-07
|
||||
|
||||
New Features:
|
||||
|
||||
- Support for attachments (PDF) and images (#1027)
|
||||
- Support for opening day notes for other days as well (#1026, thanks @alper)
|
||||
|
||||
## [0.18.5] - 2022-06-29
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Support for `alias` YAML property to define note alias (#1014 - thanks @lingyv-li)
|
||||
|
||||
Internal:
|
||||
|
||||
- Improved extension bundling (#1015 - thanks @lingyv-li)
|
||||
- Use `vscode.workspace.fs` instead of `fs` (#1005 - thanks @joshdover)
|
||||
|
||||
## [0.18.4] - 2022-06-03
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- move past `]]` when writing wikilinks (#998 - thanks @Lauviah0622)
|
||||
- highlight improvements (#890 - thanks @memeplex)
|
||||
|
||||
## [0.18.3] - 2022-04-17
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Better reporting when links fail to resolve
|
||||
- Failing link resolution during graph computation no longer fatal
|
||||
|
||||
## [0.18.2] - 2022-04-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed parsing error on empty direct links (#980 - thanks @chrisUsick)
|
||||
- Improved rendering in preview of wikilinks that have link definitions (#979 - thanks @josephdecock)
|
||||
- Restored handling of section-only wikilinks (#981)
|
||||
@@ -96,6 +134,7 @@ Fixes and Improvements:
|
||||
## [0.18.1] - 2022-04-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed parsing error for direct links with square brackets in them (#977)
|
||||
- Improved markdown direct link resolution (#972)
|
||||
- Improved templates support for custom paths (#970)
|
||||
@@ -103,14 +142,17 @@ Fixes and Improvements:
|
||||
## [0.18.0] - 2022-04-11
|
||||
|
||||
Features:
|
||||
|
||||
- Link synchronization on file rename
|
||||
|
||||
Internal:
|
||||
|
||||
- Changed graph computation on workspace change to simplify code
|
||||
|
||||
## [0.17.8] - 2022-04-01
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Do not add ignored files to Foam upon change (#480)
|
||||
- Restore full use of editor.action.openLink (#693)
|
||||
- Minor performance improvements
|
||||
@@ -118,49 +160,58 @@ Fixes and Improvements:
|
||||
## [0.17.7] - 2022-03-29
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Include links with sections in backlinks (#895)
|
||||
- Improved navigation when document editor is already open
|
||||
|
||||
## [0.17.6] - 2022-03-03
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Don't fail on error when scannig workspace (#943 - thanks @develmusa)
|
||||
|
||||
## [0.17.5] - 2022-02-22
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added FOAM_SLUG template variable (#865 - Thanks @techCarpenter)
|
||||
|
||||
## [0.17.4] - 2022-02-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improvements to Foam variables in templates (#882 - thanks @movermeyer)
|
||||
- Foam variables can now be used just any other VS Code variables, including in combination with placeholders and transformers
|
||||
|
||||
## [0.17.3] - 2022-01-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed autocompletion with tags (#885 - thanks @memeplex)
|
||||
- Improved "Open Daily Note" to be usabled in tasks (#897 - thanks @MCluck90)
|
||||
|
||||
## [0.17.2] - 2021-12-22
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved support for wikilinks in titles (#878)
|
||||
- Use syntax injection for wikilinks (#876 - thanks @memeplex)
|
||||
- Fix when applying text edits in last line
|
||||
- Fix when applying text edits in last line
|
||||
|
||||
Internal:
|
||||
|
||||
- DX: Clean up of testing setup (#881 - thanks @memeplex)
|
||||
|
||||
## [0.17.1] - 2021-12-16
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Decorate markdown files only (#857)
|
||||
- Fix template placeholders issue (#859)
|
||||
- Improved replacement range for link completion
|
||||
|
||||
Internal:
|
||||
|
||||
- Major URI/path handling refactoring (#858 - thanks @memeplex)
|
||||
|
||||
## [0.17.0] - 2021-12-08
|
||||
@@ -216,7 +267,6 @@ Fixes and Improvements:
|
||||
- Link Reference Generation is now OFF by default
|
||||
- Fixed preview navigation (#830)
|
||||
|
||||
|
||||
## [0.15.5] - 2021-11-15
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 604 KiB |
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.20.1",
|
||||
"version": "0.20.4",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -166,7 +166,7 @@
|
||||
"commands": [
|
||||
{
|
||||
"command": "foam-vscode.create-note",
|
||||
"title": "Foam: Create Note"
|
||||
"title": "Foam: Create New Note"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.clear-cache",
|
||||
@@ -210,7 +210,7 @@
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-note-from-template",
|
||||
"title": "Foam: Create Note From Template"
|
||||
"title": "Foam: Create New Note From Template"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-note-from-default-template",
|
||||
@@ -485,10 +485,12 @@
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.2.2",
|
||||
"jest-extended": "^0.11.5",
|
||||
"markdown-it": "^12.0.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^26.4.4",
|
||||
"tsdx": "^0.13.2",
|
||||
@@ -500,19 +502,14 @@
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"detect-newline": "^3.1.0",
|
||||
"fast-array-diff": "^1.0.1",
|
||||
"github-slugger": "^1.4.0",
|
||||
"glob": "^7.1.6",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.12.0",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-wiki-link": "^0.0.4",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"replace-ext": "^2.0.0",
|
||||
"title-case": "^3.0.2",
|
||||
"unified": "^9.0.0",
|
||||
"unist-util-visit": "^2.0.2",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { FileDataStore } from '../../test/test-datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -20,11 +20,13 @@ describe('generateHeadings', () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const dataStore = new FileDataStore(
|
||||
readFileFromFs,
|
||||
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
|
||||
);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
|
||||
});
|
||||
|
||||
it.skip('should add heading to a file that does not have them', async () => {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { EOL } from 'os';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { FileDataStore } from '../../test/test-datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -23,14 +23,16 @@ describe('generateLinkReferences', () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
const readFile = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
const dataStore = new FileDataStore(
|
||||
readFile,
|
||||
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
|
||||
);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider]);
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore, IMatcher } from '../services/datastore';
|
||||
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { ResourceParser } from './note';
|
||||
@@ -22,14 +22,18 @@ export interface Foam extends IDisposable {
|
||||
|
||||
export const bootstrap = async (
|
||||
matcher: IMatcher,
|
||||
watcher: IWatcher | undefined,
|
||||
dataStore: IDataStore,
|
||||
parser: ResourceParser,
|
||||
initialProviders: ResourceProvider[]
|
||||
) => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const tsStart = Date.now();
|
||||
|
||||
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
|
||||
const workspace = await FoamWorkspace.fromProviders(
|
||||
initialProviders,
|
||||
dataStore
|
||||
);
|
||||
|
||||
const tsWsDone = Date.now();
|
||||
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
|
||||
|
||||
@@ -41,13 +45,28 @@ export const bootstrap = async (
|
||||
const tsTagsEnd = Date.now();
|
||||
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
|
||||
|
||||
watcher?.onDidChange(async uri => {
|
||||
if (matcher.isMatch(uri)) {
|
||||
await workspace.fetchAndSet(uri);
|
||||
}
|
||||
});
|
||||
watcher?.onDidCreate(async uri => {
|
||||
await matcher.refresh();
|
||||
if (matcher.isMatch(uri)) {
|
||||
await workspace.fetchAndSet(uri);
|
||||
}
|
||||
});
|
||||
watcher?.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
});
|
||||
|
||||
const foam: Foam = {
|
||||
workspace,
|
||||
graph,
|
||||
tags,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
dataStore,
|
||||
matcher,
|
||||
},
|
||||
dispose: () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { URI } from './uri';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
|
||||
export interface ResourceProvider extends IDisposable {
|
||||
init: (workspace: FoamWorkspace) => Promise<void>;
|
||||
supports: (uri: URI) => boolean;
|
||||
readAsMarkdown: (uri: URI) => Promise<string | null>;
|
||||
fetch: (uri: URI) => Promise<Resource | null>;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isSome } from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore } from '../services/datastore';
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -23,7 +24,6 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
return provider.init(this);
|
||||
}
|
||||
|
||||
set(resource: Resource) {
|
||||
@@ -159,6 +159,20 @@ export class FoamWorkspace implements IDisposable {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a resource URI, and adds it to the workspace as a resource.
|
||||
* If the URI is not supported by any provider or is not found, it will not
|
||||
* add anything to the workspace, and return null.
|
||||
*
|
||||
* @param uri the URI where the resource is located
|
||||
* @returns A promise to the Resource, or null if none was found
|
||||
*/
|
||||
public async fetchAndSet(uri: URI): Promise<Resource | null> {
|
||||
const resource = await this.fetch(uri);
|
||||
resource && this.set(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
public readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
for (const provider of this.providers) {
|
||||
if (provider.supports(uri)) {
|
||||
@@ -220,12 +234,13 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
static async fromProviders(
|
||||
providers: ResourceProvider[]
|
||||
providers: ResourceProvider[],
|
||||
dataStore: IDataStore
|
||||
): Promise<FoamWorkspace> {
|
||||
const workspace = new FoamWorkspace();
|
||||
for (const provider of providers) {
|
||||
await workspace.registerProvider(provider);
|
||||
}
|
||||
await Promise.all(providers.map(p => workspace.registerProvider(p)));
|
||||
const files = await dataStore.list();
|
||||
await Promise.all(files.map(f => workspace.fetchAndSet(f)));
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Resource, ResourceLink } from '../model/note';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
@@ -37,51 +35,6 @@ const asResource = (uri: URI): Resource => {
|
||||
export class AttachmentResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly watcher?: IWatcher
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob =>
|
||||
this.dataStore.list(glob, this.matcher.exclude)
|
||||
)
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
Logger.info(
|
||||
`Found ${
|
||||
files.length
|
||||
} attachments, with extensions: ${attachmentExtensions.join(', ')}`
|
||||
);
|
||||
for (const uri of files) {
|
||||
Logger.debug('Found: ' + uri.toString());
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
|
||||
if (this.watcher != null) {
|
||||
this.disposables = [
|
||||
this.watcher.onDidChange(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidCreate(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidDelete(uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return attachmentExtensions.includes(
|
||||
uri.getExtension().toLocaleLowerCase()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { Matcher, toMatcherPathFormat } from '../../test/test-datastore';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -83,11 +83,3 @@ describe('Matcher', () => {
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Datastore', () => {
|
||||
it('uses the matcher to get the file list', async () => {
|
||||
const matcher = new Matcher([testFolder], ['**/*.md'], []);
|
||||
const ds = new FileDataStore(readFileFromFs);
|
||||
expect((await ds.list(matcher.include[0])).length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import micromatch from 'micromatch';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { glob } from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { isWindows } from '../common/platform';
|
||||
import { Event } from '../common/event';
|
||||
import { asAbsolutePaths } from '../utils/path';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files matching the given glob from the
|
||||
* store
|
||||
*/
|
||||
list: () => Promise<URI[]>;
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*
|
||||
* Returns `null` in case of errors while reading
|
||||
*/
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
}
|
||||
|
||||
export interface IWatcher {
|
||||
onDidChange: Event<URI>;
|
||||
onDidCreate: Event<URI>;
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
export interface IMatcher {
|
||||
/**
|
||||
@@ -25,6 +42,14 @@ export interface IMatcher {
|
||||
*/
|
||||
isMatch(uri: URI): boolean;
|
||||
|
||||
/**
|
||||
* Refreshes the list of files that this matcher matches
|
||||
* To be used when new files are added to the workspace,
|
||||
* it can be a more or less expensive operation depending on the
|
||||
* implementation of the matcher
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
|
||||
/**
|
||||
* The include globs
|
||||
*/
|
||||
@@ -36,98 +61,14 @@ export interface IMatcher {
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The matcher requires the path to be in unix format, so if we are in windows
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
|
||||
: (uri: URI) => uri.toFsPath();
|
||||
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
: (path: string): string => path;
|
||||
|
||||
export class Matcher implements IMatcher {
|
||||
public readonly folders: string[];
|
||||
public readonly include: string[] = [];
|
||||
public readonly exclude: string[] = [];
|
||||
|
||||
export class GenericDataStore implements IDataStore {
|
||||
constructor(
|
||||
baseFolders: URI[],
|
||||
includeGlobs: string[] = ['**/*'],
|
||||
excludeGlobs: string[] = []
|
||||
) {
|
||||
this.folders = baseFolders.map(toMatcherPathFormat);
|
||||
Logger.info('Workspace folders: ', this.folders);
|
||||
private readonly listFiles: () => Promise<URI[]>,
|
||||
private readFile: (uri: URI) => Promise<string>
|
||||
) {}
|
||||
|
||||
this.include = includeGlobs.flatMap(glob =>
|
||||
asAbsolutePaths(glob, this.folders)
|
||||
);
|
||||
this.exclude = excludeGlobs.flatMap(glob =>
|
||||
asAbsolutePaths(glob, this.folders)
|
||||
);
|
||||
|
||||
Logger.info('Glob patterns', {
|
||||
includeGlobs: this.include,
|
||||
ignoreGlobs: this.exclude,
|
||||
});
|
||||
}
|
||||
|
||||
match(files: URI[]) {
|
||||
const matches = micromatch(
|
||||
files.map(f => f.toFsPath()),
|
||||
this.include,
|
||||
{
|
||||
ignore: this.exclude,
|
||||
nocase: true,
|
||||
format: toFsPath,
|
||||
}
|
||||
);
|
||||
return matches.map(URI.file);
|
||||
}
|
||||
|
||||
isMatch(uri: URI) {
|
||||
return this.match([uri]).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWatcher {
|
||||
onDidChange: Event<URI>;
|
||||
onDidCreate: Event<URI>;
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files matching the given glob from the
|
||||
* store
|
||||
*/
|
||||
list: (glob: string, ignoreGlob?: string | string[]) => Promise<URI[]>;
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*
|
||||
* Returns `null` in case of errors while reading
|
||||
*/
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
constructor(private readFile: (uri: URI) => Promise<string>) {}
|
||||
|
||||
async list(glob: string, ignoreGlob?: string | string[]): Promise<URI[]> {
|
||||
const res = await findAllFiles(glob, {
|
||||
ignore: ignoreGlob,
|
||||
strict: false,
|
||||
});
|
||||
return res.map(URI.file);
|
||||
async list(): Promise<URI[]> {
|
||||
return this.listFiles();
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
@@ -141,3 +82,75 @@ export class FileDataStore 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
|
||||
* this matcher, far from ideal but to be refactored later
|
||||
*/
|
||||
export class FileListBasedMatcher implements IMatcher {
|
||||
private files: string[] = [];
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
|
||||
constructor(files: URI[], private readonly listFiles: () => Promise<URI[]>) {
|
||||
this.files = files.map(f => f.path);
|
||||
}
|
||||
|
||||
match(files: URI[]): URI[] {
|
||||
return files.filter(f => this.files.includes(f.path));
|
||||
}
|
||||
|
||||
isMatch(uri: URI): boolean {
|
||||
return this.files.includes(uri.path);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.files = (await this.listFiles()).map(f => f.path);
|
||||
}
|
||||
|
||||
static async createFromListFn(listFiles: () => Promise<URI[]>) {
|
||||
const files = await listFiles();
|
||||
return new FileListBasedMatcher(files, listFiles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A matcher that includes all URIs passed to it
|
||||
*/
|
||||
export class AlwaysIncludeMatcher implements IMatcher {
|
||||
include: string[] = ['**/*'];
|
||||
exclude: string[] = [];
|
||||
match(files: URI[]): URI[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
isMatch(uri: URI): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class SubstringExcludeMatcher implements IMatcher {
|
||||
include: string[] = ['**/*'];
|
||||
exclude: string[] = [];
|
||||
constructor(exclude: string) {
|
||||
this.exclude = [exclude];
|
||||
}
|
||||
|
||||
match(files: URI[]): URI[] {
|
||||
return files.filter(f => this.isMatch(f));
|
||||
}
|
||||
|
||||
isMatch(uri: URI): boolean {
|
||||
return !uri.path.includes(this.exclude[0]);
|
||||
}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,64 +8,19 @@ import { isNone, isSome } from '../utils';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, IMatcher, IWatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { MarkdownLink } from './markdown-link';
|
||||
import { IDataStore } from './datastore';
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly parser: ResourceParser,
|
||||
private readonly watcher?: IWatcher
|
||||
private readonly parser: ResourceParser
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob =>
|
||||
this.dataStore.list(glob, this.matcher.exclude)
|
||||
)
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.debug('Found: ' + uri.toString());
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (this.watcher != null) {
|
||||
this.disposables = [
|
||||
this.watcher.onDidChange(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidCreate(async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
}),
|
||||
this.watcher.onDidDelete(uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return uri.isMarkdown();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
const HASHTAG_REGEX = /(?<=^|\s)#([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX = /(?<=^|\s)([0-9]*[\p{L}\p{Emoji_Presentation}/_-][\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/gmu;
|
||||
|
||||
export const extractHashtags = (
|
||||
text: string
|
||||
|
||||
@@ -7,11 +7,13 @@ describe('hashtag extraction', () => {
|
||||
it('returns empty list if no tags are present', () => {
|
||||
expect(extractHashtags('hello world')).toEqual([]);
|
||||
});
|
||||
|
||||
it('works with simple strings', () => {
|
||||
expect(
|
||||
extractHashtags('hello #world on #this planet').map(t => t.label)
|
||||
).toEqual(['world', 'this']);
|
||||
});
|
||||
|
||||
it('detects the offset of the tag', () => {
|
||||
expect(extractHashtags('#hello')).toEqual([{ label: 'hello', offset: 0 }]);
|
||||
expect(extractHashtags(' #hello')).toEqual([{ label: 'hello', offset: 1 }]);
|
||||
@@ -19,21 +21,25 @@ describe('hashtag extraction', () => {
|
||||
{ label: 'hello', offset: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('works with tags at beginning or end of text', () => {
|
||||
expect(
|
||||
extractHashtags('#hello world on this #planet').map(t => t.label)
|
||||
).toEqual(['hello', 'planet']);
|
||||
});
|
||||
|
||||
it('supports _ and -', () => {
|
||||
expect(
|
||||
extractHashtags('#hello-world on #this_planet').map(t => t.label)
|
||||
).toEqual(['hello-world', 'this_planet']);
|
||||
});
|
||||
|
||||
it('supports nested tags', () => {
|
||||
expect(
|
||||
extractHashtags('#parent/child on #planet').map(t => t.label)
|
||||
).toEqual(['parent/child', 'planet']);
|
||||
});
|
||||
|
||||
it('ignores tags that only have numbers in text', () => {
|
||||
expect(
|
||||
extractHashtags('this #123 tag should be ignore, but not #123four').map(
|
||||
@@ -41,7 +47,8 @@ describe('hashtag extraction', () => {
|
||||
)
|
||||
).toEqual(['123four']);
|
||||
});
|
||||
it('supports unicode letters like Chinese charaters', () => {
|
||||
|
||||
it('supports unicode letters like Chinese characters', () => {
|
||||
expect(
|
||||
extractHashtags(`
|
||||
this #tag_with_unicode_letters_汉字, pure Chinese tag like #纯中文标签 and
|
||||
@@ -55,6 +62,24 @@ describe('hashtag extraction', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports emoji tags', () => {
|
||||
expect(
|
||||
extractHashtags(`this is a pure emoji #⭐, #⭐⭐, #👍👍🏽👍🏿 some mixed emoji #π🥧, #✅todo
|
||||
#urgent❗ or #❗❗urgent, and some nested emoji #📥/🟥 or #📥/🟢
|
||||
`).map(t => t.label)
|
||||
).toEqual([
|
||||
'⭐',
|
||||
'⭐⭐',
|
||||
'👍👍🏽👍🏿',
|
||||
'π🥧',
|
||||
'✅todo',
|
||||
'urgent❗',
|
||||
'❗❗urgent',
|
||||
'📥/🟥',
|
||||
'📥/🟢',
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores hashes in plain text urls and links', () => {
|
||||
expect(
|
||||
extractHashtags(`
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { workspace, ExtensionContext, window, commands } from 'vscode';
|
||||
import { MarkdownResourceProvider } from './core/services/markdown-provider';
|
||||
import { bootstrap } from './core/model/foam';
|
||||
import { URI } from './core/model/uri';
|
||||
import { FileDataStore, Matcher } from './core/services/datastore';
|
||||
import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
|
||||
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';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
@@ -28,33 +26,30 @@ export async function activate(context: ExtensionContext) {
|
||||
}
|
||||
|
||||
// Prepare Foam
|
||||
const readFile = async (uri: URI) =>
|
||||
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
const matcher = new Matcher(
|
||||
workspace.workspaceFolders.map(dir => fromVsCodeUri(dir.uri)),
|
||||
['**/*'],
|
||||
getIgnoredFilesSetting().map(g => g.toString())
|
||||
);
|
||||
const excludes = getIgnoredFilesSetting().map(g => g.toString());
|
||||
const {
|
||||
matcher,
|
||||
dataStore,
|
||||
excludePatterns,
|
||||
} = await createMatcherAndDataStore(excludes);
|
||||
|
||||
Logger.info('Loading from directories:');
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
Logger.info('- ' + folder.uri.fsPath);
|
||||
Logger.info(' Include: **/*');
|
||||
Logger.info(' Exclude: ' + excludePatterns.get(folder.name).join(','));
|
||||
}
|
||||
|
||||
const watcher = new VsCodeWatcher(
|
||||
workspace.createFileSystemWatcher('**/*')
|
||||
);
|
||||
const parserCache = new VsCodeBasedParserCache(context);
|
||||
const parser = createMarkdownParser([], parserCache);
|
||||
|
||||
const markdownProvider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
parser,
|
||||
watcher
|
||||
);
|
||||
const attachmentProvider = new AttachmentResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
watcher
|
||||
);
|
||||
const markdownProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
const attachmentProvider = new AttachmentResourceProvider();
|
||||
|
||||
const foamPromise = bootstrap(matcher, dataStore, parser, [
|
||||
const foamPromise = bootstrap(matcher, watcher, dataStore, parser, [
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
]);
|
||||
@@ -64,6 +59,7 @@ export async function activate(context: ExtensionContext) {
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
|
||||
|
||||
context.subscriptions.push(
|
||||
foam,
|
||||
watcher,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FoamFeature } from '../../types';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import {
|
||||
askUserForTemplate,
|
||||
getDefaultTemplateUri,
|
||||
getPathFromTitle,
|
||||
NoteFactory,
|
||||
} from '../../services/templates';
|
||||
@@ -67,8 +68,9 @@ async function createNote(args: CreateNoteArgs) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
templateUri =
|
||||
args.templatePath && asAbsoluteWorkspaceUri(URI.file(args.templatePath));
|
||||
templateUri = args.templatePath
|
||||
? asAbsoluteWorkspaceUri(URI.file(args.templatePath))
|
||||
: getDefaultTemplateUri();
|
||||
}
|
||||
|
||||
if (await fileExists(templateUri)) {
|
||||
|
||||
@@ -3,30 +3,26 @@ import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FileDataStore, Matcher } from '../core/services/datastore';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
showInEditor,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { HoverProvider } from './hover-provider';
|
||||
import { readFileFromFs } from '../test/test-utils';
|
||||
import { FileDataStore } from '../test/test-datastore';
|
||||
|
||||
// We can't use createTestWorkspace from /packages/foam-vscode/src/test/test-utils.ts
|
||||
// because we need a MarkdownResourceProvider with a real instance of FileDataStore.
|
||||
const createWorkspace = () => {
|
||||
const matcher = new Matcher(
|
||||
vscode.workspace.workspaceFolders.map(f => fromVsCodeUri(f.uri))
|
||||
const dataStore = new FileDataStore(
|
||||
readFileFromFs,
|
||||
vscode.workspace.workspaceFolders[0].uri.fsPath
|
||||
);
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const parser = createMarkdownParser();
|
||||
const resourceProvider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
parser
|
||||
);
|
||||
const resourceProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
const workspace = new FoamWorkspace();
|
||||
workspace.registerProvider(resourceProvider);
|
||||
return workspace;
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('Document navigation', () => {
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should create links for wikilinks', async () => {
|
||||
it('should not create links for wikilinks, as this is managed by the definition provider', async () => {
|
||||
const fileA = await createFile('# File A', ['file-a.md']);
|
||||
const fileB = await createFile(`this is a link to [[${fileA.name}]].`);
|
||||
const ws = createTestWorkspace()
|
||||
@@ -66,9 +66,7 @@ describe('Document navigation', () => {
|
||||
const provider = new NavigationProvider(ws, graph, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(fileA.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 26));
|
||||
expect(links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should create links for placeholders', async () => {
|
||||
|
||||
@@ -159,22 +159,23 @@ export class NavigationProvider
|
||||
})
|
||||
);
|
||||
|
||||
return targets.map(o => {
|
||||
const command = OPEN_COMMAND.asURI(o.target);
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
o.link.range.start.line,
|
||||
o.link.range.start.character + 2,
|
||||
o.link.range.end.line,
|
||||
o.link.range.end.character - 2
|
||||
),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = o.target.isPlaceholder()
|
||||
? `Create note for '${o.target.path}'`
|
||||
: `Go to ${o.target.toFsPath()}`;
|
||||
return documentLink;
|
||||
});
|
||||
return targets
|
||||
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
|
||||
.map(o => {
|
||||
const command = OPEN_COMMAND.asURI(o.target);
|
||||
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
o.link.range.start.line,
|
||||
o.link.range.start.character + 2,
|
||||
o.link.range.end.line,
|
||||
o.link.range.end.character - 2
|
||||
),
|
||||
command
|
||||
);
|
||||
documentLink.tooltip = `Create note for '${o.target.path}'`;
|
||||
return documentLink;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
|
||||
import { isOrphan } from './orphans';
|
||||
|
||||
const orphanA = createTestNote({
|
||||
uri: '/path/orphan-a.md',
|
||||
title: 'Orphan A',
|
||||
});
|
||||
|
||||
const nonOrphan1 = createTestNote({
|
||||
uri: '/path/non-orphan-1.md',
|
||||
});
|
||||
|
||||
const nonOrphan2 = createTestNote({
|
||||
uri: '/path/non-orphan-2.md',
|
||||
links: [{ slug: 'non-orphan-1' }],
|
||||
});
|
||||
|
||||
const workspace = createTestWorkspace()
|
||||
.set(orphanA)
|
||||
.set(nonOrphan1)
|
||||
.set(nonOrphan2);
|
||||
const graph = FoamGraph.fromWorkspace(workspace);
|
||||
|
||||
describe('isOrphan', () => {
|
||||
it('should return true when a note with no connections is provided', () => {
|
||||
expect(isOrphan(orphanA.uri, graph)).toBeTruthy();
|
||||
});
|
||||
it('should return false when a note with connections is provided', () => {
|
||||
expect(isOrphan(nonOrphan1.uri, graph)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getOrphansConfig } from '../../settings';
|
||||
import { FoamFeature } from '../../types';
|
||||
import {
|
||||
@@ -9,8 +8,8 @@ import {
|
||||
ResourceTreeItem,
|
||||
UriTreeItem,
|
||||
} from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
|
||||
const EXCLUDE_TYPES = ['image', 'attachment'];
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -18,34 +17,45 @@ const feature: FoamFeature = {
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
|
||||
fromVsCodeUri(dir.uri)
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getOrphansConfig().exclude
|
||||
);
|
||||
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'orphans',
|
||||
'orphan',
|
||||
getOrphansConfig(),
|
||||
workspacesURIs,
|
||||
() => foam.graph.getAllNodes().filter(uri => isOrphan(uri, foam.graph)),
|
||||
() =>
|
||||
foam.graph
|
||||
.getAllNodes()
|
||||
.filter(
|
||||
uri =>
|
||||
!EXCLUDE_TYPES.includes(foam.workspace.find(uri)?.type) &&
|
||||
foam.graph.getConnections(uri).length === 0
|
||||
),
|
||||
uri => {
|
||||
if (uri.isPlaceholder()) {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
const resource = foam.workspace.find(uri);
|
||||
return new ResourceTreeItem(resource, foam.workspace);
|
||||
}
|
||||
return uri.isPlaceholder()
|
||||
? new UriTreeItem(uri)
|
||||
: new ResourceTreeItem(foam.workspace.find(uri), foam.workspace);
|
||||
},
|
||||
matcher
|
||||
);
|
||||
provider.setGroupBy(getOrphansConfig().groupBy);
|
||||
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.orphans', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
|
||||
...provider.commands,
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
foam.graph.onDidUpdate(() => {
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
provider.refresh();
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const isOrphan = (uri: URI, graph: FoamGraph) =>
|
||||
graph.getConnections(uri).length === 0;
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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,
|
||||
UriTreeItem,
|
||||
} from '../../utils/grouped-resources-tree-data-provider';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -14,27 +14,34 @@ const feature: FoamFeature = {
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const workspacesURIs = vscode.workspace.workspaceFolders.map(dir =>
|
||||
fromVsCodeUri(dir.uri)
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getPlaceholdersConfig().exclude
|
||||
);
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'placeholders',
|
||||
'placeholder',
|
||||
getPlaceholdersConfig(),
|
||||
workspacesURIs,
|
||||
() => foam.graph.getAllNodes().filter(uri => uri.isPlaceholder()),
|
||||
uri => {
|
||||
return new UriTreeItem(uri);
|
||||
}
|
||||
},
|
||||
matcher
|
||||
);
|
||||
provider.setGroupBy(getPlaceholdersConfig().groupBy);
|
||||
|
||||
const treeView = vscode.window.createTreeView('foam-vscode.placeholders', {
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
'foam-vscode.placeholders',
|
||||
provider
|
||||
),
|
||||
treeView,
|
||||
...provider.commands,
|
||||
foam.graph.onDidUpdate(() => provider.refresh())
|
||||
foam.graph.onDidUpdate(() => {
|
||||
treeView.title = baseTitle + ` (${provider.numElements})`;
|
||||
provider.refresh();
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { createTestNote, readFileFromFs } from '../../test/test-utils';
|
||||
import {
|
||||
createTestNote,
|
||||
readFileFromFs,
|
||||
TEST_DATA_DIR,
|
||||
} from '../../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagReference, TagsProvider } from './tags-explorer';
|
||||
import { bootstrap, Foam } from '../../core/model/foam';
|
||||
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
|
||||
import { FileDataStore, Matcher } from '../../core/services/datastore';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { FileDataStore, Matcher } from '../../test/test-datastore';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const matcher = new Matcher([URI.file('/root')]);
|
||||
const dataStore = new FileDataStore(readFileFromFs, TEST_DATA_DIR.toFsPath());
|
||||
const matcher = new Matcher([URI.file(TEST_DATA_DIR.toFsPath())]);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore, parser);
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
@@ -26,7 +30,9 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(matcher, dataStore, parser, [mdProvider]);
|
||||
_foam = await bootstrap(matcher, undefined, dataStore, parser, [
|
||||
mdProvider,
|
||||
]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
@@ -15,11 +15,18 @@ const feature: FoamFeature = {
|
||||
) => {
|
||||
const foam = await foamPromise;
|
||||
const provider = new TagsProvider(foam, 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(
|
||||
vscode.window.registerTreeDataProvider(
|
||||
'foam-vscode.tags-explorer',
|
||||
provider
|
||||
)
|
||||
treeView,
|
||||
foam.tags.onDidUpdate(() => {
|
||||
treeView.title = baseTitle + ` (${foam.tags.tags.size})`;
|
||||
})
|
||||
);
|
||||
foam.tags.onDidUpdate(() => provider.refresh());
|
||||
},
|
||||
@@ -29,9 +36,12 @@ 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>();
|
||||
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;
|
||||
readonly onDidChangeTreeData: vscode.Event<TagTreeItem | undefined | void> =
|
||||
this._onDidChangeTreeData.event;
|
||||
|
||||
private tags: {
|
||||
tag: string;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
FileType,
|
||||
RelativePattern,
|
||||
Selection,
|
||||
SnippetString,
|
||||
TextDocument,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
@@ -13,6 +16,13 @@ import {
|
||||
import { focusNote } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { isSome } from '../core/utils';
|
||||
import {
|
||||
AlwaysIncludeMatcher,
|
||||
FileListBasedMatcher,
|
||||
GenericDataStore,
|
||||
IDataStore,
|
||||
IMatcher,
|
||||
} from '../core/services/datastore';
|
||||
|
||||
interface SelectionInfo {
|
||||
document: TextDocument;
|
||||
@@ -124,3 +134,54 @@ export function asAbsoluteWorkspaceUri(uri: URI): URI {
|
||||
const res = asAbsoluteUri(uri, folders);
|
||||
return res;
|
||||
}
|
||||
|
||||
export const createMatcherAndDataStore = async (
|
||||
excludes: string[]
|
||||
): Promise<{
|
||||
matcher: IMatcher;
|
||||
dataStore: IDataStore;
|
||||
excludePatterns: Map<string, string[]>;
|
||||
}> => {
|
||||
const excludePatterns = new Map<string, string[]>();
|
||||
workspace.workspaceFolders.forEach(f => excludePatterns.set(f.name, []));
|
||||
|
||||
for (const exclude of excludes) {
|
||||
const tokens = exclude.split('/');
|
||||
const matchesFolder = workspace.workspaceFolders.find(
|
||||
f => f.name === tokens[0]
|
||||
);
|
||||
if (matchesFolder) {
|
||||
excludePatterns.get(tokens[0]).push(tokens.slice(1).join('/'));
|
||||
} else {
|
||||
for (const [, value] of excludePatterns.entries()) {
|
||||
value.push(exclude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listFiles = async () => {
|
||||
let files: Uri[] = [];
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
const uris = await workspace.findFiles(
|
||||
new RelativePattern(folder.uri.path, '**/*'),
|
||||
new RelativePattern(
|
||||
folder.uri.path,
|
||||
`{${excludePatterns.get(folder.name).join(',')}}`
|
||||
)
|
||||
);
|
||||
files = [...files, ...uris];
|
||||
}
|
||||
|
||||
return files.map(fromVsCodeUri);
|
||||
};
|
||||
|
||||
const readFile = async (uri: URI) =>
|
||||
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
|
||||
|
||||
const dataStore = new GenericDataStore(listFiles, readFile);
|
||||
const matcher = isEmpty(excludes)
|
||||
? new AlwaysIncludeMatcher()
|
||||
: await FileListBasedMatcher.createFromListFn(listFiles);
|
||||
|
||||
return { matcher, dataStore, excludePatterns };
|
||||
};
|
||||
|
||||
85
packages/foam-vscode/src/test/test-datastore.test.ts
Normal file
85
packages/foam-vscode/src/test/test-datastore.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { Matcher, toMatcherPathFormat } from './test-datastore';
|
||||
import { TEST_DATA_DIR } from './test-utils';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = TEST_DATA_DIR.joinPath('test-datastore');
|
||||
|
||||
describe('Matcher', () => {
|
||||
it('generates globs with the base dir provided', () => {
|
||||
const matcher = new Matcher([testFolder], ['*'], []);
|
||||
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(testFolder.joinPath('*')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to including everything and excluding nothing', () => {
|
||||
const matcher = new Matcher([testFolder]);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(testFolder.joinPath('**', '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports multiple includes', () => {
|
||||
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(testFolder.joinPath('g1')),
|
||||
toMatcherPathFormat(testFolder.joinPath('g2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a match method to filter strings', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.match(files)).toEqual([
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a isMatch method to see whether a file is matched or not', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(true);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
|
||||
it('happy path', () => {
|
||||
const matcher = new Matcher([URI.file('/root/')], ['**/*'], ['**/*.pdf']);
|
||||
expect(matcher.isMatch(URI.file('/root/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/root/file.pdf'))).toBeFalsy();
|
||||
expect(matcher.isMatch(URI.file('/root/dir/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/root/dir/file.pdf'))).toBeFalsy();
|
||||
});
|
||||
|
||||
it('ignores files in the exclude list', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
|
||||
const files = [
|
||||
testFolder.joinPath('file1.md'),
|
||||
testFolder.joinPath('file2.md'),
|
||||
testFolder.joinPath('file3.mdx'),
|
||||
testFolder.joinPath('sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(false);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
});
|
||||
96
packages/foam-vscode/src/test/test-datastore.ts
Normal file
96
packages/foam-vscode/src/test/test-datastore.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import micromatch from 'micromatch';
|
||||
import { promisify } from 'util';
|
||||
import { glob } from 'glob';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { IDataStore, IMatcher } from '../core/services/datastore';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { isWindows } from '../core/common/platform';
|
||||
import { asAbsolutePaths } from '../core/utils/path';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
constructor(
|
||||
private readFile: (uri: URI) => Promise<string>,
|
||||
private readonly basedir: string
|
||||
) {}
|
||||
|
||||
async list(): Promise<URI[]> {
|
||||
const res = await findAllFiles([this.basedir, '**/*'].join('/'));
|
||||
return res.map(URI.file);
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
try {
|
||||
return await this.readFile(uri);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The matcher requires the path to be in unix format, so if we are in windows
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => uri.toFsPath().replace(/\\/g, '/')
|
||||
: (uri: URI) => uri.toFsPath();
|
||||
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
: (path: string): string => path;
|
||||
|
||||
export class Matcher implements IMatcher {
|
||||
public readonly folders: string[];
|
||||
public readonly include: string[] = [];
|
||||
public readonly exclude: string[] = [];
|
||||
|
||||
constructor(
|
||||
baseFolders: URI[],
|
||||
includeGlobs: string[] = ['**/*'],
|
||||
excludeGlobs: string[] = []
|
||||
) {
|
||||
this.folders = baseFolders.map(toMatcherPathFormat);
|
||||
Logger.info('Workspace folders: ', this.folders);
|
||||
|
||||
this.include = includeGlobs.flatMap(glob =>
|
||||
asAbsolutePaths(glob, this.folders)
|
||||
);
|
||||
this.exclude = excludeGlobs.flatMap(glob =>
|
||||
asAbsolutePaths(glob, this.folders)
|
||||
);
|
||||
|
||||
Logger.info('Glob patterns', {
|
||||
includeGlobs: this.include,
|
||||
ignoreGlobs: this.exclude,
|
||||
});
|
||||
}
|
||||
|
||||
match(files: URI[]) {
|
||||
const matches = micromatch(
|
||||
files.map(f => f.toFsPath()),
|
||||
this.include,
|
||||
{
|
||||
ignore: this.exclude,
|
||||
nocase: true,
|
||||
format: toFsPath,
|
||||
}
|
||||
);
|
||||
return matches.map(URI.file);
|
||||
}
|
||||
|
||||
isMatch(uri: URI) {
|
||||
return this.match([uri]).length > 0;
|
||||
}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ Logger.setLevel('error');
|
||||
|
||||
export const cleanWorkspace = async () => {
|
||||
const files = await vscode.workspace.findFiles('**', '{.vscode,.keep}');
|
||||
await Promise.all(files.map(f => vscode.workspace.fs.delete(f)));
|
||||
await Promise.all(files.map(f => deleteFile(fromVsCodeUri(f))));
|
||||
};
|
||||
|
||||
export const showInEditor = async (uri: URI) => {
|
||||
@@ -28,9 +28,15 @@ export const closeEditors = async () => {
|
||||
await wait(100);
|
||||
};
|
||||
|
||||
export const deleteFile = (file: URI | { uri: URI }) => {
|
||||
export const deleteFile = async (file: URI | { uri: URI }) => {
|
||||
const uri = 'uri' in file ? file.uri : file;
|
||||
return vscode.workspace.fs.delete(toVsCodeUri(uri), { recursive: true });
|
||||
try {
|
||||
await vscode.workspace.fs.delete(toVsCodeUri(uri), {
|
||||
recursive: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Logger } from '../core/utils/log';
|
||||
import { Range } from '../core/model/range';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Matcher } from '../core/services/datastore';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { NoteLinkDefinition, Resource } from '../core/model/note';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
@@ -32,13 +31,11 @@ export const strToUri = URI.file;
|
||||
|
||||
export const createTestWorkspace = () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*']);
|
||||
const parser = createMarkdownParser();
|
||||
const provider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
{
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
list: () => Promise.resolve([]),
|
||||
},
|
||||
parser
|
||||
);
|
||||
|
||||
@@ -8,11 +8,9 @@ import {
|
||||
workspace,
|
||||
Selection,
|
||||
MarkdownString,
|
||||
version,
|
||||
ViewColumn,
|
||||
} from 'vscode';
|
||||
import matter from 'gray-matter';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
import { toVsCodeUri } from './utils/vsc-utils';
|
||||
import { Logger } from './core/utils/log';
|
||||
import { URI } from './core/model/uri';
|
||||
@@ -179,14 +177,8 @@ export function getContainsTooltip(titles: string[]): string {
|
||||
* @param note A Foam Note
|
||||
*/
|
||||
export function getNoteTooltip(content: string): string {
|
||||
const STABLE_MARKDOWN_STRING_API_VERSION = '1.52.1';
|
||||
const strippedContent = stripFrontMatter(stripImages(content));
|
||||
|
||||
if (version >= STABLE_MARKDOWN_STRING_API_VERSION) {
|
||||
return formatMarkdownTooltip(strippedContent) as any;
|
||||
}
|
||||
|
||||
return formatSimpleTooltip(strippedContent);
|
||||
return formatMarkdownTooltip(strippedContent) as any;
|
||||
}
|
||||
|
||||
export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
@@ -200,16 +192,6 @@ export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
return md;
|
||||
}
|
||||
|
||||
export function formatSimpleTooltip(content: string): string {
|
||||
const CHARACTERS_LIMIT = 200;
|
||||
const flatContent = removeMarkdown(content)
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
const extract = flatContent.substr(0, CHARACTERS_LIMIT);
|
||||
const ellipsis = flatContent.length > CHARACTERS_LIMIT ? '...' : '';
|
||||
return `${extract}${ellipsis}`;
|
||||
}
|
||||
|
||||
export function getExcerpt(
|
||||
markdown: string,
|
||||
maxLines: number
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import {
|
||||
GroupedResoucesConfigGroupBy,
|
||||
GroupedResourcesConfig,
|
||||
} from '../settings';
|
||||
import { createTestNote, strToUri } from '../test/test-utils';
|
||||
AlwaysIncludeMatcher,
|
||||
SubstringExcludeMatcher,
|
||||
} from '../core/services/datastore';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import { GroupedResoucesConfigGroupBy } from '../settings';
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import {
|
||||
DirectoryTreeItem,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
UriTreeItem,
|
||||
} from './grouped-resources-tree-data-provider';
|
||||
|
||||
const testMatcher = new SubstringExcludeMatcher('path-exclude');
|
||||
|
||||
describe('GroupedResourcesTreeDataProvider', () => {
|
||||
const matchingNote1 = createTestNote({ uri: '/path/ABC.md', title: 'ABC' });
|
||||
const matchingNote2 = createTestNote({
|
||||
@@ -32,25 +35,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
.set(excludedPathNote)
|
||||
.set(notMatchingNote);
|
||||
|
||||
// Mock config
|
||||
const config: GroupedResourcesConfig = {
|
||||
exclude: ['path-exclude/**/*'],
|
||||
groupBy: GroupedResoucesConfigGroupBy.Folder,
|
||||
};
|
||||
|
||||
it('should return the grouped resources as a folder tree', async () => {
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'length3',
|
||||
'note',
|
||||
config,
|
||||
[strToUri('')],
|
||||
() =>
|
||||
workspace
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
uri => new UriTreeItem(uri),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
@@ -72,15 +69,16 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'length3',
|
||||
'note',
|
||||
config,
|
||||
[strToUri('')],
|
||||
() =>
|
||||
workspace
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
uri => new UriTreeItem(uri),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
|
||||
|
||||
const directory = new DirectoryTreeItem(
|
||||
'/path',
|
||||
[new UriTreeItem(matchingNote1.uri)],
|
||||
@@ -98,22 +96,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
});
|
||||
|
||||
it('should return the flattened resources', async () => {
|
||||
const mockConfig = {
|
||||
...config,
|
||||
groupBy: GroupedResoucesConfigGroupBy.Off,
|
||||
};
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'length3',
|
||||
'note',
|
||||
mockConfig,
|
||||
[strToUri('')],
|
||||
() =>
|
||||
workspace
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
uri => new UriTreeItem(uri),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Off);
|
||||
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
@@ -132,19 +127,19 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
});
|
||||
|
||||
it('should return the grouped resources without exclusion', async () => {
|
||||
const mockConfig = { ...config, exclude: [] };
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'length3',
|
||||
'note',
|
||||
mockConfig,
|
||||
[strToUri('')],
|
||||
() =>
|
||||
workspace
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
uri => new UriTreeItem(uri),
|
||||
new AlwaysIncludeMatcher()
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
|
||||
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
expect.anything(),
|
||||
@@ -163,15 +158,15 @@ describe('GroupedResourcesTreeDataProvider', () => {
|
||||
const provider = new GroupedResourcesTreeDataProvider(
|
||||
'length3',
|
||||
description,
|
||||
config,
|
||||
[strToUri('')],
|
||||
() =>
|
||||
workspace
|
||||
.list()
|
||||
.filter(r => r.title.length === 3)
|
||||
.map(r => r.uri),
|
||||
uri => new UriTreeItem(uri)
|
||||
uri => new UriTreeItem(uri),
|
||||
testMatcher
|
||||
);
|
||||
provider.setGroupBy(GroupedResoucesConfigGroupBy.Folder);
|
||||
const result = await provider.getChildren();
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import micromatch from 'micromatch';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResoucesConfigGroupBy,
|
||||
} from '../settings';
|
||||
import { GroupedResoucesConfigGroupBy } from '../settings';
|
||||
import { getContainsTooltip, getNoteTooltip, isSome } from '../utils';
|
||||
import { OPEN_COMMAND } from '../features/commands/open-resource';
|
||||
import { toVsCodeUri } from './vsc-utils';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { IMatcher } from '../core/services/datastore';
|
||||
|
||||
/**
|
||||
* Provides the ability to expose a TreeDataExplorerView in VSCode. This class will
|
||||
@@ -82,13 +79,10 @@ export class GroupedResourcesTreeDataProvider
|
||||
constructor(
|
||||
private providerId: string,
|
||||
private resourceName: string,
|
||||
config: GroupedResourcesConfig,
|
||||
workspaceUris: URI[],
|
||||
private computeResources: () => Array<URI>,
|
||||
private createTreeItem: (item: URI) => GroupedResourceTreeItem
|
||||
private createTreeItem: (item: URI) => GroupedResourceTreeItem,
|
||||
private matcher: IMatcher
|
||||
) {
|
||||
this.groupBy = config.groupBy;
|
||||
this.exclude = this.getGlobs(workspaceUris, config.exclude);
|
||||
this.setContext();
|
||||
this.doComputeResources();
|
||||
}
|
||||
@@ -106,6 +100,10 @@ export class GroupedResourcesTreeDataProvider
|
||||
];
|
||||
}
|
||||
|
||||
public get numElements() {
|
||||
return this.flatUris.length;
|
||||
}
|
||||
|
||||
setGroupBy(groupBy: GroupedResoucesConfigGroupBy): void {
|
||||
this.groupBy = groupBy;
|
||||
this.setContext();
|
||||
@@ -163,30 +161,10 @@ export class GroupedResourcesTreeDataProvider
|
||||
|
||||
private doComputeResources(): void {
|
||||
this.flatUris = this.computeResources()
|
||||
.filter(uri => !this.isMatch(uri))
|
||||
.filter(uri => this.matcher.isMatch(uri))
|
||||
.filter(isSome);
|
||||
}
|
||||
|
||||
private isMatch(uri: URI) {
|
||||
return micromatch.isMatch(uri.toFsPath(), this.exclude);
|
||||
}
|
||||
|
||||
private getGlobs(fsURI: URI[], globs: string[]): string[] {
|
||||
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
||||
for (const fsPath of fsURI) {
|
||||
let folder = fsPath.path.replace(/\\/g, '/');
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
exclude.push(...globs.map(g => `${folder}/${g}`));
|
||||
}
|
||||
|
||||
return exclude;
|
||||
}
|
||||
|
||||
private getUrisByDirectory(): UrisByDirectory {
|
||||
const resourcesByDirectory: UrisByDirectory = {};
|
||||
for (const uri of this.flatUris) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
||||
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
|
||||
* Press `F5` to run the tests in a new window with your extension loaded.
|
||||
* See the output of the test result in the debug console.
|
||||
* Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||
11
readme.md
11
readme.md
@@ -5,7 +5,7 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://foambubble.github.io/join-discord/g)
|
||||
@@ -186,7 +186,7 @@ You can also browse the [docs folder](https://github.com/foambubble/foam/tree/ma
|
||||
Foam is licensed under the [MIT license](LICENSE).
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[Backlinking]: docs/features/backlinking.md "Backlinking"
|
||||
[Backlinking]: docs/user/features/backlinking.md "Backlinking"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
## Contributors ✨
|
||||
@@ -330,6 +330,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://www.readingsnail.pe.kr"><img src="https://avatars.githubusercontent.com/u/1904967?v=4?s=60" width="60px;" alt="Woosuk Park"/><br /><sub><b>Woosuk Park</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=readingsnail" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.dmurph.com"><img src="https://avatars.githubusercontent.com/u/294026?v=4?s=60" width="60px;" alt="Daniel Murphy"/><br /><sub><b>Daniel Murphy</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dmurph" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Dominic-DallOsto"><img src="https://avatars.githubusercontent.com/u/26859884?v=4?s=60" width="60px;" alt="Dominic D"/><br /><sub><b>Dominic D</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Dominic-DallOsto" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://elgirafo.xyz"><img src="https://avatars.githubusercontent.com/u/80516439?v=4?s=60" width="60px;" alt="luca"/><br /><sub><b>luca</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elgirafo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Lloyd-Jackman-UKPL"><img src="https://avatars.githubusercontent.com/u/55206370?v=4?s=60" width="60px;" alt="Lloyd Jackman"/><br /><sub><b>Lloyd Jackman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lloyd-Jackman-UKPL" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://sn3akiwhizper.github.io"><img src="https://avatars.githubusercontent.com/u/102705294?v=4?s=60" width="60px;" alt="sn3akiwhizper"/><br /><sub><b>sn3akiwhizper</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sn3akiwhizper" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jonathanpberger.com/"><img src="https://avatars.githubusercontent.com/u/41085?v=4?s=60" width="60px;" alt="jonathan berger"/><br /><sub><b>jonathan berger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jonathanpberger" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/badsketch"><img src="https://avatars.githubusercontent.com/u/8953212?v=4?s=60" width="60px;" alt="Daniel Wang"/><br /><sub><b>Daniel Wang</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=badsketch" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
27
yarn.lock
27
yarn.lock
@@ -2222,9 +2222,9 @@
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/braces@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb"
|
||||
integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b"
|
||||
integrity sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==
|
||||
|
||||
"@types/dateformat@^3.0.1":
|
||||
version "3.0.1"
|
||||
@@ -2340,9 +2340,9 @@
|
||||
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
|
||||
|
||||
"@types/micromatch@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
|
||||
integrity sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.2.tgz#ce29c8b166a73bf980a5727b1e4a4d099965151d"
|
||||
integrity sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==
|
||||
dependencies:
|
||||
"@types/braces" "*"
|
||||
|
||||
@@ -5087,11 +5087,6 @@ extsprintf@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||
|
||||
fast-array-diff@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-array-diff/-/fast-array-diff-1.0.1.tgz#463bacfeddaa3f5d56b79f6847fe322f15581c92"
|
||||
integrity sha512-pU83E/Y7+c/hRrDlmIiPYHy0Ugt+QypqzHKZI5qFOWMJAspWdmOyIeN/1FbdnGPlROp6FeGLLfhMO075DBqb4A==
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@@ -9548,11 +9543,6 @@ remark-wiki-link@^0.0.4:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
unist-util-map "^1.0.3"
|
||||
|
||||
remove-markdown@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98"
|
||||
integrity sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=
|
||||
|
||||
remove-trailing-separator@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||
@@ -9575,11 +9565,6 @@ repeating@^2.0.0:
|
||||
dependencies:
|
||||
is-finite "^1.0.0"
|
||||
|
||||
replace-ext@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06"
|
||||
integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==
|
||||
|
||||
request-promise-core@1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
|
||||
|
||||
Reference in New Issue
Block a user