Compare commits

..

No commits in common. 'main' and 'v0.10.0' have entirely different histories.

@ -1,19 +0,0 @@
title: "[Help] "
body:
- type: checkboxes
id: checks
attributes:
label: Verify
options:
- label: I searched the existing discussions for help
required: true
- type: textarea
id: help
attributes:
label: How can we help you?
validations:
required: true
- type: markdown
attributes:
value: |
:warning: Unfortunately, my time is limited and I can't offer reliable user support. I might answer if you catch me on a slow day, or hopefully someone else will.

@ -1,14 +0,0 @@
body:
- type: checkboxes
id: checks
attributes:
label: Verify
options:
- label: I searched the existing discussions for similar ideas
required: true
- type: textarea
id: help
attributes:
label: Share your idea or feature request
validations:
required: true

@ -1,65 +0,0 @@
name: Bug report
description: File a bug report to help improve zk.
body:
- type: markdown
attributes:
value: |
Thank you for filing a bug report!
- type: checkboxes
id: checks
attributes:
label: Check if applicable
description: |
:warning: My time is limited and if I don't plan on fixing the reported bug myself, I might close this issue. No hard feelings.
:heart: But if you would like to contribute a fix yourself, **I'll be happy to guide you through the codebase and review a pull request**.
options:
- label: I have searched the existing issues (**required**)
required: true
- label: I'm willing to help fix the problem and contribute a pull request
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: Also tell me, what did you expect to happen?
placeholder: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: bug-steps
attributes:
label: How to reproduce?
description: |
Step by step explanation to reproduce the issue.
If you can, drag and drop:
- a zipped sample notebook
- screenshots or a screencast showing the issue
placeholder: |
1. Add a note with the content "..."
2. Run `zk edit --interactive`
3. See error
...
validations:
required: true
- type: textarea
id: vim-config
attributes:
label: zk configuration
description: |
Paste the minimal `zk` configuration file (`.zk/config.toml`) reproducing the issue.
render: toml
validations:
required: true
- type: textarea
id: bug-environment
attributes:
label: Environment
description: |
Run the following shell commands and paste the result here:
```
zk --version && echo "system: `uname -srmo`"
```
placeholder: |
zk 0.13.0
system: Darwin 22.5.0 arm64
render: bash

@ -1,10 +0,0 @@
name: Feature request
description: Suggest an idea for this project.
body:
- type: checkboxes
id: checks
attributes:
label: If you have an idea, open a discussion
options:
- label: I will [create a new discussion](https://github.com/zk-org/zk/discussions/new?category=ideas) instead of an issue.

@ -1,13 +0,0 @@
name: User support
description: You need help?
body:
- type: markdown
attributes:
value: |
:warning: Unfortunately, my time is limited and I can't offer reliable user support. I might answer if you catch me on a slow day, or hopefully someone else will.
- type: checkboxes
id: checks
attributes:
label: If you need help, open a discussion
options:
- label: I will [create a new discussion](https://github.com/zk-org/zk/discussions/new?category=help) instead of an issue.

@ -1,16 +0,0 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
# Maintain dependencies for gomod
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
# Disable version updates for gomod dependencies
open-pull-requests-limit: 0

@ -11,14 +11,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
lfs: 'true'
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v2
with:
go-version-file: 'go.mod'
go-version: 1.16
- name: Install dependencies
run: |

@ -1,40 +0,0 @@
name: "CodeQL code scanning"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
# Wed 23:33 UTC
- cron: '33 23 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -1,43 +0,0 @@
name: Deploy to GitHub Pages
on:
release:
types:
- "published"
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Fix anchor tag in README
run: ex -s -c '%s/docs\/getting-started\.md/docs\/getting-started/|x' README.md
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

@ -1,17 +1,15 @@
name: Release
on:
workflow_dispatch:
push:
tags:
- 'v*'
release:
types: [created]
jobs:
homebrew:
runs-on: macos-latest
needs:
- release
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:

@ -16,7 +16,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v4
with:
days-before-stale: 30

3
.gitignore vendored

@ -11,9 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Delve debug files
*__debug_bin*
# Dependency directories (remove the comment below to include it)
# vendor/

@ -2,134 +2,22 @@
All notable changes to this project will be documented in this file.
## Unreleased
## 0.14.1
### Fixed
* Fixed parsing large notes @khimaros in https://github.com/zk-org/zk/pull/339
* fix day range parsing (zk-org/zk#382) by @tjex in https://github.com/zk-org/zk/pull/384
* accept tripple dash file URIs as valid links by @tjex in https://github.com/zk-org/zk/pull/391
* fix(lsp): fix trigger completion of zk LSP by @Rahlir in https://github.com/zk-org/zk/pull/397
* fix(lsp): ignore diagnostic check within code blocks by @Rahlir in https://github.com/zk-org/zk/pull/399
* allow notebook as hidden dir by @tjex in https://github.com/zk-org/zk/pull/402
## 0.14.0
### Added
* New [`tool.shell`](docs/tool-shell.md) configuration key to set a custom shell (contributed by [@lsvmello](https://github.com/zk-org/zk/pull/302)).
* New [`notebook.dir`](docs/config-notebook.md) configuration key to set the default notebook (contributed by [@lsvmello](https://github.com/zk-org/zk/pull/304)).
### Changed
* The `note.ignore` configuration property was renamed to `note.exclude`, to be more consistent with the CLI flags.
### Fixed
* Fixed LSP positions using UTF-16 offsets (contributed by [@wrvsrx](https://github.com/zk-org/zk/pull/317)).
## 0.13.0
### Added
* LSP:
* `zk.new` now returns the created note's content in its output (`content`), and has two new options:
* `dryRun` will prevent `zk.new` from creating the note on the file system.
* `insertContentAtLocation` can be used to insert the created note's content into an arbitrary location.
* A new `zk.link` command to insert a link to a given note (contributed by [@psanker](https://github.com/zk-org/zk/pull/284)).
## 0.12.0
### Added
* LSP: Support for external URLs with `documentLink`.
* New `{{date}}` template helper to obtain a date object from natural language (contributed by [@zalegrala](https://github.com/zk-org/zk/pull/262)).
```
Get a relative date using natural language:
{{date "next week"}}
Format a date returned by `get-date`:
{{format-date (date "monday") "timestamp"}}
```
* `zk list` now support multiple `--match`/`-m` flags, which allows to search for several tokens appearing in any order in the notes (contributed by [@rktjmp](https://github.com/zk-org/zk/pull/268)).
### Changed
* **Breaking change:** The `{{date}}` template helper was renamed to `{{format-date}}`. You might need to update your configuration and templates.
### Fixed
* [#243](https://github.com/zk-org/zk/issues/243) LSP: Fixed finding backlink references for notes in a folder.
* [#254](https://github.com/zk-org/zk/issues/254) Fixed SQL error when pairing `--link-to` and `--linked-by`.
## 0.11.1
### Changed
* `zk new` now requires the `--interactive`/`-i` flag to read the note body from a pipe or standard input. [See rational](https://github.com/zk-org/zk/pull/242#issuecomment-1182602001).
### Fixed
* [#244](https://github.com/zk-org/zk/issues/244) Fixed `zk new` waiting for `Ctrl-D` to proceed (contributed by [@pkazmier](https://github.com/zk-org/zk/pull/242)).
## 0.11.0
### Added
* Use regular expressions when searching for notes with `--match`.
```sh
# Find notes containing emails.
$ zk list --match-strategy re --match ".+@.+"
$ zk list -Mr -m ".+@.+"
```
### Changed
* The flags `--exact-match`/`-e` are deprecated in favor of `--match-strategy exact`/`-Me`.
### Deprecated
* The LSP server does not support resolving a wiki link to a note title anymore.
* For example, `[[Planet]]` can match a note with filename `i4w0 Planet.md` but not `i4w0.md` with a Markdown title `Planet` anymore.
* This "smart" fallback resolution based on note titles was too fragile and not supported by the `zk` CLI.
### Fixed
* [#233](https://github.com/zk-org/zk/issues/233) Hide index progress in non-interactive shells.
* [#235](https://github.com/zk-org/zk/issues/235) Fix LSP link recognition with unicode (contributed by [@zkbpkp](https://github.com/zk-org/zk/issues/235)).
* [#236](https://github.com/zk-org/zk/issues/236) Fix updating links after creating a new note.
* [#239](https://github.com/zk-org/zk/discussions/239) Support standard input via shell redirection with `zk new`.
## 0.10.1
### Changed
* Removed the dependency on `libicu`.
### Fixed
* Indexed links are now automatically updated when adding a new note, if it is a better match than the previous link target.
<!--## Unreleased-->
## 0.10.0
### Added
* New `--date` flag for `zk new` to set the current date manually.
* New `--id` flag for `zk new` to skip ID generation and use a provided value (contributed by [@skbolton](https://github.com/zk-org/zk/pull/183)).
* [#144](https://github.com/zk-org/zk/issues/144) LSP auto-completion of YAML frontmatter tags.
* [zk-nvim#26](https://github.com/zk-org/zk-nvim/issues/26) The LSP server doesn't use `additionalTextEdits` anymore to remove the trigger characters when completing links.
* New `--id` flag for `zk new` to skip ID generation and use a provided value (contributed by [@skbolton](https://github.com/mickael-menu/zk/pull/183)).
* [#144](https://github.com/mickael-menu/zk/issues/144) LSP auto-completion of YAML frontmatter tags.
* [zk-nvim#26](https://github.com/mickael-menu/zk-nvim/issues/26) The LSP server doesn't use `additionalTextEdits` anymore to remove the trigger characters when completing links.
* You can customize the default behavior with the [`use-additional-text-edits` configuration key](docs/config-lsp.md).
* [#163](https://github.com/zk-org/zk/issues/163) Use the `ZK_SHELL` environment variable to override the shell for `zk` only.
* [#173](https://github.com/zk-org/zk/issues/173) Support for double star globbing in `note.ignore` config option.
* [#137](https://github.com/zk-org/zk/issues/137) Customize the `fzf` options used by `zk`'s interactive modes with the [`fzf-options`](docs/tool-fzf.md) config option (contributed by [@Nelyah](https://github.com/zk-org/zk/pull/154)).
* [#163](https://github.com/mickael-menu/zk/issues/163) Use the `ZK_SHELL` environment variable to override the shell for `zk` only.
* [#173](https://github.com/mickael-menu/zk/issues/173) Support for double star globbing in `note.ignore` config option.
* [#137](https://github.com/mickael-menu/zk/issues/137) Customize the `fzf` options used by `zk`'s interactive modes with the [`fzf-options`](docs/tool-fzf.md) config option (contributed by [@Nelyah](https://github.com/mickael-menu/zk/pull/154)).
* [#168](https://github.com/zk-org/zk/discussions/168) Customize the `fzf` key binding to create new notes with the [`fzf-bind-new`](docs/tool-fzf.md) config option.
* [#168](https://github.com/mickael-menu/zk/discussions/168) Customize the `fzf` key binding to create new notes with the [`fzf-bind-new`](docs/tool-fzf.md) config option.
### Changed
@ -137,10 +25,10 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#126](https://github.com/zk-org/zk/issues/126) Embedded image links shown as not found.
* [#152](https://github.com/zk-org/zk/issues/152) Incorrect timezone for natural dates.
* [#170](https://github.com/zk-org/zk/issues/170) Broken wiki links in subdirectories.
* [#185](https://github.com/zk-org/zk/issues/185) Don't parse a Markdown table header as a colon tag.
* [#126](https://github.com/mickael-menu/zk/issues/126) Embedded image links shown as not found.
* [#152](https://github.com/mickael-menu/zk/issues/152) Incorrect timezone for natural dates.
* [#170](https://github.com/mickael-menu/zk/issues/170) Broken wiki links in subdirectories.
* [#185](https://github.com/mickael-menu/zk/issues/185) Don't parse a Markdown table header as a colon tag.
## 0.9.0
@ -154,10 +42,10 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#111](https://github.com/zk-org/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links.
* [#118](https://github.com/zk-org/zk/issues/118) Fix infinite loop when parsing a single-character hashtag.
* [#121](https://github.com/zk-org/zk/issues/121) Take into account the `--no-input` flag with `zk init`.
* [#120](https://github.com/zk-org/zk/discussions/120) Support RFC 3339 dates with the time flags (e.g. `--created-before`).
* [#111](https://github.com/mickael-menu/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links.
* [#118](https://github.com/mickael-menu/zk/issues/118) Fix infinite loop when parsing a single-character hashtag.
* [#121](https://github.com/mickael-menu/zk/issues/121) Take into account the `--no-input` flag with `zk init`.
* [#120](https://github.com/mickael-menu/zk/discussions/120) Support RFC 3339 dates with the time flags (e.g. `--created-before`).
## 0.8.0
@ -182,9 +70,9 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#89](https://github.com/zk-org/zk/issues/89) Calling `zk index` from outside the notebook (contributed by [@adamreese](https://github.com/zk-org/zk/pull/90)).
* [#98](https://github.com/zk-org/zk/issues/98) Index wiki links using partial paths for `--linked-by` and `--link-to`.
* [#98](https://github.com/zk-org/zk/issues/98) Ignore spaces around the pipe in wiki links for LSP diagnostics.
* [#89](https://github.com/mickael-menu/zk/issues/89) Calling `zk index` from outside the notebook (contributed by [@adamreese](https://github.com/mickael-menu/zk/pull/90)).
* [#98](https://github.com/mickael-menu/zk/issues/98) Index wiki links using partial paths for `--linked-by` and `--link-to`.
* [#98](https://github.com/mickael-menu/zk/issues/98) Ignore spaces around the pipe in wiki links for LSP diagnostics.
## 0.7.0
@ -206,7 +94,7 @@ All notable changes to this project will be documented in this file.
[[book review information]]
[[Information Graphics]]
```
* Use the `{{abs-path}}` template variable when [formatting notes](docs/template-format.md) to print the absolute path to the note (contributed by [@pstuifzand](https://github.com/zk-org/zk/pull/60)).
* Use the `{{abs-path}}` template variable when [formatting notes](docs/template-format.md) to print the absolute path to the note (contributed by [@pstuifzand](https://github.com/mickael-menu/zk/pull/60)).
* A new `{{substring s index length}}` template helper extracts a portion of a given string, e.g.:
* `{{substring 'A full quote' 2 4}}` outputs `full`
* `{{substring 'A full quote' -5 5}` outputs `quote`
@ -214,9 +102,9 @@ All notable changes to this project will be documented in this file.
### Fixed
* UTF-8 handling in the LSP server.
* [#78](https://github.com/zk-org/zk/issues/78) Do not exclude notes containing broken links from the index.
* [#78](https://github.com/mickael-menu/zk/issues/78) Do not exclude notes containing broken links from the index.
* Allow setting the `--working-dir` and `--notebook-dir` flags before the `zk` subcommand when using aliases, e.g. `zk -W ~/notes my-alias`.
* [#86](https://github.com/zk-org/zk/issues/86) Index encoded Markdown links.
* [#86](https://github.com/mickael-menu/zk/issues/86) Index encoded Markdown links.
## 0.6.0
@ -230,7 +118,7 @@ All notable changes to this project will be documented in this file.
* `{{json title}}` prints with quotes `"An interesting note"`
* `{{json .}}` serializes the full template context as a JSON object.
* Use `--header` and `--footer` options with `zk list` to print arbitrary text at the start or end of the list.
* Support for LSP references to browse the backlinks of the link under the caret (contributed by [@pstuifzand](https://github.com/zk-org/zk/pull/58)).
* Support for LSP references to browse the backlinks of the link under the caret (contributed by [@pstuifzand](https://github.com/mickael-menu/zk/pull/58)).
* New [`note.ignore`](docs/config-note.md) configuration option to ignore files matching the given path globs when indexing notes.
```yaml
[note]
@ -242,7 +130,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#16](https://github.com/zk-org/zk/issues/16) Links with section anchors, e.g. `[[filename#section]]`.
* [#16](https://github.com/mickael-menu/zk/issues/16) Links with section anchors, e.g. `[[filename#section]]`.
* Unicode support in wiki links. If you use accents or ideograms, please run `zk index --force` after upgrading to fix your index.
@ -250,7 +138,7 @@ All notable changes to this project will be documented in this file.
### Added
* [Editor integration through LSP](https://github.com/zk-org/zk/issues/22):
* [Editor integration through LSP](https://github.com/mickael-menu/zk/issues/22):
* New code actions to create a note using the current selection as title.
* Custom commands to [run `new` and `index` from your editor](docs/editors-integration.md#custom-commands).
* Diagnostics to [report dead links or wiki link titles](docs/config-lsp.md).
@ -269,7 +157,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* Creating a new note from `fzf` in a directory containing spaces.
* Fix completion with Neovim's built-in LSP client (contributed by [@cormacrelf](https://github.com/zk-org/zk/pull/39)).
* Fix completion with Neovim's built-in LSP client (contributed by [@cormacrelf](https://github.com/mickael-menu/zk/pull/39)).
## 0.4.0
@ -282,7 +170,7 @@ All notable changes to this project will be documented in this file.
* Auto-complete [hashtags and colon-separated tags](docs/tags.md).
* Preview the content of a note when hovering a link.
* Navigate in your notes by following internal links.
* [And more to come...](https://github.com/zk-org/zk/issues/22)
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
* See [the documentation](docs/editors-integration.md) for configuration samples.
* Pair `--match` with `--exact-match` / `-e` to search for (case insensitive) exact occurrences in your notes.
* This can be useful when looking for terms including special characters, such as `[[name]]`.
@ -327,7 +215,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#4](https://github.com/zk-org/zk/issues/4) Terminal borked when piping content with Vim
* [#4](https://github.com/mickael-menu/zk/issues/4) Terminal borked when piping content with Vim
## 0.2.1

@ -1,62 +0,0 @@
# Contributing to `zk`
## Understanding the codebase
### Building the project
It is recommended to use the `Makefile` for compiling the project, as the `go` command requires a few parameters.
```shell
make build
```
This will be expanded to the following command:
```shell
CGO_ENABLED=1 GOARCH=arm64 go build -tags "fts5" -ldflags "-X=main.Version=`git describe --tags --match v[0-9]* 2> /dev/null` -X=main.Build=`git rev-parse --short HEAD`"
```
- `CGO_ENABLED=1` enables CGO, which is required by the `mattn/go-sqlite3` dependency.
- `GOARCH=arm64` is only required for Apple Silicon chips.
- `-tags "fts5"` enables the FTS option with `mattn/go-sqlite3`, which handles much of the magic behind `zk`'s `--match` filtering option.
- ``-ldflags "-X=main.Version=`git describe --tags --match v[0-9]* 2> /dev/null` -X=main.Build=`git rev-parse --short HEAD`"`` will automatically set `zk`'s build and version numbers using the latest Git tag and commit SHA.
### Automated tests
The project is vetted with two different kind of automated tests: unit tests and end-to-end tests.
#### Unit tests
Unit tests are using the standard [Go testing library](https://pkg.go.dev/testing). To execute them, use the command `make test`.
They are ideal for testing parsing output or individual API edge cases and minutiae.
#### End-to-end tests
Most of `zk`'s functionality is tested with functional tests ran with [`tesh`](https://github.com/mickael-menu/tesh), which you can execute with `make tesh` (or `make teshb`, to debug whitespaces changes).
When addressing a GitHub issue, it's a good idea to begin by creating a `tesh` file in `tests/issue-XXX.tesh`. If a starting notebook state is required, it can be added under `tests/fixtures`.
If you modify the output of `zk`, you may disrupt some `tesh` files. You can use `make tesh-update` to automatically update them with the correct output.
### CI workflows
Several GitHub action workflows are executed when pull requests are merged or releases are created.
- `.github/workflows/build.yml` checks that the project can be built and the tests still pass.
- `.github/workflows/codeql.yml` runs static analysis to vet code quality.
- `.github/workflows/gh-pages.yml` deploy the documentation files to GitHub Pages.
- `.github/workflows/release.yml` submits a new version to Homebrew when a Git version tag is created.
- `.github/workflows/triage.yml` automatically tags old issues and PRs as staled.
## Releasing a new version
When `zk` is ready to be released, you can update the `CHANGELOG.md` ([for example](https://github.com/zk-org/zk/commit/ea4457ad671aa85a6b15747460c6f2c9ad61bf73)) and create a new Git version tag (for example `v0.13.0`). Make sure you follow the [Semantic Versioning](https://semver.org) scheme.
Then, create [a new GitHub release](https://github.com/zk-org/zk/releases) with a copy of the latest `CHANGELOG.md` entries and the binaries for all supported platforms.
Binaries can be created automatically using `make dist-linux` and `make dist-macos`.
Unfortunately, `make dist-macos` must be run manually on both an Apple Silicon and Intel chips. The Linux builds are created using Docker and [these custom images](https://github.com/zk-org/zk-xcompile), which are hosted via [ghcr.io within zk-org](https://github.com/orgs/zk-org/packages/container/package/zk-xcompile).
This process is convoluted because `zk` requires CGO with `mattn/go-sqlite3`, which prevents using Go's native cross-compilation. Transitioning to a CGO-free SQLite driver such as [cznic/sqlite](https://gitlab.com/cznic/sqlite) could simplify the distribution process significantly.

@ -22,9 +22,6 @@ teshb: build
tesh-update: build
PATH=".:$(shell pwd):$(PATH)" tesh -u tests tests/fixtures
alpine:
$(call alpine,build)
# Produce a release bundle for all platforms.
dist: dist-macos dist-linux
rm -f zk
@ -34,31 +31,10 @@ dist-macos:
rm -f zk && make && zip -r "zk-${VERSION}-macos-`uname -m`.zip" zk
# Produce a release bundle for Linux.
dist-linux: dist-linux-amd64 dist-linux-arm64 dist-linux-i386 dist-alpine-amd64 dist-alpine-arm64 dist-alpine-i386
dist-linux-amd64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-amd64 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-amd64.tar.gz" zk
dist-linux-arm64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-arm64 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-arm64.tar.gz" zk
dist-linux-i386:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-i386 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-i386.tar.gz" zk
dist-alpine-amd64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-amd64 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-amd64.tar.gz" zk
dist-alpine-arm64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-arm64 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-arm64.tar.gz" zk
dist-alpine-i386:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-i386 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-i386.tar.gz" zk
dist-linux:
rm -f zk && docker run --platform linux/amd64 --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-i386 /bin/bash -c 'make' && tar -zcvf "zk-${VERSION}-linux-i386.tar.gz" zk
rm -f zk && docker run --platform linux/amd64 --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-amd64 /bin/bash -c 'make' && tar -zcvf "zk-${VERSION}-linux-amd64.tar.gz" zk
rm -f zk && docker run --platform linux/amd64 --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-arm64 /bin/bash -c 'make' && tar -zcvf "zk-${VERSION}-linux-arm64.tar.gz" zk
# Clean build products.
clean:
@ -71,16 +47,11 @@ BUILD := `git rev-parse --short HEAD`
ENV_PREFIX := CGO_ENABLED=1
# Add necessary env variables for Apple Silicon.
ifeq ($(shell uname -sm),Darwin arm64)
ENV_PREFIX := $(ENV) GOARCH=arm64
ENV_PREFIX := $(ENV) GOARCH=arm64 CGO_CFLAGS="-I/opt/homebrew/opt/icu4c/include" CGO_LDFLAGS="-L/opt/homebrew/opt/icu4c/lib"
endif
# Wrapper around the go binary, to set all the default parameters.
define go
$(ENV_PREFIX) go $(1) -tags "fts5" -ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
$(ENV_PREFIX) go $(1) -tags "fts5 icu" -ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
endef
# Alpine (musl) requires statically linked libs. This should be compatible for
# Void linux and other musl based distros aswell.
define alpine
$(ENV_PREFIX) go $(1) -tags "fts5" -ldflags "-extldflags=-static -X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
endef

@ -1,12 +1,10 @@
<div align="center">
<img alt="zk logo" width="20%" src="./docs/assets/media/zk-black-modern.png" />
<h1>zk</h1>
<h4>A plain text note-taking assistant</h4>
<img alt="Screencast" width="95%" src="docs/assets/media/screencast.svg"/>
<p>Looking for a quick usage example? <a href="docs/getting-started.md">Let's get started</a>.</p>
</div>
Looking for a quick usage example? [Let's get started](docs/getting-started.md).
## Description
`zk` is a command-line tool helping you to maintain a plain text [Zettelkasten](https://zettelkasten.de/introduction/) or [personal wiki](https://en.wikipedia.org/wiki/Personal_wiki).
@ -17,8 +15,8 @@ Looking for a quick usage example? [Let's get started](docs/getting-started.md).
* [Advanced search and filtering capabilities](docs/note-filtering.md) including [tags](docs/tags.md), links and mentions
* [Integration with your favorite editors](docs/editors-integration.md):
* [Any LSP-compatible editor](docs/editors-integration.md)
* [`zk-nvim`](https://github.com/zk-org/zk-nvim) for Neovim 0.8+
* [`zk-vscode`](https://github.com/zk-org/zk-vscode) for Visual Studio Code
* [`zk-nvim`](https://github.com/mickael-menu/zk-nvim) for Neovim 0.5+
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* (*unmaintained*) [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+ by [Seth Messer](https://github.com/megalithic)
* [Interactive browser](docs/tool-fzf.md), powered by `fzf`
* [Git-style command aliases](docs/config-alias.md) and [named filters](docs/config-filter.md)
@ -39,7 +37,7 @@ Looking for a quick usage example? [Let's get started](docs/getting-started.md).
## Install
[Check out the latest release](https://github.com/zk-org/zk/releases) for pre-built binaries for macOS and Linux (`zk` was not tested on Windows).
[Check out the latest release](https://github.com/mickael-menu/zk/releases) for pre-built binaries for macOS and Linux (`zk` was not tested on Windows).
### Homebrew
@ -53,18 +51,9 @@ Or, if you want to the latest changes:
brew install --HEAD zk
```
### Nix
```sh
# Run zk from Nix store without installing it:
nix run nixpkgs#zk
# Or, to install it permanently:
nix-env -iA zk
```
### Arch Linux
You can install [the zk package](https://archlinux.org/packages/extra/x86_64/zk/) from the official repos.
You can install [the zk package](https://archlinux.org/packages/community/x86_64/zk/) from the official repos.
```sh
sudo pacman -S zk
@ -72,29 +61,34 @@ sudo pacman -S zk
### Build from scratch
Make sure you have a working [Go 1.21+ installation](https://golang.org/), then clone the repository:
Make sure you have a working [Go installation](https://golang.org/), then clone the repository:
```sh
$ git clone https://github.com/zk-org/zk.git
$ git clone https://github.com/mickael-menu/zk.git
$ cd zk
```
#### On macOS / Linux
#### On macOS
`icu4c` is required to build `zk`, which you can install with [Homebrew](https://brew.sh/).
```
$ brew install icu4c
$ make
$ ./zk -h
```
## Contributing
#### On Linux
We warmly welcome issues, PRs and [discussions](https://github.com/zk-org/zk/discussions).
`libicu-dev` is required to build `zk`, use your favorite package manager to install it.
Here you can read [some useful info for contributing to `zk`](./CONTRIBUTING.md).
```
$ apt-install libicu-dev
$ make
$ ./zk -h
```
## Related projects
* [Neuron](https://github.com/srid/neuron) a great tool to publish a Zettelkasten on the web
* [Emanote](https://emanote.srid.ca/) an improved successor to Neuron
* [sirupsen's zk](https://github.com/sirupsen/zk) a collection of scripts with a similar purpose
* [zk-spaced](https://github.com/matze/zk-spaced) spaced repetition plugin for zk

@ -1,24 +0,0 @@
title: "zk"
permalink: /:title
defaults:
- scope:
path: "README.md"
values:
title: "zk"
- scope:
path: "" # all
values:
render_with_liquid: false
exclude:
- ".github/"
- ".gitignore"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "LICENSE"
- "Makefile"
- "go.mod"
- "go.sum"
- "internal/"
- "main.go"
- "tests/"

@ -0,0 +1,2 @@
permalink: /:title
theme: jekyll-theme-modernist

@ -0,0 +1,57 @@
<!doctype html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{% seo %}
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<script src="{{ '/assets/js/scale.fix.js' | relative_url }}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header {% unless site.description or site.github.project_tagline %} class="without-description" {% endunless %}>
<h1><a href="https://mickael-menu.github.io/zk/">{{ site.title | default: site.github.repository_name }}</a></h1>
{% if site.description or site.github.project_tagline %}
<p>{{ site.description | default: site.github.project_tagline }}</p>
{% endif %}
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ github_name }}</small></a></p>
<ul>
{% if site.show_downloads %}
<li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
{% endif %}
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
</ul>
</header>
<section>
{{ content }}
</section>
</div>
<footer>
{% if site.github.is_project_page %}
<p>Project maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
</footer>
<!--[if !IE]><script>fixScale(document);</script><![endif]-->
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>

@ -0,0 +1,9 @@
---
---
@import "{{ site.theme }}";
code, pre {
font-size: inherit;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@ -7,4 +7,4 @@
* [send notes for processing by other programs](external-processing.md)
* [create a note with initial content](note-creation.md) from a standard input pipe
If you find out that `zk` does not behave as expected or could communicate better with other programs, [please post an issue](https://github.com/zk-org/zk/issues).
If you find out that `zk` does not behave as expected or could communicate better with other programs, [please post an issue](https://github.com/mickael-menu/zk/issues).

@ -6,10 +6,9 @@ Declaring your own aliases is a great way to make your experience with `zk` easi
## Configuring aliases
Command aliases are declared in your [configuration file](config.md), under the `[alias]` section. They are executed with [your default shell](tool-shell.md), which allows you to:
Command aliases are declared in your [configuration file](config.md), under the `[alias]` section. They are executed with `$SHELL -c`, which allows you to:
* expand arguments with `$@` or `$*`
* [it is recommended to wrap `$@` in quotes](https://github.com/zk-org/zk/issues/316#issuecomment-1543564168)
* expand environment variables
* run several commands with `&&`
* pipe several commands with `|`
@ -18,7 +17,7 @@ An alias can call other aliases but cannot call itself. This enables you to over
```toml
[alias]
edit = 'zk edit --interactive "$@"'
edit = "zk edit --interactive $@"
```
When running an alias, the `ZK_NOTEBOOK_DIR` environment variable is set to the absolute path of the current notebook. You can use it to run commands working no matter the location of the working directory.

@ -34,7 +34,7 @@ You can override the global [note configuration](config-note.md) and [extra user
```toml
[group.journal.note]
filename = "{{format-date now}}"
filename = "{{date now}}"
template = "journal.md"
[group.journal.extra]

@ -14,8 +14,8 @@ The `[note]` section from the [configuration file](config.md) is used to set the
* `template` (string)
* Path to the [template](template.md) used to generate the note content.
* Either an absolute path, or relative to `.zk/templates/`.
* `exclude` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) excluded during note indexing.
* `ignore` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) ignored during note indexing.
* `id-charset` (string)
* Characters set used to [generate random IDs](note-id.md).
* You can use:
@ -48,10 +48,10 @@ Here are some common filename patterns you may want to use:
* Readable and practical for web servers, but fragile in case of renaming.
* `{{id}}-{{slug title}}` e.g. `i2hn8-an-interesting-concept.md`
* The best of both worlds? Readable but if you link only with the prefix ID, you can rename without breaking links.
* `{{format-date now 'timestamp'}}` e.g. `200911172034.md`
* `{{date now 'timestamp'}}` e.g. `200911172034.md`
* Verbose, but sortable by creation date and stable.
* `{{format-date now 'timestamp'}} {{title}}` e.g. `200911172034 An interesting concept.md`
* `{{date now 'timestamp'}} {{title}}` e.g. `200911172034 An interesting concept.md`
* The format of [The Archive](https://zettelkasten.de/the-archive/) and [sirupsen's zk](https://github.com/sirupsen/zk).
* `{{format-date now '%Y-%m-%d'}}` e.g. `2009-11-17.md`
* `{{date now '%Y-%m-%d'}}` e.g. `2009-11-17.md`
* Sortable, human-friendly format for a daily journal.
* i.e. [Maintaining a daily journal](daily-journal.md).

@ -1,15 +0,0 @@
# Notebook configuration
The `[notebook]` section from the [configuration file](config.md) is used to set the default notebook directory.
If the path starts with `~` it will be replaced with the user home directory (`$HOME`). This property also supports environment variables.
```toml
[notebook]
dir = "~/notebook" # same as "$HOME/notebook"
```
The following properties are customizable:
* `dir` (string)
* Path of the default notebook.
* Only available in the global config file (`~/.config/zk/config.toml`).

@ -2,14 +2,12 @@
Each [notebook](notebook.md) contains a configuration file used to customize your experience with `zk`. This file is located at `.zk/config.toml` and uses the [TOML format](https://github.com/toml-lang/toml). It is composed of several optional sections:
* `[notebook]` configures the [default notebook](config-notebook.md)
* `[note]` sets the [note creation rules](config-note.md)
* `[extra]` contains free [user variables](config-extra.md) which can be expanded in templates
* `[group]` defines [note groups](config-group.md) with custom rules
* `[format]` configures the [note format settings](note-format.md), such as Markdown options
* `[tool]` customizes interaction with external programs such as:
* [your default editor](tool-editor.md)
* [your default shell](tool-shell.md)
* [your default pager](tool-pager.md)
* [`fzf`](tool-fzf.md)
* `[lsp]` setups the [Language Server Protocol settings](config-lsp.md) for [editors integration](editors-integration.md)
@ -27,10 +25,6 @@ Notebook configuration files will inherit the settings defined in the global con
Here's an example of a complete configuration file:
```toml
# NOTEBOOK SETTINGS
[notebook]
dir = "~/notebook"
# NOTE SETTINGS
[note]
@ -69,11 +63,11 @@ author = "Mickaël"
# GROUP OVERRIDES
[group.journal]
[dir.journal]
paths = ["journal/weekly", "journal/daily"]
[group.journal.note]
filename = "{{format-date now}}"
[dir.journal.note]
filename = "{{date now}}"
# MARKDOWN SETTINGS
@ -90,9 +84,6 @@ colon-tags = true
# Default editor used to open notes.
editor = "nvim"
# Default shell used by aliases and commands.
shell = "/bin/bash"
# Pager used to scroll through long output.
pager = "less -FIRX"
@ -123,4 +114,4 @@ lucky = "zk list --quiet --format full --sort random --limit 1"
wiki-title = "hint"
# Warn for dead links between notes.
dead-link = "error"
```
```

@ -2,7 +2,7 @@
Let's assume you want to write daily notes named like `2021-02-16.md` in a `journal/daily` sub-directory. This common use case is a good fit for creating a [note group](config-group.md) overriding the default [note creation](note-creation.md) settings.
First, create a `group` entry in the [configuration file](config.md) to set the note settings for this directory. Refer to the [template syntax reference](template.md) to understand how to use the `{{format-date}}` helper.
First, create a `group` entry in the [configuration file](config.md) to set the note settings for this directory. Refer to the [template syntax reference](template.md) to understand how to use the `{{date}}` helper.
```toml
[group.daily]
@ -10,8 +10,8 @@ First, create a `group` entry in the [configuration file](config.md) to set the
paths = ["journal/daily"]
[group.daily.note]
# %Y-%m-%d is actually the default format, so you could use {{format-date now}} instead.
filename = "{{format-date now '%Y-%m-%d'}}"
# %Y-%m-%d is actually the default format, so you could use {{date now}} instead.
filename = "{{date now '%Y-%m-%d'}}"
extension = "md"
template = "daily.md"
```
@ -19,7 +19,7 @@ template = "daily.md"
Next, create a template file under `.zk/templates/daily.md` to render the note content. Here we used the date again to generate a title like "February 16, 2021".
```markdown
# {{format-date now "long"}}
# {{date now "long"}}
What did I do today?
```
@ -43,7 +43,3 @@ Let's unpack this alias:
* `$ZK_NOTEBOOK_DIR` is set to the absolute path of the current [notebook](notebook.md) when running an alias. Using it allows you to run `zk daily` no matter where you are in the notebook folder hierarchy.
* We need to use double quotes around `$ZK_NOTEBOOK_DIR`, otherwise it will not be expanded.
If you want to edit today's note, simply use this alias:
```sh
$ zk daily
```

@ -2,8 +2,8 @@
There are several extensions available to integrate `zk` in your favorite editor:
* [`zk-nvim`](https://github.com/zk-org/zk-nvim) for Neovim 0.5+
* [`zk-vscode`](https://github.com/zk-org/zk-vscode) for Visual Studio Code
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
## Language Server Protocol
@ -15,13 +15,13 @@ There are several extensions available to integrate `zk` in your favorite editor
* Navigate in your notes by following internal links.
* Create a new note using the current selection as title.
* Diagnostics for dead links and wiki-links titles.
* [And more to come...](https://github.com/zk-org/zk/issues/22)
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
You can configure some of these features in your notebook's [configuration file](config-lsp.md).
### Editor LSP configurations
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/zk-org/zk/issues/22).
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/mickael-menu/zk/issues/22).
#### Vim and Neovim
@ -150,21 +150,19 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Description |
|---------------------------|----------------------|----------------------------------------------------------------------------------------------------------------------|
| `title` | string | Title of the new note |
| `content` | string | Initial content of the note |
| `dir` | string | Parent directory, relative to the root of the notebook |
| `group` | string | [Note configuration group](config-group.md) |
| `template` | string | [Custom template used to render the note](template-creation.md) |
| `extra` | dictionary | A dictionary of extra variables to expand in the template |
| `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" |
| `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) |
| `dryRun` | boolean | When true, `zk` will not actually create the note on the file system, but will return its generated content and path |
| `insertLinkAtLocation` | location<sup>1</sup> | A location in another note where a link to the new note will be inserted |
| `insertContentAtLocation` | location<sup>1</sup> | A location in another note where the content of the new note will be inserted |
1. The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
| Key | Type | Description |
|------------------------|----------------------|-------------------------------------------------------------------------------------------|
| `title` | string | Title of the new note |
| `content` | string | Initial content of the note |
| `dir` | string | Parent directory, relative to the root of the notebook |
| `group` | string | [Note configuration group](config-group.md) |
| `template` | string | [Custom template used to render the note](template-creation.md) |
| `extra` | dictionary | A dictionary of extra variables to expand in the template |
| `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" |
| `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) |
| `insertLinkAtLocation` | location<sup>1</sup> | A location in another note where a link to the new note will be inserted |
The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
```json
{
@ -177,22 +175,7 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
```
</details>
`zk.new` returns a dictionary with two properties:
* `path` containing the absolute path to the created note.
* `content` containing the raw content of the created note.
#### `zk.link`
This LSP command allows editors to tap into the note linking mechanism. It takes three arguments:
1. A `path` to any file in the notebook that will be linked to
2. An LSP `location` object that points to where the link will be inserted
3. An optional title of the link. If `title` is not provided, the title of the note will be inserted instead
`zk.link` returns a JSON object with the path to the linked note, if the linking was successful.
**Note**: This command is _not_ exposed in the command line. This command is targeted at editor / plugin authors to extend zk functionality.
`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file.
#### `zk.list`
@ -201,31 +184,30 @@ This LSP command calls `zk list` to search a notebook. It takes two arguments:
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>A dictionary of additional options (click to expand)</summary>
| Key | Type | Required? | Description |
| ------------------ | -------------- | ----------- | ------------------------------------------------------------------------- |
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `hrefs` | string array | No | Find notes matching the given path, including its descendants |
| `limit` | integer | No | Limit the number of notes found |
| `match` | string array | No | Terms to search for in the notes |
| `exactMatch` | boolean | No | (deprecated: use `matchStrategy`) Search for exact occurrences of the `match` argument (case insensitive) |
| `matchStrategy` | string | No | Specify match strategy, which may be "fts" (default), "exact" or "re" |
| `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants |
| `tags` | string array | No | Find notes tagged with the given tags |
| `mention` | string array | No | Find notes mentioning the title of the given ones |
| `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones |
| `linkTo` | string array | No | Find notes which are linking to the given ones |
| `linkedBy` | string array | No | Find notes which are linked by the given ones |
| `orphan` | boolean | No | Find notes which are not linked by any other note |
| `related` | string array | No | Find notes which might be related to the given ones |
| `maxDistance` | integer | No | Maximum distance between two linked notes |
| `recursive` | boolean | No | Follow links recursively |
| `created` | string | No | Find notes created on the given date |
| `createdBefore` | string | No | Find notes created before the given date |
| `createdAfter` | string | No | Find notes created after the given date |
| `modified` | string | No | Find notes modified on the given date |
| `modifiedBefore` | string | No | Find notes modified before the given date |
| `modifiedAfter` | string | No | Find notes modified after the given date |
| `sort` | string array | No | Order the notes by the given criterion |
| Key | Type | Required? | Description |
|------------------|--------------|-----------|-------------------------------------------------------------------------|
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `hrefs` | string array | No | Find notes matching the given path, including its descendants |
| `limit` | integer | No | Limit the number of notes found |
| `match` | string | No | Terms to search for in the notes |
| `exactMatch` | boolean | No | Search for exact occurrences of the `match` argument (case insensitive) |
| `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants |
| `tags` | string array | No | Find notes tagged with the given tags |
| `mention` | string array | No | Find notes mentioning the title of the given ones |
| `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones |
| `linkTo` | string array | No | Find notes which are linking to the given ones |
| `linkedBy` | string array | No | Find notes which are linked by the given ones |
| `orphan` | boolean | No | Find notes which are not linked by any other note |
| `related` | string array | No | Find notes which might be related to the given ones |
| `maxDistance` | integer | No | Maximum distance between two linked notes |
| `recursive` | boolean | No | Follow links recursively |
| `created` | string | No | Find notes created on the given date |
| `createdBefore` | string | No | Find notes created before the given date |
| `createdAfter` | string | No | Find notes created after the given date |
| `modified` | string | No | Find notes modified on the given date |
| `modifiedBefore` | string | No | Find notes modified before the given date |
| `modifiedAfter` | string | No | Find notes modified after the given date |
| `sort` | string array | No | Order the notes by the given criterion |
1. As the output of this command might be very verbose and put a heavy load on the LSP client, you need to explicitly set which note fields you want to receive with the `select` option. The following fields are available: `filename`, `filenameStem`, `path`, `absPath`, `title`, `lead`, `body`, `snippets`, `rawContent`, `wordCount`, `tags`, `metadata`, `created`, `modified` and `checksum`.

@ -27,7 +27,3 @@ But you can make your [notebook](notebook.md) even more tightly integrated with
serve = "neuron gen -wS"
gen = "neuron gen -o public"
```
## Emanote
Emanote is neuron's successor. For Emanote-specific configuration, see https://emanote.srid.ca/start/resources/zk.

@ -14,17 +14,11 @@ This option is available when running `zk edit --interactive`, which spawns [`fz
## Create a note with initial content
Initial content can be fed to the template through standard input using `zk new --interactive`, which will be expandable with the `{{content}}` [template variable](template-creation.md).
Initial content can be fed to the template through a standard input pipe, which will be expandable with the `{{content}}` [template variable](template-creation.md).
For example, to use the content of the macOS clipboard as the initial content you can run:
```sh
$ pbpaste | zk new --interactive
```
Alternatively, you can use the content of a file:
```sh
$ zk new --interactive < file.txt
$ pbpaste | zk new
```

@ -45,50 +45,11 @@ $ zk list --linked-by "`zk inline journal`"
Use `--match <query>` (or `-m`) to search through the title and body of notes.
The search is powered by different strategies to answer various use cases:
* `fts` (default) uses a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database to offer near-instant results and advanced search operators.
* `exact` is useful if you need to find patterns containing special characters.
* `re` enables regular expression for advanced use cases.
Change the currently used strategy with `--match-strategy <strategy>` (or `-M`). To set the default strategy, you can declare a [custom alias](config-alias.md):
```toml
[alias]
list = "zk list --match-strategy re $@"
```
The `--match` option may be given multiple times, where each argument will be combined with a boolean AND.
For example,
```sh
$ zk list --tag "recipe" --match "pizza -pineapple" --match "mushrooms"
```
Is equivalent to,
```sh
$ zk list --tag "recipe" --match "(pizza -pineapple) AND (mushrooms)"
```
### Full-text search (`fts`)
The default match strategy is powered by a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database enabling near-instant results. Queries are not case-sensitive and terms are tokenized, which means that searching for `create` will also match `created` and `creating`.
The search is powered by a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database enabling near-instant results. Queries are not case-sensitive and terms are tokenized, which means that searching for `create` will also match `created` and `creating`.
A syntax similar to Google Search is available for advanced search queries.
```sh
# FTS is the default match strategy
$ zk list --match "tesla OR edison"
# ...but you can enable it explicitly.
$ zk list --match-strategy fts --match "tesla OR edison"
$ zk list -Mf -m "tesla OR edison"
```
#### Combining terms
### Combining terms
By default, the search engine will find the notes containing all the terms in the query, in any order.
@ -122,7 +83,7 @@ Finally, you can filter out results by excluding a term with `NOT` (all caps) or
"tesla -car"
```
#### Search in specific fields
### Search in specific fields
If you want to search only in the title or body of notes, prefix a query with `title:` or `body:`.
@ -131,7 +92,7 @@ If you want to search only in the title or body of notes, prefix a query with `t
"body: (tesla OR edison)"
```
#### Prefix terms
### Prefix terms
Match any term beginning with the given prefix with a wildcard `*`.
@ -145,25 +106,13 @@ Prefixing a query with `^` will match notes whose title or body start with the f
"title: ^journal"
```
### Exact matches (`exact`)
### Search for special characters
If you need to find patterns containing special characters, such as an `email@addre.ss` or a `[[wiki-link]]`, use the `exact` match strategy. The search will be case-insensitive.
If you need to find patterns containing special characters, such as an `email@addre.ss` or a `[[wiki-link]]`, use the `--exact-match` / `-e` option. The search will be case-insensitive.
```sh
$ zk list --match-strategy exact --match "[[link]]"
$ zk list -Me -m "[[link]]"
```
### Regular expressions (`re`)
For advanced use cases, you can use the `re` match strategy to search the notebook using regular expressions. The supported syntax is similar to the one used by Python or Perl. [See the full reference](https://golang.org/s/re2syntax).
:warning: Make sure to use quotes to prevent your shell from expanding wildcards.
```sh
# Find notes containing emails.
$ zk list --match-strategy re --match ".+@.+"
$ zk list -Mr -m ".+@.+"
$ zk list --exact-match --match "[[link]]"
$ zk list -em "[[link]]"
```
## Filter by tags

@ -6,8 +6,6 @@ To create a new notebook, simply run `zk init [<directory>]`.
Most `zk` commands are operating "Git-style" on the notebook containing the current working directory (or one of its parents). However, you can explicitly set which notebook to use with `--notebook-dir` or the `ZK_NOTEBOOK_DIR` environment variable. Setting `ZK_NOTEBOOK_DIR` in your shell configuration (e.g. `~/.profile`) can be used to define a default notebook which `zk` commands will use when the working directory is not in another notebook.
If the [default notebook](config-notebook.md) is set it will be used as `ZK_NOTEBOOK_DIR`, unless this environment variable is not already set.
## Anatomy of a notebook
Similarly to Git, a notebook is identified by the presence of a `.zk` directory at its root. This directory contains the only `zk`-specific files in your notebook:

@ -9,7 +9,7 @@ The following variables are available in the templates used when [creating new n
| `content` | string | Any text piped through the standard input |
| `dir` | string | Parent directory in the notebook |
| `extra.<key>` | string | [Additional variables](config-extra.md) provided through the config file or `--extra` |
| `now` | date | Current date and time, useful when paired with [`{{format-date now}}`](template.md) |
| `now` | date | Current date and time, useful when paired with [`{{date now}}`](template.md) |
| `env` | map | Dictionary of case-sensitive environment variables, e.g. `{{env.PATH}}`. |
These additional variables are available only to the note content template, once the filename is generated.

@ -36,27 +36,15 @@ The `{{concat s1 s2}}` helper concatenates two strings together. For example `{{
* The `{{substring s index length}}` helper extracts a portion of the given string. For example:
* `{{substring 'A full quote' 2 4}}` outputs `full`
* `{{substring 'A full quote' -5 5}}` outputs `quote`
* `{{substring 'A full quote' -5 5}` outputs `quote`
### Date helpers
### Date helper
#### Date from natural string helper
You can get a date object from a natural human date (e.g. `tomorrow`, `2 weeks ago`, `2022-03-24`) using the `{{date}}` helper. It is most useful when paired with the `{{format-date}}` helper.
```
{{date "tomorrow"}}
{{format-date (date "last week") "timestamp"}}
```
#### Date formatting helper
The `{{format-date}}` helper formats the given date for display.
The `{{date}}` helper formats the given date for display.
Template contexts usually provide a `now` variable which can be used to print the current date.
The default format output by `{{format-date <variable>}}` looks like `2009-11-17`, but you can choose a different format by providing a second argument, e.g. `{{format-date now "medium"}}`.
The default format output by `{{date <variable>}}` looks like `2009-11-17`, but you can choose a different format by providing a second argument, e.g. `{{date now "medium"}}`.
| Format | Output | Notes |
|------------------|----------------------------|--------------------------------------------------|
@ -70,7 +58,7 @@ The default format output by `{{format-date <variable>}}` looks like `2009-11-17
| `timestamp-unix` | 1258490098 | Number of seconds since January 1, 1970 |
| `elapsed` | 12 years ago | Time elapsed since then in human-friendly format |
If none of the provided formats suit you, you can use a custom format using `strftime`-style placeholders, e.g. `{{format-date now "%m-%d-%Y"}}`. See `man strftime` for a list of placeholders.
If none of the provided formats suit you, you can use a custom format using `strftime`-style placeholders, e.g. `{{date now "%m-%d-%Y"}}`. See `man strftime` for a list of placeholders.
### Slug helper

@ -4,7 +4,7 @@
Besides the standard [`fzf` configuration options](https://github.com/junegunn/fzf) documented on its website, `zk` offers additional options you can set in the `[tool]` [configuration section](config.md).
If you wish to customize more of `fzf` behavior, [please post a feature request](https://github.com/zk-org/zk/issues).
If you wish to customize more of `fzf` behavior, [please post a feature request](https://github.com/mickael-menu/zk/issues).
## Preview command

@ -1,14 +0,0 @@
# Setting your default shell
This is *currently* not supported on Windows (that defaults always to `cmd`).
You can customize which shell to use to run aliases and commands either from the [configuration file](config.md) or environment variables. In order of precedence, `zk` will use:
1. `ZK_SHELL` environment variable
2. `shell` configuration property
```toml
[tool]
shell = "/bin/bash"
```
3. `SHELL` environment variable
4. `sh` as fallback

@ -1,54 +1,39 @@
module github.com/zk-org/zk
module github.com/mickael-menu/zk
go 1.21
go 1.15
replace github.com/tliron/glsp => github.com/mickael-menu/glsp v0.1.1
require (
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/alecthomas/kong v0.5.0
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/alecthomas/kong v0.2.18-0.20210927063154-5c7b038540ab
github.com/aymerick/raymond v2.0.2+incompatible
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect
github.com/fatih/color v1.13.0
github.com/go-testfixtures/testfixtures/v3 v3.6.1
github.com/google/go-cmp v0.5.8
github.com/gosimple/slug v1.12.0
github.com/go-testfixtures/testfixtures/v3 v3.4.1
github.com/google/go-cmp v0.5.6
github.com/gosimple/slug v1.10.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lestrrat-go/strftime v1.0.6
github.com/lestrrat-go/strftime v1.0.5
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-sqlite3 v1.14.22
github.com/mattn/go-sqlite3 v1.14.8
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mickael-menu/pretty v0.2.3
github.com/mvdan/xurls v1.1.0
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
github.com/pelletier/go-toml v1.9.4
github.com/relvacode/iso8601 v1.1.0
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/rvflash/elapsed v0.2.0
github.com/schollz/progressbar/v3 v3.8.6
github.com/schollz/progressbar/v3 v3.8.3
github.com/tj/go-naturaldate v1.3.0
github.com/tliron/glsp v0.1.1
github.com/tliron/kutil v0.1.59
github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark-meta v1.1.0
github.com/zk-org/pretty v0.2.4
github.com/tliron/glsp v0.0.0-20210824162824-d103e5701036
github.com/tliron/kutil v0.1.49
github.com/yuin/goldmark v1.4.1
github.com/yuin/goldmark-meta v1.0.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/sys v0.0.0-20211002104244-808efd93c36d // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/djherbis/times.v1 v1.3.0
)
require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/petermattis/goid v0.0.0-20220526132513-07eaf5d0b9f4 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/sourcegraph/jsonrpc2 v0.1.0 // indirect
github.com/zchee/color/v2 v2.0.6 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

1380
go.sum

File diff suppressed because it is too large Load Diff

@ -6,10 +6,10 @@ import (
"strings"
"github.com/kballard/go-shellquote"
"github.com/zk-org/zk/internal/util/errors"
executil "github.com/zk-org/zk/internal/util/exec"
"github.com/zk-org/zk/internal/util/opt"
osutil "github.com/zk-org/zk/internal/util/os"
"github.com/mickael-menu/zk/internal/util/errors"
executil "github.com/mickael-menu/zk/internal/util/exec"
"github.com/mickael-menu/zk/internal/util/opt"
osutil "github.com/mickael-menu/zk/internal/util/os"
)
// Editor represents an external editor able to edit the notes.
@ -37,7 +37,7 @@ func (e *Editor) Open(paths ...string) error {
// /dev/tty is restored as stdin, in case the user used a pipe to feed
// initial note content to `zk new`. Without this, Vim doesn't work
// properly in this case.
// See https://github.com/zk-org/zk/issues/4
// See https://github.com/mickael-menu/zk/issues/4
cmd := executil.CommandFromString(e.editor + " " + shellquote.Join(paths...) + " </dev/tty")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

@ -4,8 +4,8 @@ import (
"os"
"testing"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestEditorUsesZkEditorFirst(t *testing.T) {

@ -1,11 +1,12 @@
package fs
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/zk-org/zk/internal/util"
"github.com/mickael-menu/zk/internal/util"
)
// FileStorage implements the port core.FileStorage.
@ -113,7 +114,7 @@ func (fs *FileStorage) IsDescendantOf(dir string, path string) (bool, error) {
}
func (fs *FileStorage) Read(path string) ([]byte, error) {
return os.ReadFile(path)
return ioutil.ReadFile(path)
}
func (fs *FileStorage) Write(path string, content []byte) error {

@ -9,9 +9,9 @@ import (
"sync"
"github.com/kballard/go-shellquote"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
stringsutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
stringsutil "github.com/mickael-menu/zk/internal/util/strings"
)
// ErrCancelled is returned when the user cancelled fzf.

@ -7,10 +7,10 @@ import (
"strings"
"time"
"github.com/zk-org/zk/internal/adapter/term"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/opt"
stringsutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/adapter/term"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/opt"
stringsutil "github.com/mickael-menu/zk/internal/util/strings"
)
// NoteFilter uses fzf to filter interactively a set of notes.

@ -6,23 +6,22 @@ import (
"path/filepath"
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/adapter/handlebars/helpers"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/adapter/handlebars/helpers"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/paths"
)
func Init(supportsUTF8 bool, logger util.Logger) {
helpers.RegisterConcat()
helpers.RegisterSubstring()
helpers.RegisterDate(logger)
helpers.RegisterFormatDate(logger)
helpers.RegisterJoin()
helpers.RegisterJSON(logger)
helpers.RegisterList(supportsUTF8)
helpers.RegisterPrepend(logger)
helpers.RegisterShell(logger)
helpers.RegisterSubstring()
}
// Template renders a parsed handlebars template.

@ -7,12 +7,12 @@ import (
"testing"
"time"
"github.com/zk-org/zk/internal/adapter/handlebars/helpers"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/fixtures"
"github.com/zk-org/zk/internal/util/paths"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/adapter/handlebars/helpers"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/fixtures"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func init() {
@ -226,131 +226,19 @@ func TestSlugHelper(t *testing.T) {
)
}
func TestFormatDateHelper(t *testing.T) {
context := map[string]interface{}{"now": time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{format-date now}}", context, "2009-11-17")
testString(t, "{{format-date now 'short'}}", context, "11/17/2009")
testString(t, "{{format-date now 'medium'}}", context, "Nov 17, 2009")
testString(t, "{{format-date now 'long'}}", context, "November 17, 2009")
testString(t, "{{format-date now 'full'}}", context, "Tuesday, November 17, 2009")
testString(t, "{{format-date now 'year'}}", context, "2009")
testString(t, "{{format-date now 'time'}}", context, "20:34")
testString(t, "{{format-date now 'timestamp'}}", context, "200911172034")
testString(t, "{{format-date now 'timestamp-unix'}}", context, "1258490098")
testString(t, "{{format-date now 'cust: %Y-%m'}}", context, "cust: 2009-11")
}
func TestFormatDateHelperElapsedYear(t *testing.T) {
year := time.Now().UTC().Year() - 14
context := map[string]interface{}{"now": time.Date(year, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{format-date now 'elapsed'}}", context, "14 years ago")
}
func TestFormatDateHelperElapsedViaTimeMultiplication(t *testing.T) {
// test for time being provided in via multiplications on seconds, minutes
// and hours, as expected by github.com/rvflash/elapsed
cases := []struct {
elapsed time.Duration
want string
}{
{
elapsed: -12 * time.Second,
want: "not yet",
},
{
elapsed: time.Second,
want: "just now",
},
{
elapsed: 59 * time.Second,
want: "just now",
},
{
elapsed: 60 * time.Second,
want: "1 minute ago",
},
{
elapsed: 1 * time.Minute,
want: "1 minute ago",
},
{
elapsed: 2 * time.Minute,
want: "2 minutes ago",
},
{
elapsed: 62 * time.Minute,
want: "1 hour ago",
},
{
elapsed: time.Hour,
want: "1 hour ago",
},
{
elapsed: 2 * time.Hour,
want: "2 hours ago",
},
{
elapsed: 24 * time.Hour,
want: "yesterday",
},
{
elapsed: 4 * 24 * time.Hour,
want: "4 days ago",
},
{
elapsed: 7 * 24 * time.Hour,
want: "1 week ago",
},
{
elapsed: 8 * 24 * time.Hour,
want: "2 weeks ago",
},
{
elapsed: 18 * 24 * time.Hour,
want: "3 weeks ago",
},
{
elapsed: 30 * 24 * time.Hour,
want: "1 month ago",
},
{
elapsed: 31 * 24 * time.Hour,
want: "2 months ago",
},
{
elapsed: 60 * 24 * time.Hour,
want: "2 months ago",
},
{
elapsed: 61 * 24 * time.Hour,
want: "3 months ago",
},
{
elapsed: 330 * 24 * time.Hour,
want: "11 months ago",
},
{
elapsed: 331 * 24 * time.Hour,
want: "1 year ago",
},
{
elapsed: 366 * 24 * time.Hour,
want: "2 years ago",
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.want), func(t *testing.T) {
templateContext := map[string]interface{}{"now": time.Now().Add(-tc.elapsed)}
testString(t, "{{format-date now 'elapsed'}}", templateContext, tc.want)
})
}
}
func TestDateHelper(t *testing.T) {
context := map[string]interface{}{"now": time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{format-date (date \"2009-11-17T20:34:58\") 'timestamp'}}", context, "200911172034")
testString(t, "{{date now}}", context, "2009-11-17")
testString(t, "{{date now 'short'}}", context, "11/17/2009")
testString(t, "{{date now 'medium'}}", context, "Nov 17, 2009")
testString(t, "{{date now 'long'}}", context, "November 17, 2009")
testString(t, "{{date now 'full'}}", context, "Tuesday, November 17, 2009")
testString(t, "{{date now 'year'}}", context, "2009")
testString(t, "{{date now 'time'}}", context, "20:34")
testString(t, "{{date now 'timestamp'}}", context, "200911172034")
testString(t, "{{date now 'timestamp-unix'}}", context, "1258490098")
testString(t, "{{date now 'cust: %Y-%m'}}", context, "cust: 2009-11")
testString(t, "{{date now 'elapsed'}}", context, "13 years ago")
}
func TestShellHelper(t *testing.T) {

@ -1,51 +1,24 @@
package helpers
import (
"os"
"time"
"github.com/aymerick/raymond"
"github.com/lestrrat-go/strftime"
"github.com/zk-org/zk/internal/util"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/pkg/errors"
"github.com/mickael-menu/zk/internal/util"
"github.com/rvflash/elapsed"
)
// RegisterDate registers the {{date}} template helper to use the `naturaldate` package to generate time.Time based on language strings.
// This can be used in combination with the `format-date` helper to generate dates in the user's language.
// {{format-date (date "last week") "timestamp"}}
func RegisterDate(logger util.Logger) {
raymond.RegisterHelper("date", func(arg1 interface{}, arg2 interface{}) time.Time {
var t time.Time
switch date := arg1.(type) {
case string:
t, err := dateutil.TimeFromNatural(date)
if err != nil {
logger.Err(errors.Wrap(err, "the {{date}} template helper failed to parse the date"))
}
return t
case time.Time:
logger.Println("the {{date}} template helper was renamed to {{format-date}}, please update your configuration")
os.Exit(1)
return t
default:
logger.Println("the {{date}} template helper expects a natural human date as a string for its only argument")
return t
}
})
}
// RegisterFormatDate registers the {{format-date}} template helpers which format a given date.
// RegisterDate registers the {{date}} template helpers which format a given date.
//
// It supports various styles: short, medium, long, full, year, time,
// timestamp, timestamp-unix or a custom strftime format.
//
// {{format-date now}} -> 2009-11-17
// {{format-date now "medium"}} -> Nov 17, 2009
// {{format-date now "%Y-%m"}} -> 2009-11
func RegisterFormatDate(logger util.Logger) {
raymond.RegisterHelper("format-date", func(date time.Time, arg interface{}) string {
// {{date now}} -> 2009-11-17
// {{date now "medium"}} -> Nov 17, 2009
// {{date now "%Y-%m"}} -> 2009-11
func RegisterDate(logger util.Logger) {
raymond.RegisterHelper("date", func(date time.Time, arg interface{}) string {
format := "%Y-%m-%d"
if arg, ok := arg.(string); ok {
@ -58,7 +31,7 @@ func RegisterFormatDate(logger util.Logger) {
} else {
res, err := strftime.Format(format, date, strftime.WithUnixSeconds('s'))
if err != nil {
logger.Printf("the {{format-date}} template helper failed to format the date: %v", err)
logger.Printf("the {{date}} template helper failed to format the date: %v", err)
return ""
}
return res

@ -4,8 +4,8 @@ import (
"encoding/json"
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
)
// RegisterJSON registers a {{json}} template helper which serializes its

@ -1,8 +1,8 @@
package helpers
import (
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
)
// NewLinkHelper creates a new template helper to generate an internal link

@ -2,8 +2,8 @@ package helpers
import (
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/strings"
)
// RegisterPrepend registers a {{prepend}} template helper which prepend a

@ -4,8 +4,8 @@ import (
"strings"
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/exec"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/exec"
)
// RegisterShell registers the {{sh}} template helper, which runs shell commands.

@ -3,7 +3,7 @@ package helpers
import (
"github.com/aymerick/raymond"
"github.com/gosimple/slug"
"github.com/zk-org/zk/internal/util"
"github.com/mickael-menu/zk/internal/util"
)
// NewSlugHelper creates a new template helper to slugify text.

@ -4,8 +4,8 @@ import (
"strings"
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
)
// NewStyleHelper creates a new template helper which stylizes the text input

@ -3,7 +3,7 @@ package lsp
import (
"fmt"
"github.com/zk-org/zk/internal/core"
"github.com/mickael-menu/zk/internal/core"
)
const cmdIndex = "zk.index"

@ -1,64 +0,0 @@
package lsp
import (
"fmt"
"path/filepath"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
const cmdLink = "zk.link"
type cmdLinkOpts struct {
Path *string `json:"path"`
Location *protocol.Location `json:"location"`
Title *string `json:"title"`
}
func executeCommandLink(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) {
var opts cmdLinkOpts
if len(args) > 1 {
arg, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdLink, args[1])
}
err := unmarshalJSON(arg, &opts)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdLink, arg)
}
}
if opts.Path == nil {
return nil, errors.New("'path' not provided")
}
note, err := notebook.FindByHref(*opts.Path, false)
if err != nil {
return nil, err
}
if note == nil {
return nil, errors.New("Requested note to link to not found!")
}
info := &linkInfo{
note: note,
location: opts.Location,
title: opts.Title,
}
err = linkNote(notebook, documents, context, info)
if err != nil {
return nil, err
}
return map[string]interface{}{
"path": filepath.Join(notebook.Path, note.Path),
}, nil
}

@ -5,11 +5,11 @@ import (
"path/filepath"
"time"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
strutil "github.com/mickael-menu/zk/internal/util/strings"
)
const cmdList = "zk.list"

@ -4,10 +4,10 @@ import (
"fmt"
"path/filepath"
"github.com/zk-org/zk/internal/core"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/core"
dateutil "github.com/mickael-menu/zk/internal/util/date"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
@ -15,17 +15,15 @@ import (
const cmdNew = "zk.new"
type cmdNewOpts struct {
Title string `json:"title"`
Content string `json:"content"`
Dir string `json:"dir"`
Group string `json:"group"`
Template string `json:"template"`
Extra map[string]string `json:"extra"`
Date string `json:"date"`
Edit jsonBoolean `json:"edit"`
DryRun jsonBoolean `json:"dryRun"`
InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation"`
InsertContentAtLocation *protocol.Location `json:"insertContentAtLocation"`
Title string `json:"title"`
Content string `json:"content"`
Dir string `json:"dir"`
Group string `json:"group"`
Template string `json:"template"`
Extra map[string]string `json:"extra"`
Date string `json:"date"`
Edit jsonBoolean `json:"edit"`
InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation"`
}
func executeCommandNew(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) {
@ -53,7 +51,6 @@ func executeCommandNew(notebook *core.Notebook, documents *documentStore, contex
Group: opt.NewNotEmptyString(opts.Group),
Template: opt.NewNotEmptyString(opts.Template),
Extra: opts.Extra,
DryRun: bool(opts.DryRun),
Date: date,
})
if err != nil {
@ -72,41 +69,47 @@ func executeCommandNew(notebook *core.Notebook, documents *documentStore, contex
return nil, errors.New("zk.new could not generate a new note")
}
if opts.InsertContentAtLocation != nil {
if opts.InsertLinkAtLocation != nil {
doc, ok := documents.Get(opts.InsertLinkAtLocation.URI)
if !ok {
return nil, fmt.Errorf("can't insert link in %s", opts.InsertLinkAtLocation.URI)
}
linkFormatter, err := notebook.NewLinkFormatter()
if err != nil {
return nil, err
}
path := core.NotebookPath{
Path: note.Path,
BasePath: notebook.Path,
WorkingDir: filepath.Dir(doc.Path),
}
linkFormatterContext, err := core.NewLinkFormatterContext(path, note.Title, note.Metadata)
if err != nil {
return nil, err
}
link, err := linkFormatter(linkFormatterContext)
if err != nil {
return nil, err
}
go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
Changes: map[string][]protocol.TextEdit{
opts.InsertContentAtLocation.URI: {{Range: opts.InsertContentAtLocation.Range, NewText: note.RawContent}},
opts.InsertLinkAtLocation.URI: {{Range: opts.InsertLinkAtLocation.Range, NewText: link}},
},
},
}, nil)
}
if !opts.DryRun && opts.InsertLinkAtLocation != nil {
minNote := note.AsMinimalNote()
info := &linkInfo{
note: &minNote,
location: opts.InsertLinkAtLocation,
title: &opts.Title,
}
err := linkNote(notebook, documents, context, info)
if err != nil {
return nil, err
}
}
absPath := filepath.Join(notebook.Path, note.Path)
if !opts.DryRun && opts.Edit {
if opts.Edit {
go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
URI: pathToURI(absPath),
TakeFocus: boolPtr(true),
}, nil)
}
return map[string]interface{}{
"path": absPath,
"content": note.RawContent,
}, nil
return map[string]interface{}{"path": absPath}, nil
}

@ -3,9 +3,9 @@ package lsp
import (
"fmt"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
)
const cmdTagList = "zk.tag.list"

@ -3,8 +3,8 @@ package lsp
import (
"path/filepath"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/paths"
)
// completionTemplates holds templates to render the various elements of an LSP

@ -2,17 +2,15 @@ package lsp
import (
"net/url"
"path/filepath"
"regexp"
"strings"
"unicode/utf16"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
strutil "github.com/zk-org/zk/internal/util/strings"
)
// documentStore holds opened documents.
@ -132,46 +130,34 @@ func (d *document) GetLines() []string {
// LookBehind returns the n characters before the given position, on the same line.
func (d *document) LookBehind(pos protocol.Position, length int) string {
line, ok := d.GetLine(int(pos.Line))
utf16Bytes := utf16.Encode([]rune(line))
if !ok {
return ""
}
charIdx := int(pos.Character)
if length > charIdx {
return string(utf16.Decode(utf16Bytes[0:charIdx]))
return line[0:charIdx]
}
return string(utf16.Decode(utf16Bytes[(charIdx - length):charIdx]))
return line[(charIdx - length):charIdx]
}
// LookForward returns the n characters after the given position, on the same line.
func (d *document) LookForward(pos protocol.Position, length int) string {
line, ok := d.GetLine(int(pos.Line))
utf16Bytes := utf16.Encode([]rune(line))
if !ok {
return ""
}
lineLength := len(utf16Bytes)
lineLength := len(line)
charIdx := int(pos.Character)
if lineLength <= charIdx+length {
return string(utf16.Decode(utf16Bytes[charIdx:]))
return line[charIdx:]
}
return string(utf16.Decode(utf16Bytes[charIdx:(charIdx + length)]))
return line[charIdx:(charIdx + length)]
}
// LinkFromRoot returns a Link to this document from the root of the given
// notebook.
func (d *document) LinkFromRoot(nb *core.Notebook) (*documentLink, error) {
href, err := nb.RelPath(d.Path)
if err != nil {
return nil, err
}
return &documentLink{
Href: href,
RelativeToDir: nb.Path,
}, nil
}
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?: *\| *(.+?))?\]\]`)
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
// DocumentLinkAt returns the internal or external link found in the document
// at the given position.
@ -190,66 +176,6 @@ func (d *document) DocumentLinkAt(pos protocol.Position) (*documentLink, error)
return nil, nil
}
// Recursive function to check whether a link is within inline code.
func linkWithinInlineCode(strBuffer string, linkStart, linkEnd int, insideInline bool) bool {
if backtickId := strings.Index(strBuffer, "`"); backtickId >= 0 && backtickId < linkEnd {
return linkWithinInlineCode(strBuffer[backtickId+1:],
linkStart-backtickId-1, linkEnd-backtickId-1, !insideInline)
} else {
return insideInline
}
}
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?: *\| *(.+?))?\]\]`)
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
var fileURIregex = regexp.MustCompile(`file:///`)
var fencedStartRegex = regexp.MustCompile(`^(` + "```" + `|~~~).*`)
var fencedEndRegex = regexp.MustCompile(`^(` + "```" + `|~~~)\s*`)
var indentedRegex = regexp.MustCompile(`^(\s{4}|\t).+`)
var insideInline = false
var insideFenced = false
var insideIndented = false
var currentCodeBlockStart = -1
// check whether the current line in document is within a fenced or indented
// code block
func isLineWithinCodeBlock(lines []string, lineIndex int, line string) bool {
// if line is already within code fences or indented code block
if insideFenced {
if fencedEndRegex.FindStringIndex(line) != nil &&
lines[currentCodeBlockStart][:3] == line[:3] {
// Fenced code block ends with this line
insideFenced = false
currentCodeBlockStart = -1
}
return true
} else if insideIndented {
if indentedRegex.FindStringIndex(line) == nil && len(line) > 0 {
// Indeted code block ends with this line
insideIndented = false
currentCodeBlockStart = -1
} else {
return true
}
} else {
// Check whether the current line is the start of a code fence or
// indented code block
if fencedStartRegex.FindStringIndex(line) != nil {
insideFenced = true
currentCodeBlockStart = lineIndex
return true
} else if indentedRegex.FindStringIndex(line) != nil &&
(lineIndex > 0 && len(lines[lineIndex-1]) == 0 || lineIndex == 0) {
insideIndented = true
currentCodeBlockStart = lineIndex
return true
}
}
return false
}
// DocumentLinks returns all the internal and external links found in the
// document.
func (d *document) DocumentLinks() ([]documentLink, error) {
@ -258,22 +184,13 @@ func (d *document) DocumentLinks() ([]documentLink, error) {
lines := d.GetLines()
for lineIndex, line := range lines {
if isLineWithinCodeBlock(lines, lineIndex, line) {
continue
}
appendLink := func(href string, start, end int, hasTitle bool, isWikiLink bool) {
if href == "" {
return
}
// Go regexes work with bytes, but the LSP client expects character indexes.
start = strutil.ByteIndexToRuneIndex(line, start)
end = strutil.ByteIndexToRuneIndex(line, end)
links = append(links, documentLink{
Href: href,
RelativeToDir: filepath.Dir(d.Path),
Href: href,
Range: protocol.Range{
Start: protocol.Position{
Line: protocol.UInteger(lineIndex),
@ -289,51 +206,25 @@ func (d *document) DocumentLinks() ([]documentLink, error) {
})
}
// extract link paths from [title](path) patterns
// note: match[0:1] is the entire match, match[2:3] is the contents of
// brackets, match[4:5] is contents of parentheses
for _, match := range markdownLinkRegex.FindAllStringSubmatchIndex(line, -1) {
// Ignore when inside backticks: `[title](file)`
if linkWithinInlineCode(line, match[0], match[1], insideInline) {
continue
}
// Ignore embedded images ![title](file.png)
// Ignore embedded image, e.g. ![title](href.png)
if match[0] > 0 && line[match[0]-1] == '!' {
continue
}
// ignore tripple dash file URIs [title](file:///foo.go)
if match[5]-match[4] >= 8 {
linkURL := line[match[4]:match[5]]
fileURIresult := linkURL[:8]
if fileURIregex.MatchString(fileURIresult) {
continue
}
}
href := line[match[4]:match[5]]
// Decode the href if it's percent-encoded
// Valid Markdown links are percent-encoded.
if decodedHref, err := url.PathUnescape(href); err == nil {
href = decodedHref
}
appendLink(href, match[0], match[1], false, false)
}
for _, match := range wikiLinkRegex.FindAllStringSubmatchIndex(line, -1) {
// Ignore when inside backticks: `[[filename]]`
if linkWithinInlineCode(line, match[0], match[1], insideInline) {
continue
}
href := line[match[2]:match[3]]
hasTitle := match[4] != -1
appendLink(href, match[0], match[1], hasTitle, true)
}
if strings.Count(line, "`")%2 == 1 {
insideInline = !insideInline
}
}
return links, nil
@ -345,16 +236,13 @@ func (d *document) IsTagPosition(position protocol.Position, noteContentParser c
lineIdx := int(position.Line)
charIdx := int(position.Character)
line := lines[lineIdx]
// https://github.com/zk-org/zk/issues/144#issuecomment-1006108485
// https://github.com/mickael-menu/zk/issues/144#issuecomment-1006108485
line = line[:charIdx] + "ZK_PLACEHOLDER" + line[charIdx:]
lines[lineIdx] = line
targetWord := strutil.WordAt(line, charIdx)
if targetWord == "" {
return false
}
if string(targetWord[0]) == "#" {
targetWord = targetWord[1:]
}
content := strings.Join(lines, "\n")
note, err := noteContentParser.ParseNoteContent(content)
@ -365,9 +253,8 @@ func (d *document) IsTagPosition(position protocol.Position, noteContentParser c
}
type documentLink struct {
Href string
RelativeToDir string
Range protocol.Range
Href string
Range protocol.Range
// HasTitle indicates whether this link has a title information. For
// example [[filename]] doesn't but [[filename|title]] does.
HasTitle bool

@ -3,16 +3,16 @@ package lsp
import (
"encoding/json"
"fmt"
"os"
"io/ioutil"
"path/filepath"
"strings"
"time"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
glspserv "github.com/tliron/glsp/server"
@ -209,7 +209,7 @@ func NewServer(opts ServerOpts) *Server {
handler.CompletionItemResolve = func(context *glsp.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) {
if path, ok := params.Data.(string); ok {
content, err := os.ReadFile(path)
content, err := ioutil.ReadFile(path)
if err != nil {
return params, err
}
@ -238,7 +238,7 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.noteForLink(*link, notebook)
target, err := server.noteForLink(*link, doc, notebook)
if err != nil || target == nil {
return nil, err
}
@ -250,7 +250,7 @@ func NewServer(opts ServerOpts) *Server {
}
path = fs.Canonical(path)
contents, err := os.ReadFile(path)
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
@ -281,24 +281,15 @@ func NewServer(opts ServerOpts) *Server {
documentLinks := []protocol.DocumentLink{}
for _, link := range links {
var target string
if strutil.IsURL(link.Href) {
// External link
target = link.Href
} else {
// Internal note link
targetNote, err := server.noteForLink(link, notebook)
if targetNote != nil && err == nil {
target = targetNote.URI
}
target, err := server.noteForLink(link, doc, notebook)
if target == nil || err != nil {
continue
}
if target != "" {
documentLinks = append(documentLinks, protocol.DocumentLink{
Range: link.Range,
Target: &target,
})
}
documentLinks = append(documentLinks, protocol.DocumentLink{
Range: link.Range,
Target: &target.URI,
})
}
return documentLinks, err
@ -320,7 +311,7 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.noteForLink(*link, notebook)
target, err := server.noteForLink(*link, doc, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
@ -369,13 +360,6 @@ func NewServer(opts ServerOpts) *Server {
}
return executeCommandNew(nb, server.documents, context, params.Arguments)
case cmdLink:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandLink(nb, server.documents, context, params.Arguments)
case cmdList:
nb, err := openNotebook()
if err != nil {
@ -428,7 +412,6 @@ func NewServer(opts ServerOpts) *Server {
Title: actionTitle,
Kind: stringPtr(protocol.CodeActionKindRefactor),
Command: &protocol.Command{
Title: actionTitle,
Command: cmdNew,
Arguments: []interface{}{wd, jsonOpts},
},
@ -459,19 +442,25 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
if link == nil {
link, err = doc.LinkFromRoot(notebook)
href, err := notebook.RelPath(doc.Path)
if err != nil {
return nil, err
}
link = &documentLink{Href: href}
}
target, err := server.noteForLink(*link, notebook)
target, err := server.noteForLink(*link, doc, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
p, err := notebook.RelPath(target.Path)
if err != nil {
return nil, err
}
opts := core.NoteFindOpts{
LinkTo: &core.LinkFilter{Hrefs: []string{target.Path}},
LinkTo: &core.LinkFilter{Hrefs: []string{p}},
}
notes, err := notebook.FindNotes(opts)
@ -521,17 +510,21 @@ func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
return s.notebooks.Open(doc.Path)
}
// noteForLink returns the Note object for the note targeted by the given link.
// noteForLink returns the LSP documentUri for the note targeted by the given link.
//
// Match by order of precedence:
// 1. Prefix of relative path
// 2. Find any occurrence of the href in a note path (substring)
// 3. Match the href as a term in the note titles
func (s *Server) noteForLink(link documentLink, notebook *core.Notebook) (*Note, error) {
note, err := s.noteForHref(link.Href, link.RelativeToDir, notebook)
func (s *Server) noteForLink(link documentLink, doc *document, notebook *core.Notebook) (*Note, error) {
note, err := s.noteForHref(link.Href, doc, notebook)
if note == nil && err == nil && link.IsWikiLink {
// Try to find a partial href match.
note, err = notebook.FindByHref(link.Href, true)
if note == nil && err == nil {
// Fallback on matching the note title.
note, err = s.noteMatchingTitle(link.Href, notebook)
}
}
if note == nil || err != nil {
return nil, err
@ -541,17 +534,13 @@ func (s *Server) noteForLink(link documentLink, notebook *core.Notebook) (*Note,
return &Note{*note, pathToURI(joined_path)}, nil
}
// noteForHref returns the Note object for the note targeted by the given HREF
// relative to relativeToDir.
func (s *Server) noteForHref(href string, relativeToDir string, notebook *core.Notebook) (*core.MinimalNote, error) {
// noteForHref returns the LSP documentUri for the note targeted by the given HREF.
func (s *Server) noteForHref(href string, doc *document, notebook *core.Notebook) (*core.MinimalNote, error) {
if strutil.IsURL(href) {
return nil, nil
}
path := href
if relativeToDir != "" {
path = filepath.Clean(filepath.Join(relativeToDir, path))
}
path := filepath.Clean(filepath.Join(filepath.Dir(doc.Path), href))
path, err := filepath.Rel(notebook.Path, path)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve href: %s", href)
@ -563,6 +552,19 @@ func (s *Server) noteForHref(href string, relativeToDir string, notebook *core.N
return note, err
}
// noteMatchingTitle returns the LSP documentUri for the note matching the given search terms.
func (s *Server) noteMatchingTitle(terms string, notebook *core.Notebook) (*core.MinimalNote, error) {
if terms == "" {
return nil, nil
}
note, err := notebook.FindMatching("title:(" + terms + ")")
if err != nil {
s.logger.Printf("findMatching(title: %s): %s", terms, err.Error())
}
return note, err
}
type Note struct {
core.MinimalNote
URI protocol.DocumentUri
@ -603,7 +605,7 @@ func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyF
if strutil.IsURL(link.Href) {
continue
}
target, err := s.noteForLink(link, notebook)
target, err := s.noteForLink(link, doc, notebook)
if err != nil {
s.logger.Err(err)
continue
@ -643,16 +645,10 @@ func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyF
// buildInvokedCompletionList builds the completion item response for a
// completion started automatically when typing an identifier, or manually.
func (s *Server) buildInvokedCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
currentWord := doc.WordAt(position)
if strings.HasPrefix(doc.LookBehind(position, len(currentWord)+2), "[[") {
return s.buildLinkCompletionList(notebook, doc, position)
}
if doc.IsTagPosition(position, notebook.Parser) {
return s.buildTagCompletionList(notebook, doc.WordAt(position))
if !doc.IsTagPosition(position, notebook.Parser) {
return nil, nil
}
return nil, nil
return s.buildTagCompletionList(notebook, doc.WordAt(position))
}
// buildTriggerCompletionList builds the completion item response for a
@ -810,18 +806,12 @@ func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNot
if s.useAdditionalTextEditsWithNotebook(notebook) {
addTextEdits := []protocol.TextEdit{}
startOffset := -2
if doc.LookBehind(pos, 2) != "[[" {
currentWord := doc.WordAt(pos)
startOffset = -2 - len(currentWord)
}
// Some LSP clients (e.g. VSCode) don't support deleting the trigger
// characters with the main TextEdit. So let's add an additional
// TextEdit for that.
addTextEdits = append(addTextEdits, protocol.TextEdit{
NewText: "",
Range: rangeFromPosition(pos, startOffset, 0),
Range: rangeFromPosition(pos, -2, 0),
})
item.AdditionalTextEdits = addTextEdits
@ -848,12 +838,7 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo
// Overwrite [[ trigger directly if the additional text edits are disabled.
startOffset := 0
if !s.useAdditionalTextEditsWithNotebook(notebook) {
if doc.LookBehind(pos, 2) == "[[" {
startOffset = -2
} else {
currentWord := doc.WordAt(pos)
startOffset = -2 - len(currentWord)
}
startOffset = -2
}
// Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to

@ -3,14 +3,10 @@ package lsp
import (
"fmt"
"net/url"
"path/filepath"
"runtime"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
"github.com/mickael-menu/zk/internal/util/errors"
)
func pathToURI(path string) string {
@ -60,59 +56,3 @@ func (b *jsonBoolean) UnmarshalJSON(data []byte) error {
}
return nil
}
type linkInfo struct {
note *core.MinimalNote
location *protocol.Location
title *string
}
func linkNote(notebook *core.Notebook, documents *documentStore, context *glsp.Context, info *linkInfo) error {
if info.location == nil {
return errors.New("'location' not provided")
}
// Get current document to edit
doc, ok := documents.Get(info.location.URI)
if !ok {
return fmt.Errorf("Cannot insert link in '%s'", info.location.URI)
}
formatter, err := notebook.NewLinkFormatter()
if err != nil {
return err
}
path := core.NotebookPath{
Path: info.note.Path,
BasePath: notebook.Path,
WorkingDir: filepath.Dir(doc.Path),
}
var title *string
title = info.title
if title == nil {
title = &info.note.Title
}
formatterContext, err := core.NewLinkFormatterContext(path, *title, info.note.Metadata)
if err != nil {
return err
}
link, err := formatter(formatterContext)
if err != nil {
return err
}
go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
Changes: map[string][]protocol.TextEdit{
info.location.URI: {{Range: info.location.Range, NewText: link}},
},
},
}, nil)
return nil
}

@ -6,6 +6,7 @@ import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
@ -13,7 +14,7 @@ import (
// Tags represents a list of inline tags in a Markdown document.
type Tags struct {
ast.BaseInline
gast.BaseInline
// Tags in this list.
Tags []string
}
@ -21,13 +22,13 @@ type Tags struct {
func (n *Tags) Dump(source []byte, level int) {
m := map[string]string{}
m["Tags"] = strings.Join(n.Tags, ", ")
ast.DumpHelper(n, source, level, m, nil)
gast.DumpHelper(n, source, level, m, nil)
}
// KindTags is a NodeKind of the Tags node.
var KindTags = ast.NewNodeKind("Tags")
var KindTags = gast.NewNodeKind("Tags")
func (n *Tags) Kind() ast.NodeKind {
func (n *Tags) Kind() gast.NodeKind {
return KindTags
}
@ -167,7 +168,7 @@ func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
block.Advance(endPos)
return &Tags{
BaseInline: ast.BaseInline{},
BaseInline: gast.BaseInline{},
Tags: []string{tag},
}
}
@ -247,7 +248,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
block.Advance(endPos)
return &Tags{
BaseInline: ast.BaseInline{},
BaseInline: gast.BaseInline{},
Tags: tags,
}
}
@ -266,7 +267,7 @@ func isValidTag(tag string) bool {
}
// Prevent Markdown table syntax to be parsed a a colon tag, e.g. |:---:|
// https://github.com/zk-org/zk/issues/185
// https://github.com/mickael-menu/zk/issues/185
for _, c := range tag {
if c != '-' {
return true

@ -3,7 +3,7 @@ package extensions
import (
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/mickael-menu/zk/internal/core"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"

@ -7,12 +7,12 @@ import (
"regexp"
"strings"
"github.com/zk-org/zk/internal/adapter/markdown/extensions"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/zk-org/zk/internal/util/yaml"
"github.com/mickael-menu/zk/internal/adapter/markdown/extensions"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/opt"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/util/yaml"
"github.com/mvdan/xurls"
"github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta"

@ -3,10 +3,10 @@ package markdown
import (
"testing"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestParseTitle(t *testing.T) {
@ -190,7 +190,7 @@ func TestParseHashtags(t *testing.T) {
test("#multi word# end", []string{"multi"})
// Single character
// See https://github.com/zk-org/zk/issues/118
// See https://github.com/mickael-menu/zk/issues/118
test("#a", []string{"a"})
}
@ -570,7 +570,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
})
// Markdown links are decoded, but not WikiLinks.
// i.e. https://github.com/zk-org/zk/issues/86
// i.e. https://github.com/mickael-menu/zk/issues/86
test("[foo%20bar](202110031652%20foo%20bar)", []core.Link{
{
Title: "foo%20bar",

@ -5,9 +5,9 @@ import (
"fmt"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
)
// CollectionDAO persists collections (e.g. tags) in the SQLite database.

@ -3,9 +3,9 @@ package sqlite
import (
"testing"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestCollectionDAOFindOrCreate(t *testing.T) {

@ -3,11 +3,10 @@ package sqlite
import (
"database/sql"
"fmt"
"regexp"
sqlite "github.com/mattn/go-sqlite3"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
)
func init() {
@ -17,9 +16,6 @@ func init() {
if err := conn.RegisterFunc("mention_query", buildMentionQuery, true); err != nil {
return err
}
if err := conn.RegisterFunc("regexp", regexp.MatchString, true); err != nil {
return err
}
return nil
},
})
@ -214,7 +210,7 @@ func (db *DB) migrate() error {
{ // 7
SQL: []string{},
// https://github.com/zk-org/zk/issues/170#issuecomment-1107848441
// https://github.com/mickael-menu/zk/issues/170#issuecomment-1107848441
NeedsReindexing: true,
},
}

@ -3,8 +3,8 @@ package sqlite
import (
"testing"
"github.com/zk-org/zk/internal/util/fixtures"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/fixtures"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestOpen(t *testing.T) {

@ -4,8 +4,8 @@ import (
"database/sql"
"fmt"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
)
// LinkDAO persists links in the SQLite database.
@ -15,8 +15,8 @@ type LinkDAO struct {
// Prepared SQL statements
addLinkStmt *LazyStmt
setLinksTargetStmt *LazyStmt
removeLinksStmt *LazyStmt
updateTargetIDStmt *LazyStmt
}
// NewLinkDAO creates a new instance of a DAO working on the given database
@ -32,17 +32,19 @@ func NewLinkDAO(tx Transaction, logger util.Logger) *LinkDAO {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
// Set links matching a given href and missing a target ID to the given
// target ID.
setLinksTargetStmt: tx.PrepareLazy(`
UPDATE links
SET target_id = ?
WHERE target_id IS NULL AND external = 0 AND ? LIKE href || '%'
`),
// Remove all the outbound links of a note.
removeLinksStmt: tx.PrepareLazy(`
DELETE FROM links
WHERE source_id = ?
`),
updateTargetIDStmt: tx.PrepareLazy(`
UPDATE links
SET target_id = ?
WHERE id = ?
`),
}
}
@ -67,9 +69,10 @@ func (d *LinkDAO) RemoveAll(id core.NoteID) error {
return err
}
// SetTargetID updates the target note of a link.
func (d *LinkDAO) SetTargetID(id core.LinkID, targetID core.NoteID) error {
_, err := d.updateTargetIDStmt.Exec(noteIDToSQL(targetID), linkIDToSQL(id))
// SetTargetID updates the missing target_id for links matching the given href.
// FIXME: Probably doesn't work for all type of href (partial, wikilinks, etc.)
func (d *LinkDAO) SetTargetID(href string, id core.NoteID) error {
_, err := d.setLinksTargetStmt.Exec(int64(id), href)
return err
}
@ -87,31 +90,15 @@ func joinLinkRels(rels []core.LinkRelation) string {
return res
}
// FindInternal returns all the links internal to the notebook.
func (d *LinkDAO) FindInternal() ([]core.ResolvedLink, error) {
return d.findWhere("external = 0")
}
// FindBetweenNotes returns all the links existing between the given notes.
func (d *LinkDAO) FindBetweenNotes(ids []core.NoteID) ([]core.ResolvedLink, error) {
idsString := joinNoteIDs(ids, ",")
return d.findWhere(fmt.Sprintf("source_id IN (%s) AND target_id IN (%s)", idsString, idsString))
}
// findWhere returns all the links, filtered by the given where query.
func (d *LinkDAO) findWhere(where string) ([]core.ResolvedLink, error) {
links := make([]core.ResolvedLink, 0)
query := `
idsString := joinNoteIDs(ids, ",")
rows, err := d.tx.Query(fmt.Sprintf(`
SELECT id, source_id, source_path, target_id, target_path, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM resolved_links
`
if where != "" {
query += "\nWHERE " + where
}
rows, err := d.tx.Query(query)
WHERE source_id IN (%s) AND target_id IN (%s)
`, idsString, idsString))
if err != nil {
return links, err
}
@ -133,11 +120,10 @@ func (d *LinkDAO) findWhere(where string) ([]core.ResolvedLink, error) {
func (d *LinkDAO) scanLink(row RowScanner) (*core.ResolvedLink, error) {
var (
id, sourceID, snippetStart, snippetEnd int
targetID sql.NullInt64
sourcePath, title, href, linkType, snippet string
external bool
targetPath, rels sql.NullString
id, sourceID, targetID, snippetStart, snippetEnd int
sourcePath, targetPath, title, href, linkType, snippet string
external bool
rels sql.NullString
)
err := row.Scan(
@ -151,11 +137,10 @@ func (d *LinkDAO) scanLink(row RowScanner) (*core.ResolvedLink, error) {
return nil, err
default:
return &core.ResolvedLink{
ID: core.LinkID(id),
SourceID: core.NoteID(sourceID),
SourcePath: sourcePath,
TargetID: core.NoteID(targetID.Int64),
TargetPath: targetPath.String,
TargetID: core.NoteID(targetID),
TargetPath: targetPath,
Link: core.Link{
Title: title,
Href: href,

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func testLinkDAO(t *testing.T, callback func(tx Transaction, dao *LinkDAO)) {

@ -3,7 +3,7 @@ package sqlite
import (
"database/sql"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/errors"
)
// Known metadata keys.

@ -3,7 +3,7 @@ package sqlite
import (
"testing"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestMetadataDAOGetUnknown(t *testing.T) {

@ -8,12 +8,14 @@ import (
"strings"
"time"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/fts5"
"github.com/zk-org/zk/internal/util/paths"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/fts5"
"github.com/mickael-menu/zk/internal/util/icu"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/paths"
strutil "github.com/mickael-menu/zk/internal/util/strings"
)
// NoteDAO persists notes in the SQLite database.
@ -74,7 +76,7 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO {
SELECT id FROM notes
WHERE path REGEXP ?
-- To find the best match possible, we sort by path length.
-- See https://github.com/zk-org/zk/issues/23
-- See https://github.com/mickael-menu/zk/issues/23
ORDER BY LENGTH(path) ASC
`),
@ -277,13 +279,12 @@ func (d *NoteDAO) findIdsByHrefs(hrefs []string, allowPartialHrefs bool) ([]core
return ids, nil
}
// FIXME: This logic is duplicated in NoteIndex.linkMatchesPath(). Maybe there's a way to share it using a custom SQLite function?
func (d *NoteDAO) FindIdsByHref(href string, allowPartialHref bool) ([]core.NoteID, error) {
// Remove any anchor at the end of the HREF, since it's most likely
// matching a sub-section in the note.
href = strings.SplitN(href, "#", 2)[0]
href = regexp.QuoteMeta(href)
href = icu.EscapePattern(href)
if allowPartialHref {
ids, err := d.findIdsByPathRegex("^(.*/)?[^/]*" + href + "[^/]*$")
@ -297,7 +298,7 @@ func (d *NoteDAO) FindIdsByHref(href string, allowPartialHref bool) ([]core.Note
}
}
ids, err := d.findIdsByPathRegex("^(?:" + href + "[^/]*|" + href + "/.+)$")
ids, err := d.findIdsByPathRegex(href + "[^/]*|" + href + "/.+")
if len(ids) > 0 || err != nil {
return ids, err
}
@ -379,8 +380,8 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
if opts.Mention == nil {
return opts, nil
}
if opts.MatchStrategy != core.MatchStrategyFts {
return opts, fmt.Errorf("--mention can only be used with --match-strategy=fts")
if opts.ExactMatch {
return opts, fmt.Errorf("--exact-match and --mention cannot be used together")
}
// Find the IDs for the mentioned paths.
@ -420,7 +421,9 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
}
// Expand the mention queries in the match predicate.
opts.Match = append(opts.Match, " ("+strings.Join(mentionQueries, " OR ")+") ")
match := opts.Match.String()
match += " " + strings.Join(mentionQueries, " OR ")
opts.Match = opt.NewString(match)
return opts, nil
}
@ -445,7 +448,7 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
transitiveClosure := false
maxDistance := 0
setupLinkFilter := func(tableAlias string, hrefs []string, direction int, negate, recursive bool) error {
setupLinkFilter := func(hrefs []string, direction int, negate, recursive bool) error {
ids, err := d.findIdsByHrefs(hrefs, true /* allowPartialHrefs */)
if err != nil {
return err
@ -460,29 +463,27 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
if recursive {
transitiveClosure = true
linksSrc = "transitive_closure"
additionalOrderTerms = append(additionalOrderTerms, tableAlias+".distance")
}
if !negate {
if direction != 0 {
snippetCol = fmt.Sprintf("GROUP_CONCAT(REPLACE(%s.snippet, %[1]s.title, '<zk:match>' || %[1]s.title || '</zk:match>'), '\x01')", tableAlias)
snippetCol = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '<zk:match>' || l.title || '</zk:match>'), '\x01')"
}
joinOns := make([]string, 0)
if direction <= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = %[1]s.target_id AND %[1]s.source_id IN %[2]s)", tableAlias, idsList,
"(n.id = l.target_id AND l.source_id IN %s)", idsList,
))
}
if direction >= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = %[1]s.source_id AND %[1]s.target_id IN %[2]s)", tableAlias, idsList,
"(n.id = l.source_id AND l.target_id IN %s)", idsList,
))
}
joinClauses = append(joinClauses, fmt.Sprintf(
"LEFT JOIN %[2]s %[1]s ON %[3]s",
tableAlias,
"LEFT JOIN %s l ON %s",
linksSrc,
strings.Join(joinOns, " OR "),
))
@ -516,27 +517,16 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
return nil
}
if 0 < len(opts.Match) {
switch opts.MatchStrategy {
case core.MatchStrategyExact:
for _, match := range opts.Match {
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(match, '\\'))
}
case core.MatchStrategyFts:
if !opts.Match.IsNull() {
if opts.ExactMatch {
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(opts.Match.String(), '\\'))
} else {
snippetCol = `snippet(fts_match.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts fts_match ON n.id = fts_match.rowid")
additionalOrderTerms = append(additionalOrderTerms, `bm25(fts_match.notes_fts, 1000.0, 500.0, 1.0)`)
for _, match := range opts.Match {
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(match))
}
case core.MatchStrategyRe:
for _, match := range opts.Match {
whereExprs = append(whereExprs, "n.raw_content REGEXP ?")
args = append(args, match)
}
break
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(opts.Match.String()))
}
}
@ -623,7 +613,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkedBy != nil {
filter := opts.LinkedBy
maxDistance = filter.MaxDistance
err := setupLinkFilter("l_by", filter.Hrefs, -1, filter.Negate, filter.Recursive)
err := setupLinkFilter(filter.Hrefs, -1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -632,7 +622,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkTo != nil {
filter := opts.LinkTo
maxDistance = filter.MaxDistance
err := setupLinkFilter("l_to", filter.Hrefs, 1, filter.Negate, filter.Recursive)
err := setupLinkFilter(filter.Hrefs, 1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -640,11 +630,11 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.Related != nil {
maxDistance = 2
err := setupLinkFilter("l_rel", opts.Related, 0, false, true)
err := setupLinkFilter(opts.Related, 0, false, true)
if err != nil {
return nil, err
}
groupBy += " HAVING MIN(l_rel.distance) = 2"
groupBy += " HAVING MIN(l.distance) = 2"
}
if opts.Orphan {
@ -692,6 +682,8 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
// Credit to https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network
if transitiveClosure {
orderTerms = append([]string{"l.distance"}, orderTerms...)
query += `WITH RECURSIVE transitive_closure(source_id, target_id, title, snippet, distance, path) AS (
SELECT source_id, target_id, title, snippet,
1 AS distance,

@ -6,11 +6,11 @@ import (
"testing"
"time"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/paths"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestNoteDAOIndexed(t *testing.T) {
@ -239,7 +239,7 @@ func TestNoteDAOFindIdsByHref(t *testing.T) {
test("test", true, []core.NoteID{6, 5, 8})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []core.NoteID{8})
}
@ -258,7 +258,7 @@ func TestNoteDAOFindIncludingHrefs(t *testing.T) {
test("test", true, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []string{"ref/test/ref.md"})
}
@ -280,7 +280,7 @@ func TestNoteDAOFindExcludingHrefs(t *testing.T) {
"log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md",
"log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
}
@ -312,10 +312,9 @@ func TestNoteDAOFindMinimalAll(t *testing.T) {
func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
Match: opt.NewString("daily | index"),
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
})
assert.Nil(t, err)
@ -367,10 +366,7 @@ func TestNoteDAOFindTag(t *testing.T) {
func TestNoteDAOFindMatch(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
},
core.NoteFindOpts{Match: opt.NewString("daily | index")},
[]core.ContextualNote{
{
Note: core.Note{
@ -452,26 +448,10 @@ func TestNoteDAOFindMatch(t *testing.T) {
)
}
func TestNoteDAOFindMatchWithMultiMatch(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: []string{"daily | index", "second"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
},
[]string{
"log/2021-01-04.md",
},
)
}
func TestNoteDAOFindMatchWithSort(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Match: opt.NewString("daily | index"),
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
@ -489,8 +469,8 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test := func(match string, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: []string{match},
MatchStrategy: core.MatchStrategyExact,
Match: opt.NewString(match),
ExactMatch: true,
},
expected,
)
@ -502,27 +482,13 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test(`[exact% ch\ar_acters]`, []string{"ref/test/a.md"})
}
func TestNoteDAOFindMentionRequiresFtsMatchStrategy(t *testing.T) {
func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyExact,
Mention: []string{"mention"},
ExactMatch: true,
Mention: []string{"mention"},
})
assert.Err(t, err, "--mention can only be used with --match-strategy=fts")
})
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyRe,
Mention: []string{"mention"},
})
assert.Err(t, err, "--mention can only be used with --match-strategy=fts")
})
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"mention"},
})
assert.Err(t, err, "could not find notes at: mention")
assert.Err(t, err, "--exact-match and --mention cannot be used together")
})
}
@ -593,10 +559,7 @@ func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) {
func TestNoteDAOFindMentions(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"log/2021-01-03.md", "index.md"},
},
core.NoteFindOpts{Mention: []string{"log/2021-01-03.md", "index.md"}},
[]core.ContextualNote{
{
Note: core.Note{
@ -660,8 +623,7 @@ func TestNoteDAOFindMentions(t *testing.T) {
func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"log/2021-01-03.md", "index.md"},
Mention: []string{"log/2021-01-03.md", "index.md"},
LinkTo: &core.LinkFilter{
Hrefs: []string{"log/2021-01-03.md", "index.md"},
Negate: true,
@ -674,8 +636,7 @@ func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
func TestNoteDAOFindMentionUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"will-not-be-found"},
Mention: []string{"will-not-be-found"},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")
@ -684,10 +645,7 @@ func TestNoteDAOFindMentionUnknown(t *testing.T) {
func TestNoteDAOFindMentionedBy(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
},
core.NoteFindOpts{MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"}},
[]core.ContextualNote{
{
Note: core.Note{
@ -739,8 +697,7 @@ func TestNoteDAOFindMentionedBy(t *testing.T) {
func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
LinkedBy: &core.LinkFilter{
Hrefs: []string{"ref/test/b.md", "log/2021-01-04.md"},
Negate: true,
@ -753,8 +710,7 @@ func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
func TestNoteDAOFindMentionedByUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"will-not-be-found"},
MentionedBy: []string{"will-not-be-found"},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")

@ -1,24 +1,18 @@
package sqlite
import (
"path/filepath"
"regexp"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/paths"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/paths"
)
// NoteIndex persists note indexing results in the SQLite database.
// It implements the port core.NoteIndex and acts as a facade to the DAOs.
type NoteIndex struct {
notebookPath string
db *DB
dao *dao
logger util.Logger
db *DB
dao *dao
logger util.Logger
}
type dao struct {
@ -28,11 +22,10 @@ type dao struct {
metadata *MetadataDAO
}
func NewNoteIndex(notebookPath string, db *DB, logger util.Logger) *NoteIndex {
func NewNoteIndex(db *DB, logger util.Logger) *NoteIndex {
return &NoteIndex{
notebookPath: notebookPath,
db: db,
logger: logger,
db: db,
logger: logger,
}
}
@ -54,37 +47,6 @@ func (ni *NoteIndex) FindMinimal(opts core.NoteFindOpts) (notes []core.MinimalNo
return
}
// FindLinkMatch implements core.NoteIndex.
func (ni *NoteIndex) FindLinkMatch(baseDir string, href string, linkType core.LinkType) (id core.NoteID, err error) {
err = ni.commit(func(dao *dao) error {
id, err = ni.findLinkMatch(dao, baseDir, href, linkType)
return err
})
return
}
func (ni *NoteIndex) findLinkMatch(dao *dao, baseDir string, href string, linkType core.LinkType) (core.NoteID, error) {
if strutil.IsURL(href) {
return 0, nil
}
id, _ := ni.findPathMatch(dao, baseDir, href)
if id.IsValid() {
return id, nil
}
allowPartialMatch := (linkType == core.LinkTypeWikiLink)
return dao.notes.FindIdByHref(href, allowPartialMatch)
}
func (ni *NoteIndex) findPathMatch(dao *dao, baseDir string, href string) (core.NoteID, error) {
href, err := ni.relNotebookPath(baseDir, href)
if err != nil {
return 0, err
}
return dao.notes.FindIdByHref(href, false)
}
// FindLinksBetweenNotes implements core.NoteIndex.
func (ni *NoteIndex) FindLinksBetweenNotes(ids []core.NoteID) (links []core.ResolvedLink, err error) {
err = ni.commit(func(dao *dao) error {
@ -120,14 +82,8 @@ func (ni *NoteIndex) Add(note core.Note) (id core.NoteID, err error) {
if err != nil {
return err
}
note.ID = id
err = ni.addLinks(dao, id, note.Links)
if err != nil {
return err
}
err = ni.fixExistingLinks(dao, note.ID, note.Path)
err = ni.addLinks(dao, id, note)
if err != nil {
return err
}
@ -139,84 +95,6 @@ func (ni *NoteIndex) Add(note core.Note) (id core.NoteID, err error) {
return
}
// fixExistingLinks will go over all indexed links and update their target to
// the given id if they match the given path better than their current
// targetPath.
func (ni *NoteIndex) fixExistingLinks(dao *dao, id core.NoteID, path string) error {
links, err := dao.links.FindInternal()
if err != nil {
return err
}
for _, link := range links {
// To find the best match possible, shortest paths take precedence.
// See https://github.com/zk-org/zk/issues/23
if link.TargetPath != "" && len(link.TargetPath) < len(path) {
continue
}
if matches, err := ni.linkMatchesPath(link, path); matches && err == nil {
err = dao.links.SetTargetID(link.ID, id)
}
if err != nil {
return err
}
}
return nil
}
// linkMatchesPath returns whether the given link can be used to reach the
// given note path.
func (ni *NoteIndex) linkMatchesPath(link core.ResolvedLink, path string) (bool, error) {
// Remove any anchor at the end of the HREF, since it's most likely
// matching a sub-section in the note.
href := strings.SplitN(link.Href, "#", 2)[0]
matchString := func(pattern string, s string) bool {
reg := regexp.MustCompile(pattern)
return reg.MatchString(s)
}
matches := func(href string, allowPartialHref bool) bool {
if href == "" {
return false
}
href = regexp.QuoteMeta(href)
if allowPartialHref {
if matchString("^(.*/)?[^/]*"+href+"[^/]*$", path) {
return true
}
if matchString(".*"+href+".*", path) {
return true
}
}
return matchString("^(?:"+href+"[^/]*|"+href+"/.+)$", path)
}
baseDir := filepath.Join(ni.notebookPath, filepath.Dir(link.SourcePath))
if relHref, err := ni.relNotebookPath(baseDir, href); err != nil {
if matches(relHref, false) {
return true, nil
}
}
allowPartialMatch := (link.Type == core.LinkTypeWikiLink)
return matches(href, allowPartialMatch), nil
}
// relNotebookHref makes the given href (which is relative to baseDir) relative
// to the notebook root instead.
func (ni *NoteIndex) relNotebookPath(baseDir string, href string) (string, error) {
path := filepath.Clean(filepath.Join(baseDir, href))
path, err := filepath.Rel(ni.notebookPath, path)
return path,
errors.Wrapf(err, "failed to make href relative to the notebook: %s", href)
}
// Update implements core.NoteIndex.
func (ni *NoteIndex) Update(note core.Note) error {
err := ni.commit(func(dao *dao) error {
@ -230,7 +108,7 @@ func (ni *NoteIndex) Update(note core.Note) error {
if err != nil {
return err
}
err = ni.addLinks(dao, id, note.Links)
err = ni.addLinks(dao, id, note)
if err != nil {
return err
}
@ -261,19 +139,26 @@ func (ni *NoteIndex) associateTags(collections *CollectionDAO, noteId core.NoteI
return nil
}
func (ni *NoteIndex) addLinks(dao *dao, id core.NoteID, links []core.Link) error {
resolvedLinks, err := ni.resolveLinkNoteIDs(dao, id, links)
func (ni *NoteIndex) addLinks(dao *dao, id core.NoteID, note core.Note) error {
links, err := ni.resolveLinkNoteIDs(dao, id, note.Links)
if err != nil {
return err
}
return dao.links.Add(resolvedLinks)
err = dao.links.Add(links)
if err != nil {
return err
}
return dao.links.SetTargetID(note.Path, id)
}
func (ni *NoteIndex) resolveLinkNoteIDs(dao *dao, sourceID core.NoteID, links []core.Link) ([]core.ResolvedLink, error) {
resolvedLinks := []core.ResolvedLink{}
for _, link := range links {
targetID, err := ni.findLinkMatch(dao, "" /* base dir */, link.Href, link.Type)
allowPartialMatch := (link.Type == core.LinkTypeWikiLink)
targetID, err := dao.notes.FindIdByHref(link.Href, allowPartialMatch)
if err != nil {
return resolvedLinks, err
}

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
// FIXME: Missing tests
@ -220,7 +220,7 @@ func TestNoteIndexUpdateWithTags(t *testing.T) {
func testNoteIndex(t *testing.T) (*DB, *NoteIndex) {
db := testDB(t)
return db, NewNoteIndex("", db, &util.NullLogger)
return db, NewNoteIndex(db, &util.NullLogger)
}
func assertTagExistsOrNot(t *testing.T, db *DB, shouldExist bool, tag string) {

@ -4,7 +4,7 @@ import (
"database/sql"
"sync"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/errors"
)
// LazyStmt is a wrapper around a sql.Stmt which will be evaluated on first use.

@ -1,4 +1,4 @@
# See https://github.com/zk-org/zk/issues/23
# See https://github.com/mickael-menu/zk/issues/23
- id: 1
path: "prefix-longest.md"
sortable_path: "prefix-longest.md"

@ -4,8 +4,8 @@ import (
"testing"
"github.com/go-testfixtures/testfixtures/v3"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
// testDB is an utility function to create a database loaded with the default fixtures.

@ -6,8 +6,8 @@ import (
"strconv"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
)
type RowScanner interface {
@ -30,14 +30,6 @@ func escapeLikeTerm(term string, escapeChar rune) string {
return escape(escape(escape(term, string(escapeChar)), "%"), "_")
}
func linkIDToSQL(id core.LinkID) sql.NullInt64 {
if id.IsValid() {
return sql.NullInt64{Int64: int64(id), Valid: true}
} else {
return sql.NullInt64{}
}
}
func noteIDToSQL(id core.NoteID) sql.NullInt64 {
if id.IsValid() {
return sql.NullInt64{Int64: int64(id), Valid: true}

@ -3,7 +3,7 @@ package sqlite
import (
"testing"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestEscapeLikeTerm(t *testing.T) {

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/fatih/color"
"github.com/zk-org/zk/internal/core"
"github.com/mickael-menu/zk/internal/core"
)
// Style implements core.Styler using ANSI escape codes to be used with a terminal.

@ -4,8 +4,8 @@ import (
"testing"
"github.com/fatih/color"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func createTerminal() *Terminal {

@ -5,10 +5,10 @@ import (
"os"
"path/filepath"
"github.com/zk-org/zk/internal/adapter/fzf"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/adapter/fzf"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
)
// Edit opens notes matching a set of criteria with the user editor.

@ -5,11 +5,11 @@ import (
"fmt"
"os"
"github.com/zk-org/zk/internal/adapter/fzf"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/adapter/fzf"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/strings"
)
// Graph produces a directed graph of the notes matching a set of criteria.

@ -2,13 +2,9 @@ package cmd
import (
"fmt"
"os"
"time"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/paths"
"github.com/schollz/progressbar/v3"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
)
// Index indexes the content of all the notes in the notebook.
@ -28,37 +24,10 @@ func (cmd *Index) Run(container *cli.Container) error {
return err
}
return cmd.RunWithNotebook(container, notebook)
}
func (cmd *Index) RunWithNotebook(container *cli.Container, notebook *core.Notebook) error {
showProgress := container.Terminal.IsInteractive()
var bar *progressbar.ProgressBar
if showProgress {
bar = progressbar.NewOptions(-1,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSpinnerType(14),
)
}
opts := core.NoteIndexOpts{
stats, err := notebook.Index(core.NoteIndexOpts{
Force: cmd.Force,
Verbose: cmd.Verbose,
}
stats, err := notebook.IndexWithCallback(opts, func(change paths.DiffChange) {
if showProgress {
bar.Add(1)
bar.Describe(change.String())
}
})
if showProgress {
bar.Clear()
}
if err != nil {
return err
}

@ -6,9 +6,9 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/strings"
)
// Init creates a notebook in the given directory
@ -32,8 +32,7 @@ func (cmd *Init) Run(container *cli.Container) error {
return err
}
index := Index{Quiet: true}
err = index.RunWithNotebook(container, notebook)
_, err = notebook.Index(core.NoteIndexOpts{})
if err != nil {
return err
}

@ -5,10 +5,10 @@ import (
"io"
"os"
"github.com/zk-org/zk/internal/adapter/fzf"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/adapter/fzf"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/strings"
)
// List displays notes matching a set of criteria.
@ -155,26 +155,26 @@ var defaultNoteFormats = map[string]string{
"path": `{{path}}`,
"link": `{{link}}`,
"oneline": `{{style "title" title}} {{style "path" path}} ({{format-date created "elapsed"}})`,
"oneline": `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})`,
"short": `{{style "title" title}} {{style "path" path}} ({{format-date created "elapsed"}})
"short": `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
{{list snippets}}`,
"medium": `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Created: {{date created "short"}}
{{list snippets}}`,
"long": `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Modified: {{format-date modified "short"}}
Created: {{date created "short"}}
Modified: {{date modified "short"}}
{{list snippets}}`,
"full": `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Modified: {{format-date modified "short"}}
Created: {{date created "short"}}
Modified: {{date modified "short"}}
Tags: {{join tags ", "}}
{{prepend " " body}}

@ -3,12 +3,12 @@ package cmd
import (
"testing"
"github.com/zk-org/zk/internal/util/test/assert"
"github.com/mickael-menu/zk/internal/util/test/assert"
)
func TestListFormatDefault(t *testing.T) {
cmd := List{}
assert.Equal(t, cmd.noteTemplate(), `{{style "title" title}} {{style "path" path}} ({{format-date created "elapsed"}})
assert.Equal(t, cmd.noteTemplate(), `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
{{list snippets}}`)
}
@ -25,26 +25,26 @@ func TestListFormatPredefined(t *testing.T) {
test("path", `{{path}}`)
test("link", `{{link}}`)
test("oneline", `{{style "title" title}} {{style "path" path}} ({{format-date created "elapsed"}})`)
test("oneline", `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})`)
test("short", `{{style "title" title}} {{style "path" path}} ({{format-date created "elapsed"}})
test("short", `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
{{list snippets}}`)
test("medium", `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Created: {{date created "short"}}
{{list snippets}}`)
test("long", `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Modified: {{format-date modified "short"}}
Created: {{date created "short"}}
Modified: {{date modified "short"}}
{{list snippets}}`)
test("full", `{{style "title" title}} {{style "path" path}}
Created: {{format-date created "short"}}
Modified: {{format-date modified "short"}}
Created: {{date created "short"}}
Modified: {{date modified "short"}}
Tags: {{join tags ", "}}
{{prepend " " body}}

@ -1,9 +1,9 @@
package cmd
import (
"github.com/zk-org/zk/internal/adapter/lsp"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/adapter/lsp"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/util/opt"
)
// LSP starts a server implementing the Language Server Protocol.

@ -3,29 +3,28 @@ package cmd
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/zk-org/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
dateutil "github.com/mickael-menu/zk/internal/util/date"
"github.com/mickael-menu/zk/internal/util/opt"
osutil "github.com/mickael-menu/zk/internal/util/os"
)
// New adds a new note to the notebook.
type New struct {
Directory string `arg optional default:"." help:"Directory in which to create the note."`
Interactive bool `short:i help:"Read contents from standard input."`
Title string `short:t placeholder:TITLE help:"Title of the new note."`
Date string ` placeholder:DATE help:"Set the current date."`
Group string `short:g placeholder:NAME help:"Name of the config group this note belongs to. Takes precedence over the config of the directory."`
Extra map[string]string ` help:"Extra variables passed to the templates." mapsep:","`
Template string ` placeholder:PATH help:"Custom template used to render the note."`
PrintPath bool `short:p help:"Print the path of the created note instead of editing it."`
DryRun bool `short:n help:"Don't actually create the note. Instead, prints its content on stdout and the generated path on stderr."`
ID string ` placeholder:ID help:"Skip id generation and use provided value."`
Directory string `arg optional default:"." help:"Directory in which to create the note."`
Title string `short:t placeholder:TITLE help:"Title of the new note."`
Date string ` placeholder:DATE help:"Set the current date."`
Group string `short:g placeholder:NAME help:"Name of the config group this note belongs to. Takes precedence over the config of the directory."`
Extra map[string]string ` help:"Extra variables passed to the templates." mapsep:","`
Template string ` placeholder:PATH help:"Custom template used to render the note."`
PrintPath bool `short:p help:"Print the path of the created note instead of editing it."`
DryRun bool `short:n help:"Don't actually create the note. Instead, prints its content on stdout and the generated path on stderr."`
ID string ` placeholder:ID help:"Skip id generation and use provided value."`
}
func (cmd *New) Run(container *cli.Container) error {
@ -34,12 +33,9 @@ func (cmd *New) Run(container *cli.Container) error {
return err
}
var content []byte
if cmd.Interactive {
content, err = io.ReadAll(os.Stdin)
if err != nil {
return err
}
content, err := osutil.ReadStdinPipe()
if err != nil {
return err
}
date := time.Now()
@ -52,7 +48,7 @@ func (cmd *New) Run(container *cli.Container) error {
note, err := notebook.NewNote(core.NewNoteOpts{
Title: opt.NewNotEmptyString(cmd.Title),
Content: string(content),
Content: content.Unwrap(),
Directory: opt.NewNotEmptyString(cmd.Directory),
Group: opt.NewNotEmptyString(cmd.Group),
Template: opt.NewNotEmptyString(cmd.Template),

@ -5,10 +5,10 @@ import (
"io"
"os"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/strings"
)
// Tag manages the note tags in the notebook.

@ -4,23 +4,22 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/zk-org/zk/internal/adapter/editor"
"github.com/zk-org/zk/internal/adapter/fs"
"github.com/zk-org/zk/internal/adapter/fzf"
"github.com/zk-org/zk/internal/adapter/handlebars"
hbhelpers "github.com/zk-org/zk/internal/adapter/handlebars/helpers"
"github.com/zk-org/zk/internal/adapter/markdown"
"github.com/zk-org/zk/internal/adapter/sqlite"
"github.com/zk-org/zk/internal/adapter/term"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
osutil "github.com/zk-org/zk/internal/util/os"
"github.com/zk-org/zk/internal/util/pager"
"github.com/zk-org/zk/internal/util/paths"
"github.com/zk-org/zk/internal/util/rand"
"github.com/mickael-menu/zk/internal/adapter/editor"
"github.com/mickael-menu/zk/internal/adapter/fs"
"github.com/mickael-menu/zk/internal/adapter/fzf"
"github.com/mickael-menu/zk/internal/adapter/handlebars"
hbhelpers "github.com/mickael-menu/zk/internal/adapter/handlebars/helpers"
"github.com/mickael-menu/zk/internal/adapter/markdown"
"github.com/mickael-menu/zk/internal/adapter/sqlite"
"github.com/mickael-menu/zk/internal/adapter/term"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
osutil "github.com/mickael-menu/zk/internal/util/os"
"github.com/mickael-menu/zk/internal/util/pager"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/util/rand"
)
type Dirs struct {
@ -66,32 +65,12 @@ func NewContainer(version string) (*Container, error) {
return nil, wrap(err)
}
if configPath != "" {
config, err = core.OpenConfig(configPath, config, fs, true)
config, err = core.OpenConfig(configPath, config, fs)
if err != nil {
return nil, wrap(err)
}
}
// Set the default notebook if not already set
// might be overrided if --notebook-dir flag is present
if osutil.GetOptEnv("ZK_NOTEBOOK_DIR").IsNull() && !config.Notebook.Dir.IsNull() {
// Expand in case there are environment variables on the path
notebookDir := os.Expand(config.Notebook.Dir.Unwrap(), os.Getenv)
if strings.HasPrefix(notebookDir, "~") {
dirname, err := os.UserHomeDir()
if err != nil {
return nil, wrap(err)
}
notebookDir = filepath.Join(dirname, notebookDir[1:])
}
os.Setenv("ZK_NOTEBOOK_DIR", notebookDir)
}
// Set the default shell if not already set
if osutil.GetOptEnv("ZK_SHELL").IsNull() && !config.Tool.Shell.IsEmpty() {
os.Setenv("ZK_SHELL", config.Tool.Shell.Unwrap())
}
return &Container{
Version: version,
Config: config,
@ -111,7 +90,7 @@ func NewContainer(version string) (*Container, error) {
}
notebook := core.NewNotebook(path, config, core.NotebookPorts{
NoteIndex: sqlite.NewNoteIndex(path, db, logger),
NoteIndex: sqlite.NewNoteIndex(db, logger),
NoteContentParser: markdown.NewParser(
markdown.ParserOpts{
HashtagEnabled: config.Format.Markdown.Hashtags,
@ -160,10 +139,6 @@ func NewContainer(version string) (*Container, error) {
// XDG Base Directory specification
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
func locateGlobalConfig() (string, error) {
if _, ok := os.LookupEnv("RUNNING_TESH"); ok {
return "", nil
}
configPath := filepath.Join(globalConfigDir(), "config.toml")
exists, err := paths.Exists(configPath)
switch {

@ -6,10 +6,11 @@ import (
"github.com/alecthomas/kong"
"github.com/kballard/go-shellquote"
"github.com/zk-org/zk/internal/core"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/core"
dateutil "github.com/mickael-menu/zk/internal/util/date"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/strings"
)
// Filtering holds filtering options to select notes.
@ -18,8 +19,8 @@ type Filtering struct {
Interactive bool `kong:"group='filter',short='i',help='Select notes interactively with fzf.'" json:"-"`
Limit int `kong:"group='filter',short='n',placeholder='COUNT',help='Limit the number of notes found.'" json:"limit"`
Match []string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"`
MatchStrategy string `kong:"group='filter',short='M',default='fts',placeholder='STRATEGY',help='Text matching strategy among: fts, re, exact.'" json:"matchStrategy"`
Match string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"`
ExactMatch bool `kong:"group='filter',short='e',help='Search for exact occurrences of the --match argument (case insensitive).'" json:"exactMatch"`
Exclude []string `kong:"group='filter',short='x',placeholder='PATH',help='Ignore notes matching the given path, including its descendants.'" json:"excludeHrefs"`
Tag []string `kong:"group='filter',short='t',help='Find notes tagged with the given tags.'" json:"tags"`
Mention []string `kong:"group='filter',placeholder='PATH',help='Find notes mentioning the title of the given ones.'" json:"mention"`
@ -40,9 +41,6 @@ type Filtering struct {
ModifiedAfter string `kong:"group='filter',placeholder='DATE',help='Find notes modified after the given date.'" json:"modifiedAfter"`
Sort []string `kong:"group='sort',short='s',placeholder='TERM',help='Order the notes by the given criterion.'" json:"sort"`
// Deprecated
ExactMatch bool `kong:"hidden,short='e'" json:"exactMatch"`
}
// ExpandNamedFilters expands recursively any named filter found in the Path field.
@ -116,9 +114,10 @@ func (f Filtering) ExpandNamedFilters(filters map[string]string, expandedFilters
f.ModifiedAfter = parsedFilter.ModifiedAfter
}
f.Match = append(f.Match, parsedFilter.Match...)
if f.MatchStrategy == "" {
f.MatchStrategy = parsedFilter.MatchStrategy
if f.Match == "" {
f.Match = parsedFilter.Match
} else if parsedFilter.Match != "" {
f.Match = fmt.Sprintf("(%s) AND (%s)", f.Match, parsedFilter.Match)
}
} else {
@ -139,16 +138,8 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
return opts, err
}
if f.ExactMatch {
return opts, fmt.Errorf("the --exact-match (-e) option is deprecated, use --match-strategy=exact (-Me) instead")
}
opts.Match = make([]string, len(f.Match))
copy(opts.Match, f.Match)
opts.MatchStrategy, err = core.MatchStrategyFromString(f.MatchStrategy)
if err != nil {
return opts, err
}
opts.Match = opt.NewNotEmptyString(f.Match)
opts.ExactMatch = f.ExactMatch
if paths, ok := relPaths(notebook, f.Path); ok {
opts.IncludeHrefs = paths
@ -280,9 +271,7 @@ func parseDayRange(date string) (start time.Time, end time.Time, err error) {
return
}
// we add -1 second so that the day range ends at 23:59:59
// i.e, the 'new day' begins at 00:00:00
start = startOfDay(day).Add(time.Second * -1)
start = startOfDay(day)
end = start.AddDate(0, 0, 1)
return start, end, nil
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save