Compare commits

...

103 Commits
v0.9.0 ... main

Author SHA1 Message Date
Sören Tempel 00a4361d20
Determine fixture path using the current working directory (#422) 2 weeks ago
Tillman Jex 578894f45b
release zk v0.14.1 (#417) 1 month ago
Tillman Jex e21519a1b4
readme: exit maintenance mode, add logo, contributing (#416) 1 month ago
Tillman Jex cfa74a7f0c
update Makefile (go 1.21 and alpine), update contributing.md (#412) 1 month ago
Tillman Jex 8e2add0606
raise sqlite version for musl build compatibility (#414) 1 month ago
guangwu bf5cad60dc
chore: pkg imported more than once (#410)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
Co-authored-by: tjex <tjex@tjex.net>
1 month ago
Alexis Praga c3f26ca12b
Doc: how to edit today's daily note (#407)
documentation daily note tutorial extra detail
2 months ago
Tillman Jex 0973f9929d
allow notebook as hidden dir (#402) 2 months ago
dependabot[bot] 45b68121ce
Bump actions/configure-pages from 4 to 5 (#398)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
3 months ago
Tadeas Uhlir c9d4734d3c
fix(lsp): ignore diagnostic check within code blocks (#399)
Co-authored-by: tjex <tjex@tjex.net>
3 months ago
Tadeas Uhlir 05c50a70d5
fix(lsp): fix trigger completion of zk LSP (#397) 3 months ago
Tillman Jex b10d51dbc2
accept tripple dash file URIs as valid links (#391)
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>
3 months ago
Tillman Jex 56f4e650ea
git: ignore delve debug files (#396) 3 months ago
Sumit Sahrawat 6a06ded3c4
Fix hyperlink from `README.md` to `docs/getting-started.md` (#395) 3 months ago
Kyle Huggins 1471c6be2c
Update documentation to restore GitHub Pages functionality (#387) 5 months ago
Tillman Jex d4e542b70c
tesh test case for yaml dates without time stamp (#385) 5 months ago
Tillman Jex 537c7f7554
fix day range parsing (zk-org/zk#382) (#384) 5 months ago
Michael McDonagh 55d5487d24
Fix broken links (#381) 6 months ago
dependabot[bot] f7079be6c0
Bump actions/stale from 7 to 9 (#365)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
6 months ago
dependabot[bot] a1f2a701af
Bump github/codeql-action from 2 to 3 (#367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
6 months ago
dependabot[bot] 70a9afb9f5
Bump actions/setup-go from 4 to 5 (#363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
Michael McDonagh 0eaf26483f
Remove references to old repo (#373)
Finalise transfer from old repo (github.com/mickael-menu/zk) to new (github.com/zk-org/zk)

Co-authored-by: tjex <tjex@tjex.net>
6 months ago
Julio Lopez 5a2333d0af
test: add TestFormatDateHelperElapsed (#359)
improve and extend test coverage in handlebars_test.go, which deals with time and date formatting.
6 months ago
Mickaël Menu e3e52dfe69
Add `CONTRIBUTING.md` (#378) 6 months ago
Julio Lopez 87f3680a9b
chore(ci): use Go 1.21 (#360)
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>
6 months ago
dependabot[bot] f120f9546c
Bump golang.org/x/crypto from 0.0.0-20220525230936-793ad666bf5e to 0.17.0 (#368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>

Ran tests against the PR, all pass.
6 months ago
Tillman Jex 5bb1c0ba89
Maintenance mode callout fix (#377) 6 months ago
Tillman Jex 1e28fe4b92
notification of maintenance mode (#376)
update readme to communicate maintenance mode due to project handover
6 months ago
Michael McDonagh 50fb638dd9
Update the arch repo link (#372) 6 months ago
Michael McDonagh 7ba9df6526
Relative output needs relative input (#374) 6 months ago
Julio Lopez be1249d7a6
Use 'go-version-file' option in build workflow (#355) 8 months ago
Julio Lopez 228c96fcea
Address io/ioutil deprecation (#354) 8 months ago
dependabot[bot] 1fd1298c00
Bump actions/checkout from 3 to 4 (#344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
khimaros 0b4db9ade6
Fix parsing large notes (#339) 11 months ago
Mickaël Menu 072fae2f6c
Fix `{{date}}` helper test (#324) 1 year ago
Mickaël Menu b71a74eabc Release zk 0.14.0 1 year ago
Mickaël Menu 1ec777de7e Update documentation of config alias 1 year ago
Mickaël Menu 75205fe099
Rename `note.ignore` config property to `note.exclude` (#322) 1 year ago
wrvsrx 6252e51595
Fix LSP positions using UTF-16 offsets (#317) 1 year ago
Leonardo Mello e26ac5133e
Add `notebook` configuration to set default notebook path (#304) 1 year ago
Leonardo Mello a8d1db4c57
Add `tool.shell` configuration key (#302) 1 year ago
dependabot[bot] ba611e1c1e
Bump actions/setup-go from 3 to 4 (#299) 1 year ago
Mickaël Menu 39bcb8069f
Bump go-sqlite3 (#296) 1 year ago
dependabot[bot] 89ff022593
Bump golang.org/x/text from 0.3.7 to 0.3.8 (#292) 1 year ago
Mickaël Menu ea4457ad67 Release zk 0.13.0 1 year ago
Cyril Dutrieux 6401a4e1f6
Add "title" to the Command in code actions (#288) 1 year ago
Patrick Anker f7d4db07d6
Add `zk.link` LSP command (#284) 1 year ago
Leiser Fernández Gallo 150c82fb22
Fix emanote URL (#283) 1 year ago
Matthias Vogelgesang 279f9ef3cd
Add `zk-spaced` to list of related projects (#281) 1 year ago
dependabot[bot] 6506198743
Bump actions/stale from 6 to 7 (#277) 2 years ago
Mickaël Menu ae3a86dbfa
Add new options for LSP command `zk.new` (#276) 2 years ago
Mickaël Menu d79da8933a Release zk 0.12.0 2 years ago
Mickaël Menu 15d4cfc921
Rename the `{{date}}` helper to `{{format-date}}` (#274) 2 years ago
Oliver Marriott 142b636342
Support multiple `--match` flags (#268) 2 years ago
Oliver Marriott 9d88245102
Fix `--working-dir=` and `--notebook-dir=` options parsing (#267) 2 years ago
Zach Leslie 404ef9d6f5
Add the `get-date` template helper (#262) 2 years ago
Mickaël Menu c21c4fc21f
LSP: Add support for external URLs with `documentLink` (#261) 2 years ago
dependabot[bot] cdf4f8e0c1
Bump actions/stale from 5 to 6 (#258) 2 years ago
Mickaël Menu 1745097256
Fix error when pairing `--link-to` and `--linked-by` (#255) 2 years ago
Sridhar Ratnakumar 9faec3628a
neuron.md: Add link to docs for Emanote (#247) 2 years ago
Kristof Lünenschloß 814a5d7c07
Link to mickael-menu/zk-nvim instead of megalithic/zk.nvim (#249) 2 years ago
bibor ab1d8fd0bd
LSP: Fix double use of `notebook.RelPath` (#246) 2 years ago
Mickaël Menu 61b9c0f5d7
LSP: Fix finding backlink references for notes in a folder (#245) 2 years ago
Mickaël Menu 78c40eeecf Release zk 0.11.1 2 years ago
Pete Kazmier a6e522562e
Add explicit flag to read from standard input (#242) 2 years ago
Mickaël Menu eefc3be9c6 Release zk 0.11.0 2 years ago
Mickaël Menu c06375ee3a
Fix updating links after creating a new note (#237) 2 years ago
Mickaël Menu 8bfafe5dab
Fix LSP link recognition with unicode (#238) 2 years ago
Mickaël Menu 53aabce1ed
Support standard input via shell redirection with `zk new` (#240) 2 years ago
Mickaël Menu aaf6c42bd8
Hide index progress in non-interactive shells (#234) 2 years ago
Mickaël Menu 68e6b70eae
Upgrade to Go 1.18 and update dependencies (#221) 2 years ago
Mickaël Menu 4b76fbadf1
Support regular expressions with `--match` (#222) 2 years ago
Mickaël Menu 1a05a04432
LSP: Deprecate title matching for wiki links (#218) 2 years ago
Mickaël Menu dbd791f672 Release zk 0.10.1 2 years ago
Mickaël Menu c356b7bd00
Update existing links when adding a new note (#219) 2 years ago
Mickaël Menu 3c634fb00a
Remove dependency on `libicu` (#213) 2 years ago
dependabot[bot] 1167cb99ae
Bump GitHub workflow actions (#210) 2 years ago
Arto Jonsson d9be3c04a6
Add Dependabot configuration (#206) 2 years ago
Arto Jonsson bfa065d1e3
Enable GitHub code scanning (#207) 2 years ago
Mickaël Menu 60e9491c1b Fix running the Homebrew workflow (#204) 2 years ago
Sridhar Ratnakumar 94f563cd51
Add Nix install instructions (#198) 2 years ago
Mickaël Menu d8c9031f9c
Release zk 0.10.0 2 years ago
Mickaël Menu 7622d9ee19
Don't parse Markdown table headers a colon tags (#196) 2 years ago
Nelyah 94e8a0d437
Customize `fzf` options and key bindings (#154) 2 years ago
Mickaël Menu 525047fab9
Add support for double star globbing in `note.ignore` config option (#195) 2 years ago
Mickaël Menu c237b4d57d
Fix broken wiki links in subdirectories (#193) 2 years ago
Demaro Stanberry 0bfab74eac
Fix file url parsing on Windows (#186) 2 years ago
Stephen Bolton 9e33d679e2
Add `--id` flag to `zk new` (#183) 2 years ago
codito a0ced3c330
Fix LSP path autocompletion on Windows (#175) 2 years ago
codito 83c15cc927
Fix Windows build (#171) 2 years ago
Mickaël Menu e037befdf1
Add support for `ZK_SHELL` (#166) 2 years ago
Mickaël Menu c6e529fdfa Add a triage GitHub workflow 2 years ago
Mickaël Menu c429517c6b
Fix incorrect timezone for natural dates (#156) 2 years ago
Mickaël Menu 7b92ca06cc
Disable `additionalTextEdits` for completion items by default (#160) 2 years ago
Mickaël Menu a4b31b4794
Bump the Homebrew formula on GitHub releases (#157) 2 years ago
Mitchell Hanberg 73dfc83aeb
Point Homebrew install instructions to homebrew-core (#153) 2 years ago
Mickaël Menu da6e842406
Run `tesh` with the GitHub checks (#148) 2 years ago
Mickaël Menu eb095e5fc0
Add `tesh` test suite (#147) 2 years ago
Mickaël Menu 6ccbbe8613
LSP auto-completion of YAML frontmatter tags (#146) 2 years ago
Mickaël Menu 04a157f3be
Clarify the Homebrew instructions (#145) 3 years ago
Karim Abou Zeid 63dee0ce3f
Add install instructions for Arch Linux. (#140) 3 years ago
Mickaël Menu 8fe0671b1d
Mention Homebrew tap and zk-nvim plugin
cc @mhanberg
3 years ago
Mickaël Menu eddc4b5845
Fix embedded image links shown as not found (#127) 3 years ago

@ -0,0 +1,19 @@
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.

@ -0,0 +1,14 @@
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

@ -0,0 +1,65 @@
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

@ -0,0 +1,10 @@
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.

@ -0,0 +1,13 @@
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.

@ -0,0 +1,16 @@
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,18 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.16
go-version-file: 'go.mod'
- name: Install dependencies
run: |
go install github.com/mickael-menu/tesh@latest
- name: Build
run: make build
@ -26,3 +30,8 @@ jobs:
- name: Test
run: make test
- name: Tesh
# See https://github.com/actions/runner/issues/241#issuecomment-924327172
shell: script --return --quiet --command "bash {0}"
run: make tesh

@ -0,0 +1,40 @@
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

@ -0,0 +1,43 @@
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

@ -0,0 +1,19 @@
name: Release
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
homebrew:
runs-on: macos-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.HOMEBREW_GITHUB_TOKEN}}
formula: zk

@ -0,0 +1,31 @@
name: Triage
on:
push:
paths:
- .github/workflows/triage.yml
schedule:
# Once every day at midnight UTC
- cron: "0 0 * * *"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
days-before-issue-close: 5
stale-issue-label: 'stale'
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs in the next 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
exempt-issue-labels: 'help wanted,feature request,enhancement,bug'
days-before-pr-close: -1
stale-pr-label: 'stale'
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity.'

9
.gitignore vendored

@ -11,7 +11,14 @@
# 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/
.zk
# IDEs/Editors
.vscode/
notebook.db
zk

@ -2,7 +2,146 @@
All notable changes to this project will be documented in this file.
<!-- ## Unreleased -->
## 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.
## 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.
* 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)).
* [#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.
### Changed
* The default `fzf` key binding to create a new note with `zk edit --interactive` was changed to `Ctrl-E`, to avoid conflict with the default `Ctrl-N` binding.
### 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.
## 0.9.0
@ -15,10 +154,10 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#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`).
* [#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`).
## 0.8.0
@ -43,9 +182,9 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#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.
* [#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.
## 0.7.0
@ -67,7 +206,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/mickael-menu/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/zk-org/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`
@ -75,9 +214,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/mickael-menu/zk/issues/78) Do not exclude notes containing broken links from the index.
* [#78](https://github.com/zk-org/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/mickael-menu/zk/issues/86) Index encoded Markdown links.
* [#86](https://github.com/zk-org/zk/issues/86) Index encoded Markdown links.
## 0.6.0
@ -91,7 +230,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/mickael-menu/zk/pull/58)).
* 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)).
* New [`note.ignore`](docs/config-note.md) configuration option to ignore files matching the given path globs when indexing notes.
```yaml
[note]
@ -103,7 +242,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#16](https://github.com/mickael-menu/zk/issues/16) Links with section anchors, e.g. `[[filename#section]]`.
* [#16](https://github.com/zk-org/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.
@ -111,7 +250,7 @@ All notable changes to this project will be documented in this file.
### Added
* [Editor integration through LSP](https://github.com/mickael-menu/zk/issues/22):
* [Editor integration through LSP](https://github.com/zk-org/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).
@ -130,7 +269,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/mickael-menu/zk/pull/39)).
* Fix completion with Neovim's built-in LSP client (contributed by [@cormacrelf](https://github.com/zk-org/zk/pull/39)).
## 0.4.0
@ -143,7 +282,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/mickael-menu/zk/issues/22)
* [And more to come...](https://github.com/zk-org/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]]`.
@ -188,7 +327,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#4](https://github.com/mickael-menu/zk/issues/4) Terminal borked when piping content with Vim
* [#4](https://github.com/zk-org/zk/issues/4) Terminal borked when piping content with Vim
## 0.2.1

@ -0,0 +1,62 @@
# 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.

@ -10,6 +10,21 @@ install:
test:
$(call go,test,./...)
# Run end-to-end tests.
tesh: build
@PATH=".:$(shell pwd):$(PATH)" tesh tests tests/fixtures
# Run end-to-end tests and prints difference as raw bytes.
teshb: build
@PATH=".:$(shell pwd):$(PATH)" tesh -b tests tests/fixtures
# Update end-to-end tests.
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
@ -19,10 +34,31 @@ dist-macos:
rm -f zk && make && zip -r "zk-${VERSION}-macos-`uname -m`.zip" zk
# Produce a release bundle for Linux.
dist-linux:
rm -f zk && docker run --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 --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 --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
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
# Clean build products.
clean:
@ -35,11 +71,16 @@ 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 CGO_CFLAGS="-I/opt/homebrew/opt/icu4c/include" CGO_LDFLAGS="-L/opt/homebrew/opt/icu4c/lib"
ENV_PREFIX := $(ENV) GOARCH=arm64
endif
# Wrapper around the go binary, to set all the default parameters.
define go
$(ENV_PREFIX) go $(1) -tags "fts5 icu" -ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
$(ENV_PREFIX) go $(1) -tags "fts5" -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,10 +1,12 @@
<div align="center">
<h1>zk</h1>
<img alt="zk logo" width="20%" src="./docs/assets/media/zk-black-modern.png" />
<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).
@ -15,7 +17,8 @@
* [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-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* [`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
* (*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)
@ -36,38 +39,62 @@
## Install
[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).
### Build from scratch
[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).
Make sure you have a working [Go installation](https://golang.org/), then clone the repository:
### Homebrew
```sh
$ git clone https://github.com/mickael-menu/zk.git
$ cd zk
brew install zk
```
#### On macOS
Or, if you want to the latest changes:
```sh
brew install --HEAD zk
```
`icu4c` is required to build `zk`, which you can install with [Homebrew](https://brew.sh/).
### Nix
```sh
# Run zk from Nix store without installing it:
nix run nixpkgs#zk
# Or, to install it permanently:
nix-env -iA zk
```
$ brew install icu4c
$ make
$ ./zk -h
### Arch Linux
You can install [the zk package](https://archlinux.org/packages/extra/x86_64/zk/) from the official repos.
```sh
sudo pacman -S zk
```
#### On Linux
### Build from scratch
Make sure you have a working [Go 1.21+ installation](https://golang.org/), then clone the repository:
```sh
$ git clone https://github.com/zk-org/zk.git
$ cd zk
```
`libicu-dev` is required to build `zk`, use your favorite package manager to install it.
#### On macOS / Linux
```
$ apt-install libicu-dev
$ make
$ ./zk -h
```
## Contributing
We warmly welcome issues, PRs and [discussions](https://github.com/zk-org/zk/discussions).
Here you can read [some useful info for contributing to `zk`](./CONTRIBUTING.md).
## 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

@ -0,0 +1,24 @@
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/"

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

@ -1,57 +0,0 @@
<!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>

@ -1,9 +0,0 @@
---
---
@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: 60 KiB

After

Width:  |  Height:  |  Size: 60 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: 21 KiB

After

Width:  |  Height:  |  Size: 21 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: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

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/mickael-menu/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/zk-org/zk/issues).

@ -6,9 +6,10 @@ 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 `$SHELL -c`, which allows you to:
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:
* 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 `|`
@ -17,7 +18,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 = "{{date now}}"
filename = "{{format-date now}}"
template = "journal.md"
[group.journal.extra]

@ -7,10 +7,11 @@ The `[lsp]` [configuration file](config.md) section provides settings to fine-tu
Customize how completion items appear in your editor when auto-completing links with the `[lsp.completion]` sub-section.
| Setting | Type | Description |
|--------------------|------------|----------------------------------------------------------------------------|
|-----------------------------|------------|---------------------------------------------------------------------------------------|
| `note-label` | `template` | Label displayed in the completion pop-up for each note |
| `note-filter-text` | `template` | Text used as a source when filtering the completion pop-up with keystrokes |
| `note-detail` | `template` | Additional information about a completion item |
| `use-additional-text-edits` | `boolean` | Indicates whether `additionalTextEdits` will be used to remove the trigger characters |
Each key accepts a [template](template.md) with the following context:

@ -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/`.
* `ignore` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) ignored during note indexing.
* `exclude` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) excluded 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.
* `{{date now 'timestamp'}}` e.g. `200911172034.md`
* `{{format-date now 'timestamp'}}` e.g. `200911172034.md`
* Verbose, but sortable by creation date and stable.
* `{{date now 'timestamp'}} {{title}}` e.g. `200911172034 An interesting concept.md`
* `{{format-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).
* `{{date now '%Y-%m-%d'}}` e.g. `2009-11-17.md`
* `{{format-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).

@ -0,0 +1,15 @@
# 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,12 +2,14 @@
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)
@ -25,6 +27,10 @@ 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]
@ -63,11 +69,11 @@ author = "Mickaël"
# GROUP OVERRIDES
[dir.journal]
[group.journal]
paths = ["journal/weekly", "journal/daily"]
[dir.journal.note]
filename = "{{date now}}"
[group.journal.note]
filename = "{{format-date now}}"
# MARKDOWN SETTINGS
@ -84,6 +90,9 @@ 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"

@ -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 `{{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 `{{format-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 {{date now}} instead.
filename = "{{date now '%Y-%m-%d'}}"
# %Y-%m-%d is actually the default format, so you could use {{format-date now}} instead.
filename = "{{format-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
# {{date now "long"}}
# {{format-date now "long"}}
What did I do today?
```
@ -43,3 +43,7 @@ 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/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
* [`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
## 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/mickael-menu/zk/issues/22)
* [And more to come...](https://github.com/zk-org/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/mickael-menu/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/zk-org/zk/issues/22).
#### Vim and Neovim
@ -151,7 +151,7 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
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 |
@ -160,9 +160,11 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
| `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 |
The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
1. The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
```json
{
@ -175,7 +177,22 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
```
</details>
`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file.
`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.list`
@ -189,8 +206,9 @@ This LSP command calls `zk list` to search a notebook. It takes two arguments:
| `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) |
| `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 |

@ -27,7 +27,7 @@ You can customize your experience using [custom templates](template.md) to gener
If you are not sure whether a note already exists for a particular subject, the "search or create" mode might be more appropriate than `zk new`. It is inspired by [Notational Velocity](https://notational.net/) and enables searching for an existing note or creating a new one in a single action.
From `zk`'s interactive edit screen, press `Ctrl-N` to create a new note using the current search query as title.
From `zk`'s interactive edit screen, press `Ctrl-E` to create a new note using the current search query as title.
<div align="center"><img alt="Create a note" width="85%" src="assets/media/new2.svg"/></div>

@ -27,3 +27,7 @@ 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.

@ -10,15 +10,21 @@ By default, `zk new` will start [your editor](tool-editor.md) after creating the
If you are not sure whether a note already exists for a particular subject, the "search or create" mode might be more appropriate than `zk new`. It is inspired by [Notational Velocity](https://notational.net/) and enables searching for an existing note or creating a new one in a single action.
This option is available when running `zk edit --interactive`, which spawns [`fzf`](tool-fzf.md) to filter selected notes. From `fzf`, press `Ctrl-N` to create a new note using the current search query as title.
This option is available when running `zk edit --interactive`, which spawns [`fzf`](tool-fzf.md) to filter selected notes. From `fzf`, press `Ctrl-E` to create a new note using the current search query as title.
## Create a note with initial content
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).
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).
For example, to use the content of the macOS clipboard as the initial content you can run:
```sh
$ pbpaste | zk new
$ pbpaste | zk new --interactive
```
Alternatively, you can use the content of a file:
```sh
$ zk new --interactive < file.txt
```

@ -45,11 +45,50 @@ $ 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 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 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`.
A syntax similar to Google Search is available for advanced search queries.
### Combining terms
```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
By default, the search engine will find the notes containing all the terms in the query, in any order.
@ -83,7 +122,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:`.
@ -92,7 +131,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 `*`.
@ -106,13 +145,25 @@ Prefixing a query with `^` will match notes whose title or body start with the f
"title: ^journal"
```
### Search for special characters
### Exact matches (`exact`)
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.
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.
```sh
$ zk list --match-strategy exact --match "[[link]]"
$ zk list -Me -m "[[link]]"
```
$ zk list --exact-match --match "[[link]]"
$ zk list -em "[[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 ".+@.+"
```
## Filter by tags

@ -6,6 +6,8 @@ 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 [`{{date now}}`](template.md) |
| `now` | date | Current date and time, useful when paired with [`{{format-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,15 +36,27 @@ 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 helper
### Date helpers
The `{{date}}` helper formats the given date for display.
#### 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.
Template contexts usually provide a `now` variable which can be used to print the current date.
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"}}`.
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"}}`.
| Format | Output | Notes |
|------------------|----------------------------|--------------------------------------------------|
@ -58,7 +70,7 @@ The default format output by `{{date <variable>}}` looks like `2009-11-17`, but
| `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. `{{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. `{{format-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/mickael-menu/zk/issues).
If you wish to customize more of `fzf` behavior, [please post a feature request](https://github.com/zk-org/zk/issues).
## Preview command
@ -60,3 +60,33 @@ The following variables are available in the line template.
| `checksum` | string | SHA-256 checksum of the note file |
1. YAML keys are normalized to lower case.
## `fzf` options
You can override the default `fzf` options used by `zk` with `fzf-options`. Look at `man fzf` for the list of available options.
```toml
[tool]
fzf-options = "--height 40% --border"
```
Note that this overrides all the default options used by `zk`, you might want to keep some of them:
* `--tiebreak begin` Prefer matches located at the beginning of the line
* `--exact` Look for exact matches instead of fuzzy ones by default
* `--tabstop 4` Length of tab characters
* `--height 100%` Height of the list relative to the terminal window
* `--layout reverse` Display the input field at the top
* `--no-hscroll` Make sure the path and titles are always visible
* `--color hl:-1,hl+:-1` Don't highlight search terms
* `--preview-window wrap` Enable line wrapping in the preview window
## Key bindings
When running `fzf` with `zk edit --interactive`, you can [create a new note with the `Ctrl-E` key binding](note-creation.md#search-or-create-with-a-single-command). This binding is customizable with `fzf-bind-new`. You can also disable it by setting it to an empty string (`""`).
```toml
[tool]
fzf-bind-new = "Ctrl-C"
```

@ -0,0 +1,14 @@
# 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,38 +1,54 @@
module github.com/mickael-menu/zk
module github.com/zk-org/zk
go 1.15
replace github.com/tliron/glsp => github.com/mickael-menu/glsp v0.1.1
go 1.21
require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/alecthomas/kong v0.2.18-0.20210927063154-5c7b038540ab
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/alecthomas/kong v0.5.0
github.com/aymerick/raymond v2.0.2+incompatible
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/fatih/color v1.13.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/go-testfixtures/testfixtures/v3 v3.6.1
github.com/google/go-cmp v0.5.8
github.com/gosimple/slug v1.12.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lestrrat-go/strftime v1.0.5
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/lestrrat-go/strftime v1.0.6
github.com/mattn/go-isatty v0.0.14
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/mattn/go-sqlite3 v1.14.22
github.com/mvdan/xurls v1.1.0
github.com/pelletier/go-toml v1.9.4
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
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.3
github.com/schollz/progressbar/v3 v3.8.6
github.com/tj/go-naturaldate v1.3.0
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
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
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
)

1382
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/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"
"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"
)
// 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/mickael-menu/zk/issues/4
// See https://github.com/zk-org/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/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestEditorUsesZkEditorFirst(t *testing.T) {

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

@ -8,9 +8,10 @@ import (
"strings"
"sync"
"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"
"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"
)
// ErrCancelled is returned when the user cancelled fzf.
@ -26,6 +27,8 @@ var (
type Opts struct {
// Preview command executed by fzf when hovering a line.
PreviewCmd opt.String
// Optionally provide additional arguments, taken from the config `fzf-options` property.
Options opt.String
// Amount of space between two non-empty fields.
Padding int
// Delimiter used by fzf between fields.
@ -72,21 +75,18 @@ func New(opts Opts) (*Fzf, error) {
opts.Delimiter = "\x01"
}
// Hard-coded fzf options that are required by zk.
args := []string{
"--delimiter", opts.Delimiter,
"--tiebreak", "begin",
"--ansi",
"--exact",
"--tabstop", "4",
"--height", "100%",
"--layout", "reverse",
//"--info", "inline",
// Make sure the path and titles are always visible
"--no-hscroll",
// Don't highlight search terms
"--color", "hl:-1,hl+:-1",
"--preview-window", "wrap",
"--delimiter", opts.Delimiter,
}
// Additional options.
additionalArgs, err := shellquote.Split(opts.Options.String())
if err != nil {
return nil, errors.Wrapf(err, "can't split the fzf-options: %s", opts.Options.String())
}
args = append(args, additionalArgs...)
header := ""
binds := []string{}

@ -4,12 +4,13 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"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"
"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"
)
// NoteFilter uses fzf to filter interactively a set of notes.
@ -32,6 +33,10 @@ type NoteFilterOpts struct {
AlwaysFilter bool
// Format for a single line, taken from the config `fzf-line` property.
LineTemplate opt.String
// Optionally provide additional arguments, taken from the config `fzf-options` property.
FzfOptions opt.String
// Key binding for the new action.
NewBinding opt.String
// Preview command to run when selecting a note.
PreviewCmd opt.String
// When non null, a "create new note from query" binding will be added to
@ -88,16 +93,20 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
suffix = " in " + dir.Name + "/"
}
newBinding := f.opts.NewBinding.OrString("Ctrl-E").String()
if newBinding != "" {
bindings = append(bindings, Binding{
Keys: "Ctrl-N",
Keys: newBinding,
Description: "create a note with the query as title" + suffix,
Action: fmt.Sprintf(`abort+execute("%s" new "%s" --title {q} < /dev/tty > /dev/tty)`, zkBin, dir.Path),
})
}
}
previewCmd := f.opts.PreviewCmd.OrString("cat {-1}").Unwrap()
fzf, err := New(Opts{
Options: f.opts.FzfOptions.OrString(defaultOptions),
PreviewCmd: opt.NewNotEmptyString(previewCmd),
Padding: 2,
Bindings: bindings,
@ -158,6 +167,18 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
var defaultLineTemplate = `{{style "title" title-or-path}} {{style "understate" body}} {{style "understate" (json metadata)}}`
// defaultOptions are the default fzf options used when filtering notes.
var defaultOptions = strings.Join([]string{
"--tiebreak begin", // Prefer matches located at the beginning of the line
"--exact", // Look for exact matches instead of fuzzy ones by default
"--tabstop 4", // Length of tab characters
"--height 100%", // Height of the list relative to the terminal window
"--layout reverse", // Display the input field at the top
"--no-hscroll", // Make sure the path and titles are always visible
"--color hl:-1,hl+:-1", // Don't highlight search terms
"--preview-window wrap", // Enable line wrapping in the preview window
}, " ")
type lineRenderContext struct {
Filename string
FilenameStem string `handlebars:"filename-stem"`

@ -6,22 +6,23 @@ import (
"path/filepath"
"github.com/aymerick/raymond"
"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"
"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"
)
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/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"
"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"
)
func init() {
@ -226,19 +226,131 @@ 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, "{{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")
testString(t, "{{format-date (date \"2009-11-17T20:34:58\") 'timestamp'}}", context, "200911172034")
}
func TestShellHelper(t *testing.T) {

@ -1,24 +1,51 @@
package helpers
import (
"os"
"time"
"github.com/aymerick/raymond"
"github.com/lestrrat-go/strftime"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/util"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/pkg/errors"
"github.com/rvflash/elapsed"
)
// RegisterDate registers the {{date}} template helpers which format a given date.
// 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.
//
// It supports various styles: short, medium, long, full, year, time,
// timestamp, timestamp-unix or a custom strftime format.
//
// {{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-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 {
format := "%Y-%m-%d"
if arg, ok := arg.(string); ok {
@ -31,7 +58,7 @@ func RegisterDate(logger util.Logger) {
} else {
res, err := strftime.Format(format, date, strftime.WithUnixSeconds('s'))
if err != nil {
logger.Printf("the {{date}} template helper failed to format the date: %v", err)
logger.Printf("the {{format-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/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
)
// RegisterJSON registers a {{json}} template helper which serializes its

@ -1,8 +1,8 @@
package helpers
import (
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/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/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/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/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/exec"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/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/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/util"
)
// NewSlugHelper creates a new template helper to slugify text.

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

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

@ -0,0 +1,64 @@
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/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"
"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"
)
const cmdList = "zk.list"

@ -4,10 +4,10 @@ import (
"fmt"
"path/filepath"
"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/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/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
@ -23,7 +23,9 @@ type cmdNewOpts struct {
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"`
}
func executeCommandNew(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) {
@ -51,6 +53,7 @@ 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 {
@ -69,43 +72,41 @@ func executeCommandNew(notebook *core.Notebook, documents *documentStore, contex
return nil, errors.New("zk.new could not generate a new note")
}
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
if opts.InsertContentAtLocation != nil {
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}},
},
},
}, nil)
}
currentDir := filepath.Dir(doc.Path)
linkFormatterContext, err := core.NewLinkFormatterContext(note.AsMinimalNote(), notebook.Path, currentDir)
if err != nil {
return nil, err
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)
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.InsertLinkAtLocation.URI: {{Range: opts.InsertLinkAtLocation.Range, NewText: link}},
},
},
}, nil)
}
absPath := filepath.Join(notebook.Path, note.Path)
if opts.Edit {
if !opts.DryRun && opts.Edit {
go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
URI: pathToURI(absPath),
TakeFocus: boolPtr(true),
}, nil)
}
return map[string]interface{}{"path": absPath}, nil
return map[string]interface{}{
"path": absPath,
"content": note.RawContent,
}, nil
}

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

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

@ -2,14 +2,17 @@ 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"
"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.
@ -93,25 +96,13 @@ func (d *document) ApplyChanges(changes []interface{}) {
d.lines = nil
}
var nonEmptyString = regexp.MustCompile(`\S+`)
// WordAt returns the word found at the given location.
// Credit https://github.com/aca/neuron-language-server/blob/450a7cff71c14e291ee85ff8a0614fa9d4dd5145/utils.go#L13
func (d *document) WordAt(pos protocol.Position) string {
line, ok := d.GetLine(int(pos.Line))
if !ok {
return ""
}
charIdx := int(pos.Character)
wordIdxs := nonEmptyString.FindAllStringIndex(line, -1)
for _, wordIdx := range wordIdxs {
if wordIdx[0] <= charIdx && charIdx <= wordIdx[1] {
return line[wordIdx[0]:wordIdx[1]]
}
}
return ""
return strutil.WordAt(line, int(pos.Character))
}
// ContentAtRange returns the document text at given range.
@ -141,34 +132,46 @@ 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 line[0:charIdx]
return string(utf16.Decode(utf16Bytes[0:charIdx]))
}
return line[(charIdx - length):charIdx]
return string(utf16.Decode(utf16Bytes[(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(line)
lineLength := len(utf16Bytes)
charIdx := int(pos.Character)
if lineLength <= charIdx+length {
return line[charIdx:]
return string(utf16.Decode(utf16Bytes[charIdx:]))
}
return line[charIdx:(charIdx + length)]
return string(utf16.Decode(utf16Bytes[charIdx:(charIdx + length)]))
}
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?: *\| *(.+?))?\]\]`)
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
// 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
}
// DocumentLinkAt returns the internal or external link found in the document
// at the given position.
@ -187,6 +190,66 @@ 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) {
@ -195,13 +258,22 @@ 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),
Range: protocol.Range{
Start: protocol.Position{
Line: protocol.UInteger(lineIndex),
@ -217,27 +289,84 @@ 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)
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]]
// Valid Markdown links are percent-encoded.
// Decode the href if it's 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
}
// IsTagPosition returns whether the given caret position is inside a tag (YAML frontmatter, #hashtag, etc.).
func (d *document) IsTagPosition(position protocol.Position, noteContentParser core.NoteContentParser) bool {
lines := strutil.CopyList(d.GetLines())
lineIdx := int(position.Line)
charIdx := int(position.Character)
line := lines[lineIdx]
// https://github.com/zk-org/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)
if err != nil {
return false
}
return strutil.Contains(note.Tags, targetWord)
}
type documentLink struct {
Href string
RelativeToDir string
Range protocol.Range
// HasTitle indicates whether this link has a title information. For
// example [[filename]] doesn't but [[filename|title]] does.

@ -3,16 +3,16 @@ package lsp
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"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/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/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
glspserv "github.com/tliron/glsp/server"
@ -25,9 +25,11 @@ type Server struct {
server *glspserv.Server
notebooks *core.NotebookStore
documents *documentStore
noteContentParser core.NoteContentParser
templateLoader core.TemplateLoader
fs core.FileStorage
logger util.Logger
useAdditionalTextEdits opt.Bool
}
// ServerOpts holds the options to create a new Server.
@ -65,6 +67,7 @@ func NewServer(opts ServerOpts) *Server {
templateLoader: opts.TemplateLoader,
fs: fs,
logger: opts.Logger,
useAdditionalTextEdits: opt.NullBool,
}
var clientCapabilities protocol.ClientCapabilities
@ -78,6 +81,15 @@ func NewServer(opts ServerOpts) *Server {
protocol.SetTraceValue(*params.Trace)
}
if params.ClientInfo != nil {
if params.ClientInfo.Name == "Visual Studio Code" {
// Visual Studio Code doesn't seem to support inl
// VSCode doesn't support deleting the trigger characters with
// the main TextEdit. We'll use additional text edits instead.
server.useAdditionalTextEdits = opt.True
}
}
capabilities := handler.CreateServerCapabilities()
capabilities.HoverProvider = true
capabilities.DefinitionProvider = true
@ -178,8 +190,6 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentCompletion = func(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) {
// We don't use the context because clients might not send it. Instead,
// we'll look for trigger patterns in the document.
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
@ -190,33 +200,16 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
switch doc.LookBehind(params.Position, 3) {
case "]((":
return server.buildLinkCompletionList(doc, notebook, params)
}
switch doc.LookBehind(params.Position, 2) {
case "[[":
return server.buildLinkCompletionList(doc, notebook, params)
}
switch doc.LookBehind(params.Position, 1) {
case "#":
if notebook.Config.Format.Markdown.Hashtags {
return server.buildTagCompletionList(notebook, "#")
}
case ":":
if notebook.Config.Format.Markdown.ColonTags {
return server.buildTagCompletionList(notebook, ":")
if params.Context != nil && params.Context.TriggerKind == protocol.CompletionTriggerKindInvoked {
return server.buildInvokedCompletionList(notebook, doc, params.Position)
} else {
return server.buildTriggerCompletionList(notebook, doc, params.Position)
}
}
return nil, nil
}
handler.CompletionItemResolve = func(context *glsp.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) {
if path, ok := params.Data.(string); ok {
content, err := ioutil.ReadFile(path)
content, err := os.ReadFile(path)
if err != nil {
return params, err
}
@ -245,7 +238,7 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.noteForLink(*link, doc, notebook)
target, err := server.noteForLink(*link, notebook)
if err != nil || target == nil {
return nil, err
}
@ -257,7 +250,7 @@ func NewServer(opts ServerOpts) *Server {
}
path = fs.Canonical(path)
contents, err := ioutil.ReadFile(path)
contents, err := os.ReadFile(path)
if err != nil {
return nil, err
}
@ -288,16 +281,25 @@ func NewServer(opts ServerOpts) *Server {
documentLinks := []protocol.DocumentLink{}
for _, link := range links {
target, err := server.noteForLink(link, doc, notebook)
if target == nil || err != nil {
continue
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
}
}
if target != "" {
documentLinks = append(documentLinks, protocol.DocumentLink{
Range: link.Range,
Target: &target.URI,
Target: &target,
})
}
}
return documentLinks, err
}
@ -318,7 +320,7 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.noteForLink(*link, doc, notebook)
target, err := server.noteForLink(*link, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
@ -367,6 +369,13 @@ 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 {
@ -419,6 +428,7 @@ func NewServer(opts ServerOpts) *Server {
Title: actionTitle,
Kind: stringPtr(protocol.CodeActionKindRefactor),
Command: &protocol.Command{
Title: actionTitle,
Command: cmdNew,
Arguments: []interface{}{wd, jsonOpts},
},
@ -449,25 +459,19 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
if link == nil {
href, err := notebook.RelPath(doc.Path)
link, err = doc.LinkFromRoot(notebook)
if err != nil {
return nil, err
}
link = &documentLink{Href: href}
}
target, err := server.noteForLink(*link, doc, notebook)
target, err := server.noteForLink(*link, 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{p}},
LinkTo: &core.LinkFilter{Hrefs: []string{target.Path}},
}
notes, err := notebook.FindNotes(opts)
@ -517,21 +521,17 @@ func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
return s.notebooks.Open(doc.Path)
}
// noteForLink returns the LSP documentUri for the note targeted by the given link.
// noteForLink returns the Note object 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, doc *document, notebook *core.Notebook) (*Note, error) {
note, err := s.noteForHref(link.Href, doc, notebook)
func (s *Server) noteForLink(link documentLink, notebook *core.Notebook) (*Note, error) {
note, err := s.noteForHref(link.Href, link.RelativeToDir, 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,13 +541,17 @@ func (s *Server) noteForLink(link documentLink, doc *document, notebook *core.No
return &Note{*note, pathToURI(joined_path)}, nil
}
// 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) {
// 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) {
if strutil.IsURL(href) {
return nil, nil
}
path := filepath.Clean(filepath.Join(filepath.Dir(doc.Path), href))
path := href
if relativeToDir != "" {
path = filepath.Clean(filepath.Join(relativeToDir, path))
}
path, err := filepath.Rel(notebook.Path, path)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve href: %s", href)
@ -559,19 +563,6 @@ func (s *Server) noteForHref(href string, doc *document, notebook *core.Notebook
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
@ -612,7 +603,7 @@ func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyF
if strutil.IsURL(link.Href) {
continue
}
target, err := s.noteForLink(link, doc, notebook)
target, err := s.noteForLink(link, notebook)
if err != nil {
s.logger.Err(err)
continue
@ -649,7 +640,51 @@ func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyF
}()
}
func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar string) ([]protocol.CompletionItem, error) {
// 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))
}
return nil, nil
}
// buildTriggerCompletionList builds the completion item response for a
// completion started with a trigger character.
func (s *Server) buildTriggerCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
// We don't use the context because clients might not send it. Instead,
// we'll look for trigger patterns in the document.
switch doc.LookBehind(position, 3) {
case "]((":
return s.buildLinkCompletionList(notebook, doc, position)
}
switch doc.LookBehind(position, 2) {
case "[[":
return s.buildLinkCompletionList(notebook, doc, position)
}
switch doc.LookBehind(position, 1) {
case "#":
if notebook.Config.Format.Markdown.Hashtags {
return s.buildTagCompletionList(notebook, "#")
}
case ":":
if notebook.Config.Format.Markdown.ColonTags {
return s.buildTagCompletionList(notebook, ":")
}
}
return nil, nil
}
func (s *Server) buildTagCompletionList(notebook *core.Notebook, prefix string) ([]protocol.CompletionItem, error) {
tags, err := notebook.FindCollections(core.CollectionKindTag, nil)
if err != nil {
return nil, err
@ -659,7 +694,7 @@ func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar str
for _, tag := range tags {
items = append(items, protocol.CompletionItem{
Label: tag.Name,
InsertText: s.buildInsertForTag(tag.Name, triggerChar, notebook.Config),
InsertText: s.buildInsertForTag(tag.Name, prefix, notebook.Config),
Detail: stringPtr(fmt.Sprintf("%d %s", tag.NoteCount, strutil.Pluralize("note", tag.NoteCount))),
})
}
@ -667,8 +702,8 @@ func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar str
return items, nil
}
func (s *Server) buildInsertForTag(name string, triggerChar string, config core.Config) *string {
switch triggerChar {
func (s *Server) buildInsertForTag(name string, prefix string, config core.Config) *string {
switch prefix {
case ":":
name += ":"
case "#":
@ -683,8 +718,8 @@ func (s *Server) buildInsertForTag(name string, triggerChar string, config core.
return &name
}
func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) ([]protocol.CompletionItem, error) {
linkFormatter, err := newLinkFormatter(doc, notebook, params)
func (s *Server) buildLinkCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
linkFormatter, err := newLinkFormatter(notebook, doc, position)
if err != nil {
return nil, err
}
@ -701,7 +736,7 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
var items []protocol.CompletionItem
for _, note := range notes {
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter, templates)
item, err := s.newCompletionItem(notebook, note, doc, position, linkFormatter, templates)
if err != nil {
s.logger.Err(err)
continue
@ -713,8 +748,8 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
return items, nil
}
func newLinkFormatter(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) (core.LinkFormatter, error) {
if doc.LookBehind(params.Position, 3) == "]((" {
func newLinkFormatter(notebook *core.Notebook, doc *document, position protocol.Position) (core.LinkFormatter, error) {
if doc.LookBehind(position, 3) == "]((" {
return core.NewMarkdownLinkFormatter(notebook.Config.Format.Markdown, true)
} else {
return notebook.NewLinkFormatter()
@ -772,24 +807,36 @@ func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNot
return item, err
}
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, -2, 0),
Range: rangeFromPosition(pos, startOffset, 0),
})
item.AdditionalTextEdits = addTextEdits
}
return item, nil
}
func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (interface{}, error) {
currentDir := filepath.Dir(doc.Path)
context, err := core.NewLinkFormatterContext(note, notebook.Path, currentDir)
path := core.NotebookPath{
Path: note.Path,
BasePath: notebook.Path,
WorkingDir: filepath.Dir(doc.Path),
}
context, err := core.NewLinkFormatterContext(path, note.Title, note.Metadata)
if err != nil {
return nil, err
}
@ -798,6 +845,17 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo
return nil, err
}
// 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)
}
}
// Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to
// remove the closing ]] or )) after the completion.
endOffset := 0
@ -808,10 +866,17 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo
return protocol.TextEdit{
NewText: link,
Range: rangeFromPosition(pos, 0, endOffset),
Range: rangeFromPosition(pos, startOffset, endOffset),
}, nil
}
func (s *Server) useAdditionalTextEditsWithNotebook(nb *core.Notebook) bool {
return nb.Config.LSP.Completion.UseAdditionalTextEdits.
Or(s.useAdditionalTextEdits).
OrBool(false).
Unwrap()
}
func positionInRange(content string, rng protocol.Range, pos protocol.Position) bool {
start, end := rng.IndexesIn(content)
i := pos.IndexIn(content)

@ -3,8 +3,14 @@ package lsp
import (
"fmt"
"net/url"
"path/filepath"
"runtime"
"strings"
"github.com/mickael-menu/zk/internal/util/errors"
"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"
)
func pathToURI(path string) string {
@ -16,13 +22,26 @@ func pathToURI(path string) string {
}
func uriToPath(uri string) (string, error) {
parsed, err := url.Parse(uri)
s := strings.ReplaceAll(uri, "%5C", "/")
parsed, err := url.Parse(s)
if err != nil {
return "", err
}
if parsed.Scheme != "file" {
return "", errors.New("URI was not a file:// URI")
}
if runtime.GOOS == "windows" {
// In Windows "file:///c:/tmp/foo.md" is parsed to "/c:/tmp/foo.md".
// Strip the first character to get a valid path.
if strings.Contains(parsed.Path[1:], ":") {
// url.Parse() behaves differently with "file:///c:/..." and "file://c:/..."
return parsed.Path[1:], nil
} else {
// if the windows drive is not included in Path it will be in Host
return parsed.Host + "/" + parsed.Path[1:], nil
}
}
return parsed.Path, nil
}
@ -41,3 +60,59 @@ 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,7 +6,6 @@ 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"
@ -14,7 +13,7 @@ import (
// Tags represents a list of inline tags in a Markdown document.
type Tags struct {
gast.BaseInline
ast.BaseInline
// Tags in this list.
Tags []string
}
@ -22,13 +21,13 @@ type Tags struct {
func (n *Tags) Dump(source []byte, level int) {
m := map[string]string{}
m["Tags"] = strings.Join(n.Tags, ", ")
gast.DumpHelper(n, source, level, m, nil)
ast.DumpHelper(n, source, level, m, nil)
}
// KindTags is a NodeKind of the Tags node.
var KindTags = gast.NewNodeKind("Tags")
var KindTags = ast.NewNodeKind("Tags")
func (n *Tags) Kind() gast.NodeKind {
func (n *Tags) Kind() ast.NodeKind {
return KindTags
}
@ -168,7 +167,7 @@ func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
block.Advance(endPos)
return &Tags{
BaseInline: gast.BaseInline{},
BaseInline: ast.BaseInline{},
Tags: []string{tag},
}
}
@ -226,7 +225,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
} else if char == ':' {
tag = strings.TrimSpace(tag)
if len(tag) == 0 {
if !isValidTag(tag) {
break
}
tags = append(tags, tag)
@ -248,7 +247,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
block.Advance(endPos)
return &Tags{
BaseInline: gast.BaseInline{},
BaseInline: ast.BaseInline{},
Tags: tags,
}
}
@ -260,3 +259,19 @@ func isValidTagChar(r rune, excluded rune) bool {
r == '&' || r == '+' || r == '=' || r == ':' ||
r == '#')
}
func isValidTag(tag string) bool {
if len(tag) == 0 {
return false
}
// Prevent Markdown table syntax to be parsed a a colon tag, e.g. |:---:|
// https://github.com/zk-org/zk/issues/185
for _, c := range tag {
if c != '-' {
return true
}
}
return false
}

@ -3,7 +3,7 @@ package extensions
import (
"strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/zk-org/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/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/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/mvdan/xurls"
"github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta"

@ -3,10 +3,10 @@ package markdown
import (
"testing"
"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"
"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"
)
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/mickael-menu/zk/issues/118
// See https://github.com/zk-org/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/mickael-menu/zk/issues/86
// i.e. https://github.com/zk-org/zk/issues/86
test("[foo%20bar](202110031652%20foo%20bar)", []core.Link{
{
Title: "foo%20bar",

@ -5,9 +5,9 @@ import (
"fmt"
"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/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
)
// CollectionDAO persists collections (e.g. tags) in the SQLite database.

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

@ -3,10 +3,11 @@ package sqlite
import (
"database/sql"
"fmt"
"regexp"
sqlite "github.com/mattn/go-sqlite3"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
)
func init() {
@ -16,6 +17,9 @@ 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
},
})
@ -207,6 +211,12 @@ func (db *DB) migrate() error {
LEFT JOIN notes t ON l.target_id = t.id`,
},
},
{ // 7
SQL: []string{},
// https://github.com/zk-org/zk/issues/170#issuecomment-1107848441
NeedsReindexing: true,
},
}
needsReindexing := false

@ -3,8 +3,8 @@ package sqlite
import (
"testing"
"github.com/mickael-menu/zk/internal/util/fixtures"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/fixtures"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestOpen(t *testing.T) {
@ -27,7 +27,7 @@ func TestMigrateFrom0(t *testing.T) {
var version int
err := tx.QueryRow("PRAGMA user_version").Scan(&version)
assert.Nil(t, err)
assert.Equal(t, version, 6)
assert.Equal(t, version, 7)
_, err = tx.Exec(`
INSERT INTO notes (path, sortable_path, title, body, word_count, checksum)

@ -4,8 +4,8 @@ import (
"database/sql"
"fmt"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/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,19 +32,17 @@ 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 = ?
`),
}
}
@ -69,10 +67,9 @@ func (d *LinkDAO) RemoveAll(id core.NoteID) error {
return err
}
// 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)
// 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))
return err
}
@ -90,15 +87,31 @@ 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)
idsString := joinNoteIDs(ids, ",")
rows, err := d.tx.Query(fmt.Sprintf(`
query := `
SELECT id, source_id, source_path, target_id, target_path, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM resolved_links
WHERE source_id IN (%s) AND target_id IN (%s)
`, idsString, idsString))
`
if where != "" {
query += "\nWHERE " + where
}
rows, err := d.tx.Query(query)
if err != nil {
return links, err
}
@ -120,10 +133,11 @@ func (d *LinkDAO) FindBetweenNotes(ids []core.NoteID) ([]core.ResolvedLink, erro
func (d *LinkDAO) scanLink(row RowScanner) (*core.ResolvedLink, error) {
var (
id, sourceID, targetID, snippetStart, snippetEnd int
sourcePath, targetPath, title, href, linkType, snippet string
id, sourceID, snippetStart, snippetEnd int
targetID sql.NullInt64
sourcePath, title, href, linkType, snippet string
external bool
rels sql.NullString
targetPath, rels sql.NullString
)
err := row.Scan(
@ -137,10 +151,11 @@ 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),
TargetPath: targetPath,
TargetID: core.NoteID(targetID.Int64),
TargetPath: targetPath.String,
Link: core.Link{
Title: title,
Href: href,

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/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/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/errors"
)
// Known metadata keys.

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

@ -8,14 +8,12 @@ import (
"strings"
"time"
"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"
"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"
)
// NoteDAO persists notes in the SQLite database.
@ -76,7 +74,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/mickael-menu/zk/issues/23
-- See https://github.com/zk-org/zk/issues/23
ORDER BY LENGTH(path) ASC
`),
@ -279,12 +277,13 @@ 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 = icu.EscapePattern(href)
href = regexp.QuoteMeta(href)
if allowPartialHref {
ids, err := d.findIdsByPathRegex("^(.*/)?[^/]*" + href + "[^/]*$")
@ -298,7 +297,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
}
@ -380,8 +379,8 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
if opts.Mention == nil {
return opts, nil
}
if opts.ExactMatch {
return opts, fmt.Errorf("--exact-match and --mention cannot be used together")
if opts.MatchStrategy != core.MatchStrategyFts {
return opts, fmt.Errorf("--mention can only be used with --match-strategy=fts")
}
// Find the IDs for the mentioned paths.
@ -421,9 +420,7 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
}
// Expand the mention queries in the match predicate.
match := opts.Match.String()
match += " " + strings.Join(mentionQueries, " OR ")
opts.Match = opt.NewString(match)
opts.Match = append(opts.Match, " ("+strings.Join(mentionQueries, " OR ")+") ")
return opts, nil
}
@ -448,7 +445,7 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
transitiveClosure := false
maxDistance := 0
setupLinkFilter := func(hrefs []string, direction int, negate, recursive bool) error {
setupLinkFilter := func(tableAlias string, hrefs []string, direction int, negate, recursive bool) error {
ids, err := d.findIdsByHrefs(hrefs, true /* allowPartialHrefs */)
if err != nil {
return err
@ -463,27 +460,29 @@ 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 = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '<zk:match>' || l.title || '</zk:match>'), '\x01')"
snippetCol = fmt.Sprintf("GROUP_CONCAT(REPLACE(%s.snippet, %[1]s.title, '<zk:match>' || %[1]s.title || '</zk:match>'), '\x01')", tableAlias)
}
joinOns := make([]string, 0)
if direction <= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = l.target_id AND l.source_id IN %s)", idsList,
"(n.id = %[1]s.target_id AND %[1]s.source_id IN %[2]s)", tableAlias, idsList,
))
}
if direction >= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = l.source_id AND l.target_id IN %s)", idsList,
"(n.id = %[1]s.source_id AND %[1]s.target_id IN %[2]s)", tableAlias, idsList,
))
}
joinClauses = append(joinClauses, fmt.Sprintf(
"LEFT JOIN %s l ON %s",
"LEFT JOIN %[2]s %[1]s ON %[3]s",
tableAlias,
linksSrc,
strings.Join(joinOns, " OR "),
))
@ -517,16 +516,27 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
return nil
}
if !opts.Match.IsNull() {
if opts.ExactMatch {
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(opts.Match.String(), '\\'))
} else {
args = append(args, escapeLikeTerm(match, '\\'))
}
case core.MatchStrategyFts:
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(opts.Match.String()))
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
}
}
@ -613,7 +623,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(filter.Hrefs, -1, filter.Negate, filter.Recursive)
err := setupLinkFilter("l_by", filter.Hrefs, -1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -622,7 +632,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(filter.Hrefs, 1, filter.Negate, filter.Recursive)
err := setupLinkFilter("l_to", filter.Hrefs, 1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -630,11 +640,11 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.Related != nil {
maxDistance = 2
err := setupLinkFilter(opts.Related, 0, false, true)
err := setupLinkFilter("l_rel", opts.Related, 0, false, true)
if err != nil {
return nil, err
}
groupBy += " HAVING MIN(l.distance) = 2"
groupBy += " HAVING MIN(l_rel.distance) = 2"
}
if opts.Orphan {
@ -682,8 +692,6 @@ 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/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"
"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"
)
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/mickael-menu/zk/issues/111
// See https://github.com/zk-org/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/mickael-menu/zk/issues/111
// See https://github.com/zk-org/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/mickael-menu/zk/issues/111
// See https://github.com/zk-org/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,7 +312,8 @@ func TestNoteDAOFindMinimalAll(t *testing.T) {
func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
})
@ -366,7 +367,10 @@ func TestNoteDAOFindTag(t *testing.T) {
func TestNoteDAOFindMatch(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{Match: opt.NewString("daily | index")},
core.NoteFindOpts{
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
},
[]core.ContextualNote{
{
Note: core.Note{
@ -448,10 +452,26 @@ 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: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
@ -469,8 +489,8 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test := func(match string, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: opt.NewString(match),
ExactMatch: true,
Match: []string{match},
MatchStrategy: core.MatchStrategyExact,
},
expected,
)
@ -482,13 +502,27 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test(`[exact% ch\ar_acters]`, []string{"ref/test/a.md"})
}
func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) {
func TestNoteDAOFindMentionRequiresFtsMatchStrategy(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyExact,
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{
ExactMatch: true,
MatchStrategy: core.MatchStrategyRe,
Mention: []string{"mention"},
})
assert.Err(t, err, "--exact-match and --mention cannot be used together")
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")
})
}
@ -559,7 +593,10 @@ func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) {
func TestNoteDAOFindMentions(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{Mention: []string{"log/2021-01-03.md", "index.md"}},
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"log/2021-01-03.md", "index.md"},
},
[]core.ContextualNote{
{
Note: core.Note{
@ -623,6 +660,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"},
LinkTo: &core.LinkFilter{
Hrefs: []string{"log/2021-01-03.md", "index.md"},
@ -636,6 +674,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"},
}
_, err := dao.Find(opts)
@ -645,7 +684,10 @@ func TestNoteDAOFindMentionUnknown(t *testing.T) {
func TestNoteDAOFindMentionedBy(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"}},
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
},
[]core.ContextualNote{
{
Note: core.Note{
@ -697,6 +739,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"},
LinkedBy: &core.LinkFilter{
Hrefs: []string{"ref/test/b.md", "log/2021-01-04.md"},
@ -710,6 +753,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"},
}
_, err := dao.Find(opts)

@ -1,15 +1,21 @@
package sqlite
import (
"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"
"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"
)
// 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
@ -22,8 +28,9 @@ type dao struct {
metadata *MetadataDAO
}
func NewNoteIndex(db *DB, logger util.Logger) *NoteIndex {
func NewNoteIndex(notebookPath string, db *DB, logger util.Logger) *NoteIndex {
return &NoteIndex{
notebookPath: notebookPath,
db: db,
logger: logger,
}
@ -47,6 +54,37 @@ 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 {
@ -82,8 +120,14 @@ 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)
err = ni.addLinks(dao, id, note.Links)
if err != nil {
return err
}
err = ni.fixExistingLinks(dao, note.ID, note.Path)
if err != nil {
return err
}
@ -95,6 +139,84 @@ 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 {
@ -108,7 +230,7 @@ func (ni *NoteIndex) Update(note core.Note) error {
if err != nil {
return err
}
err = ni.addLinks(dao, id, note)
err = ni.addLinks(dao, id, note.Links)
if err != nil {
return err
}
@ -139,26 +261,19 @@ func (ni *NoteIndex) associateTags(collections *CollectionDAO, noteId core.NoteI
return nil
}
func (ni *NoteIndex) addLinks(dao *dao, id core.NoteID, note core.Note) error {
links, err := ni.resolveLinkNoteIDs(dao, id, note.Links)
func (ni *NoteIndex) addLinks(dao *dao, id core.NoteID, links []core.Link) error {
resolvedLinks, err := ni.resolveLinkNoteIDs(dao, id, links)
if err != nil {
return err
}
err = dao.links.Add(links)
if err != nil {
return err
}
return dao.links.SetTargetID(note.Path, id)
return dao.links.Add(resolvedLinks)
}
func (ni *NoteIndex) resolveLinkNoteIDs(dao *dao, sourceID core.NoteID, links []core.Link) ([]core.ResolvedLink, error) {
resolvedLinks := []core.ResolvedLink{}
for _, link := range links {
allowPartialMatch := (link.Type == core.LinkTypeWikiLink)
targetID, err := dao.notes.FindIdByHref(link.Href, allowPartialMatch)
targetID, err := ni.findLinkMatch(dao, "" /* base dir */, link.Href, link.Type)
if err != nil {
return resolvedLinks, err
}

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/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/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/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/mickael-menu/zk/issues/23
# See https://github.com/zk-org/zk/issues/23
- id: 1
path: "prefix-longest.md"
sortable_path: "prefix-longest.md"

Binary file not shown.

@ -4,8 +4,8 @@ import (
"testing"
"github.com/go-testfixtures/testfixtures/v3"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/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/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
)
type RowScanner interface {
@ -30,6 +30,14 @@ 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/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestEscapeLikeTerm(t *testing.T) {

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/fatih/color"
"github.com/mickael-menu/zk/internal/core"
"github.com/zk-org/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/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/test/assert"
)
func createTerminal() *Terminal {

@ -1,6 +1,7 @@
package term
import (
"fmt"
"os"
"strings"
@ -11,6 +12,7 @@ import (
// Terminal offers utilities to interact with the terminal.
type Terminal struct {
NoInput bool
ForceInput string
}
func New() *Terminal {
@ -38,8 +40,15 @@ func (t *Terminal) SupportsUTF8() bool {
// Confirm is a shortcut to prompt a yes/no question to the user.
func (t *Terminal) Confirm(msg string, defaultAnswer bool) (confirmed, skipped bool) {
if !t.IsInteractive() {
switch strings.ToLower(t.ForceInput) {
case "y":
return t.forceConfirm(msg, true)
case "n":
return t.forceConfirm(msg, false)
default:
return defaultAnswer, true
}
}
confirmed = false
prompt := &survey.Confirm{
@ -49,3 +58,16 @@ func (t *Terminal) Confirm(msg string, defaultAnswer bool) (confirmed, skipped b
survey.AskOne(prompt, &confirmed)
return confirmed, false
}
func (t *Terminal) forceConfirm(msg string, answer bool) (confirmed, skipped bool) {
msg = "? " + msg + " ("
if answer {
msg += "Y/n"
} else {
msg += "y/N"
}
msg += ")"
fmt.Println(msg)
return answer, false
}

@ -5,10 +5,10 @@ import (
"os"
"path/filepath"
"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/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"
)
// Edit opens notes matching a set of criteria with the user editor.

@ -5,11 +5,11 @@ import (
"fmt"
"os"
"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"
"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"
)
// Graph produces a directed graph of the notes matching a set of criteria.

@ -2,16 +2,20 @@ package cmd
import (
"fmt"
"os"
"time"
"github.com/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"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"
)
// Index indexes the content of all the notes in the notebook.
type Index struct {
Force bool `short:"f" help:"Force indexing all the notes."`
Verbose bool `short:"v" help:"Print detailed information about the indexing process."`
Quiet bool `short:"q" help:"Do not print statistics nor progress."`
Verbose bool `short:"v" xor:"print" help:"Print detailed information about the indexing process."`
Quiet bool `short:"q" xor:"print" help:"Do not print statistics nor progress."`
}
func (cmd *Index) Help() string {
@ -24,10 +28,37 @@ func (cmd *Index) Run(container *cli.Container) error {
return err
}
stats, err := notebook.Index(core.NoteIndexOpts{
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{
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/mickael-menu/zk/internal/cli"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/strings"
)
// Init creates a notebook in the given directory
@ -32,7 +32,8 @@ func (cmd *Init) Run(container *cli.Container) error {
return err
}
_, err = notebook.Index(core.NoteIndexOpts{})
index := Index{Quiet: true}
err = index.RunWithNotebook(container, notebook)
if err != nil {
return err
}
@ -89,9 +90,9 @@ func startInitWizard() (core.InitOpts, error) {
opts.WikiLinks = answers.WikiLink
opts.Hashtags = strings.InList(answers.Tags, hashtag)
opts.MultiwordTags = strings.InList(answers.Tags, multiwordTag)
opts.ColonTags = strings.InList(answers.Tags, colonTag)
opts.Hashtags = strings.Contains(answers.Tags, hashtag)
opts.MultiwordTags = strings.Contains(answers.Tags, multiwordTag)
opts.ColonTags = strings.Contains(answers.Tags, colonTag)
return opts, nil
}

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

Loading…
Cancel
Save