diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 51c90fca..b0dd231c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,7 +18,7 @@ Make sure that the path there to plenary is correct for you. ## Keeping the CHANGELOG up-to-date -This project tries hard to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and we maintain a [`CHANGELOG`](https://github.com/epwalsh/obsidian.nvim/blob/main/CHANGELOG.md) with a format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +This project tries hard to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and we maintain a [`CHANGELOG`](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/CHANGELOG.md) with a format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). If your PR addresses a bug or makes any other substantial change, please be sure to add an entry under the "Unreleased" section at the top of `CHANGELOG.md`. Entries should always be in the form of a list item under a level-3 header of either "Added", "Fixed", "Changed", or "Removed" for the most part. If the corresponding level-3 header for your item does not already exist in the "Unreleased" section, you should add it. @@ -49,7 +49,7 @@ However you can test how changes to the README will affect the Vim doc by runnin To do this you'll need install `pandoc` (e.g. `brew install pandoc` on Mac) and clone [panvimdoc](https://github.com/kdheepak/panvimdoc). Then from the panvimdoc repo root, run: ```bash -./panvimdoc.sh --project-name obsidian --input-file ../../epwalsh/obsidian.nvim/README.md --description 'a plugin for writing and navigating an Obsidian vault' --toc 'false' --vim-version 'NVIM v0.8.0' --demojify 'false' --dedup-subheadings 'false' --shift-heading-level-by '-1' && mv doc/obsidian.txt /tmp/ +./panvimdoc.sh --project-name obsidian --input-file ../../obsidian-nvim/obsidian.nvim/README.md --description 'a plugin for writing and navigating an Obsidian vault' --toc 'false' --vim-version 'NVIM v0.8.0' --demojify 'false' --dedup-subheadings 'false' --shift-heading-level-by '-1' && mv doc/obsidian.txt /tmp/ ``` This will build the Vim documentation to `/tmp/obsidian.txt`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e071bd1d..4c2baeab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: > - #### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/epwalsh/obsidian.nvim/issues?q=is%3Aissue+sort%3Acreated-desc+). + #### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/obsidian-nvim/obsidian.nvim/issues?q=is%3Aissue+sort%3Acreated-desc+). - type: textarea attributes: label: 🐛 Describe the bug diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0be5a8b0..c10877e3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Docs on: pull_request: branches: - - '*' + - "*" push: branches: [main] workflow_dispatch: @@ -14,9 +14,7 @@ concurrency: env: runtime: ~/.local/share/nvim/site/pack/vendor/start - minidoc-git: https://github.com/echasnovski/mini.doc - minidoc-path: ~/.local/share/nvim/site/pack/vendor/start/mini.doc - nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz + nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.tar.gz jobs: docs: @@ -38,7 +36,6 @@ jobs: mkdir -p ${{ env.runtime }} mkdir -p _neovim curl -sL ${{ env.nvim_url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim" - git clone --depth 1 ${{ env.minidoc-git }} ${{ env.minidoc-path }} ln -s $(pwd) ${{ env.runtime }} - name: Generate API docs @@ -74,5 +71,5 @@ jobs: if: github.event_name != 'pull_request' with: commit_user_name: github-actions[bot] - commit_message: 'chore(docs): auto generate docs' + commit_message: "chore(docs): auto generate docs" branch: ${{ github.head_ref }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 11692f71..0cfa36eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: include: - os: ubuntu-latest - nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz + nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.tar.gz packages: luarocks ripgrep manager: sudo apt-get diff --git a/CHANGELOG.md b/CHANGELOG.md index b585ed0e..718e539a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,24 +3,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with respect to the public API, which currently includes the installation steps, dependencies, configuration, keymappings, commands, and other plugin functionality. At the moment this does *not* include the Lua `Client` API, although in the future it will once that API stabilizes. +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with respect to the public API, which currently includes the installation steps, dependencies, configuration, keymappings, commands, and other plugin functionality. At the moment this does _not_ include the Lua `Client` API, although in the future it will once that API stabilizes. ## Unreleased ### Added +- Added support `text/uri-list` to `ObsidianPasteImg`. + +## [v3.10.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.10.0) - 2025-04-12 + +### Added + - Added `opts.follow_img_func` option for customizing how to handle image paths. - Added better handling for undefined template fields, which will now be prompted for. +- Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker +- Added support for the [`blink.cmp`](https://github.com/Saghen/blink.cmp) completion plugin. +- Added health check module +- Added a minimal sandbox script `minimal.lua` ### Changed - Renamed `opts.image_name_func` to `opts.attachments.img_name_func`. +- Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present +- `smart_action` shows picker for tags (`ObsidianTag`) when cursor is on a tag +- `ObsidianToggleCheckbox` now works with numbered lists +- `Makefile` is friendlier: self-documenting and automatically gets dependencies ### Fixed - Fixed an edge case with collecting backlinks. - Fixed typo in `ObsidianPasteImg`'s command description - Fixed the case when `opts.attachments` is `nil`. +- Fixed bug where `ObsidianNewFromTemplate` did not respect `note_id_func` +- Fixed bug where parser treats "Nan" as a number instead of a string ## [v3.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v3.9.0) - 2024-07-11 @@ -189,15 +205,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v3.7.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v3.7.0) - 2024-03-08 There's a lot of new features and improvements here that I'm really excited about 🥳 They've improved my workflow a ton and I hope they do for you too. To highlight the 3 biggest additions: + 1. 🔗 Full support for header anchor links and block links! That means both for following links and completion of links. Various forms of anchor/block links are support. Here are a few examples: - - Typical Obsidian-style wiki links, e.g. `[[My note#Heading 1]]`, `[[My note#Heading 1#Sub heading]]`, `[[My note#^block-123]]`. - - Wiki links with a label, e.g. `[[my-note#heading-1|Heading 1 in My Note]]`. - - Markdown links, e.g. `[Heading 1 in My Note](my-note.md#heading-1)`. - We also support links to headers within the same note, like for a table of contents, e.g. `[[#Heading 1]]`, `[[#heading-1|Heading]]`, `[[#^block-1]]`. + - Typical Obsidian-style wiki links, e.g. `[[My note#Heading 1]]`, `[[My note#Heading 1#Sub heading]]`, `[[My note#^block-123]]`. + - Wiki links with a label, e.g. `[[my-note#heading-1|Heading 1 in My Note]]`. + - Markdown links, e.g. `[Heading 1 in My Note](my-note.md#heading-1)`. + + We also support links to headers within the same note, like for a table of contents, e.g. `[[#Heading 1]]`, `[[#heading-1|Heading]]`, `[[#^block-1]]`. 2. 📲 A basic callback system to let you easily customize obisidian.nvim's behavior even more. There are currently 4 events: `post_setup`, `enter_note`, `pre_write_note`, and `post_set_workspace`. You can define a function for each of these in your config. -3. 🔭 Improved picker integrations (especially for telescope), particular for the `:ObsidianTags` command. See https://github.com/epwalsh/obsidian.nvim/discussions/450 for a demo. +3. 🔭 Improved picker integrations (especially for telescope), particular for the `:ObsidianTags` command. See for a demo. Full changelog below 👇 @@ -205,44 +223,44 @@ Full changelog below 👇 - Added a configurable callback system to further customize obsidian.nvim's behavior. Callbacks are defined through the `callbacks` field in the config: - ```lua - callbacks = { - -- Runs at the end of `require("obsidian").setup()`. - ---@param client obsidian.Client - post_setup = function(client) end, - - -- Runs anytime you enter the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - enter_note = function(client, note) end, - - -- Runs anytime you leave the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - leave_note = function(client, note) end, - - -- Runs right before writing the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - pre_write_note = function(client, note) end, - - -- Runs anytime the workspace is set/changed. - ---@param client obsidian.Client - ---@param workspace obsidian.Workspace - post_set_workspace = function(client, workspace) end, - } - ``` + ```lua + callbacks = { + -- Runs at the end of `require("obsidian").setup()`. + ---@param client obsidian.Client + post_setup = function(client) end, + + -- Runs anytime you enter the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + enter_note = function(client, note) end, + + -- Runs anytime you leave the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + leave_note = function(client, note) end, + + -- Runs right before writing the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + pre_write_note = function(client, note) end, + + -- Runs anytime the workspace is set/changed. + ---@param client obsidian.Client + ---@param workspace obsidian.Workspace + post_set_workspace = function(client, workspace) end, + } + ``` - Added configuration option `note_path_func(spec): obsidian.Path` for customizing how file names for new notes are generated. This takes a single argument, a table that looks like `{ id: string, dir: obsidian.Path, title: string|? }`, and returns an `obsidian.Path` object. The default behavior is equivalent to this: - ```lua - ---@param spec { id: string, dir: obsidian.Path, title: string|? } - ---@return string|obsidian.Path The full path to the new note. - note_path_func = function(spec) - local path = spec.dir / tostring(spec.id) - return path:with_suffix(".md") - end - ``` + ```lua + ---@param spec { id: string, dir: obsidian.Path, title: string|? } + ---@return string|obsidian.Path The full path to the new note. + note_path_func = function(spec) + local path = spec.dir / tostring(spec.id) + return path:with_suffix ".md" + end + ``` - Added config option `picker.tag_mappings`, analogous to `picker.note_mappings`. - Added `log` field to `obsidian.Client` for easier access to the logger. @@ -437,7 +455,7 @@ Minor internal improvements. ### Fixed -- Fixed parsing header with trailing whitespace (https://github.com/epwalsh/obsidian.nvim/issues/341#issuecomment-1925445271). +- Fixed parsing header with trailing whitespace (). ## [v2.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v2.9.0) - 2024-01-31 @@ -467,7 +485,7 @@ Minor internal improvements. ### Fixed - Fixed a YAML parsing issue with unquoted URLs in an array item. -- Fixed an issue on Windows when cloning a template into a new note. The root cause was this bug in plenary: https://github.com/nvim-lua/plenary.nvim/issues/489. We've added a work-around. +- Fixed an issue on Windows when cloning a template into a new note. The root cause was this bug in plenary: . We've added a work-around. ## [v2.7.1](https://github.com/epwalsh/obsidian.nvim/releases/tag/v2.7.1) - 2024-01-23 @@ -492,7 +510,7 @@ Minor internal improvements. ### Added -- Added extmarks that conceal "-", "*", or "+" with "•" by default. This can turned off by setting `.ui.bullets` to `nil` in your config. +- Added extmarks that conceal "-", "\*", or "+" with "•" by default. This can turned off by setting `.ui.bullets` to `nil` in your config. ### Fixed @@ -548,26 +566,26 @@ Minor internal improvements. - Added Lua API methods `Client:set_workspace(workspace: obsidian.Workspace)` and `Client:switch_workspace(workspace: string|obsidian.Workspace)`. - Added the ability to override settings per workspace by providing the `overrides` field in a workspace definition. For example: - ```lua - require("obsidian").setup({ - workspaces = { - { - name = "personal", - path = "~/vaults/personal", - }, - { - name = "work", - path = "~/vaults/work", - -- Optional, override certain settings. - overrides = { - notes_subdir = "notes", - }, + ```lua + require("obsidian").setup { + workspaces = { + { + name = "personal", + path = "~/vaults/personal", + }, + { + name = "work", + path = "~/vaults/work", + -- Optional, override certain settings. + overrides = { + notes_subdir = "notes", }, }, + }, - -- ... other options ... - }) - ``` + -- ... other options ... + } + ``` ### Fixed @@ -797,6 +815,7 @@ Major internal refactoring to bring performance improvements through async execu - Added `mappings` configuration field. - Added `open_notes_in` configuration field - Added `backlinks` options to the config. The default is + ```lua backlinks = { -- The default height of the backlinks pane. diff --git a/Makefile b/Makefile index 78516378..399458d4 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,31 @@ +SHELL:=/usr/bin/env bash + +.DEFAULT_GOAL:=help +PROJECT_NAME = "obsidian.nvim" TEST = test/obsidian -# This is where you have plenary installed locally. Override this at runtime if yours is elsewhere. +# Depending on your setup you have to override the locations at runtime. PLENARY = ~/.local/share/nvim/lazy/plenary.nvim/ MINIDOC = ~/.local/share/nvim/lazy/mini.doc/ -.PHONY : all -all : style lint test -.PHONY : test -test : +################################################################################ +##@ Developmment +.PHONY: chores +chores: style lint test ## Run all develoment tasks + +.PHONY: test +test: $(PLENARY) ## Run unit tests PLENARY=$(PLENARY) nvim \ --headless \ --noplugin \ -u test/minimal_init.vim \ -c "PlenaryBustedDirectory $(TEST) { minimal_init = './test/minimal_init.vim' }" +$(PLENARY): + git clone --depth 1 https://github.com/nvim-lua/plenary.nvim.git $(PLENARY) + .PHONY: api-docs -api-docs : +api-docs: $(MINIDOC) ## Generate API documentation with mini.doc MINIDOC=$(MINIDOC) nvim \ --headless \ --noplugin \ @@ -23,14 +33,28 @@ api-docs : -c "luafile scripts/generate_api_docs.lua" \ -c "qa!" -.PHONY : lint -lint : +$(MINIDOC): + git clone --depth 1 https://github.com/echasnovski/mini.doc $(MINIDOC) + +.PHONY: lint +lint: ## Lint the code luacheck . -.PHONY : style -style : +.PHONY: style +style: ## format the code stylua --check . -.PHONY : version -version : + +################################################################################ +##@ Helpers +.PHONY: version +version: ## Print the obsidian.nvim version @nvim --headless -c 'lua print("v" .. require("obsidian").VERSION)' -c q 2>&1 + +.PHONY: help +help: ## Display this help + @echo "Welcome to $$(tput bold)${PROJECT_NAME}$$(tput sgr0) 🥳📈🎉" + @echo "" + @echo "To get started:" + @echo " >>> $$(tput bold)make chores$$(tput sgr0)" + @awk 'BEGIN {FS = ":.*##"; printf "\033[36m\033[0m"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index d5d382b6..bccbd7b1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@

obsidian.nvim

-

Setup · Configure · Contribute · Discuss

-
Latest release Last commit Latest Neovim Made with Lua Buy me a coffee
+

Setup · Configure · Contribute · Discuss

+
Latest release Last commit Latest Neovim Made with Lua

-A Neovim plugin for writing and navigating [Obsidian](https://obsidian.md) vaults, written in Lua. +A **community fork** of the Neovim plugin for writing and navigating [Obsidian](https://obsidian.md) vaults, written in Lua, created by [epwalsh](https://github.com/epwalsh). Built for people who love the concept of Obsidian -- a simple, markdown-based notes app -- but love Neovim too much to stand typing characters into anything else. -If you're new to Obsidian I highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ?si=8nbnNsRVnw0xfX2S) for a great overview. +If you're new to Obsidian we highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ) for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile app and has a lot of functionality that's not feasible to implement in Neovim, such as the graph explorer view. That said, this plugin stands on its own as well. You don't necessarily need to use it alongside the Obsidian app. +## About the fork + +The original project has not been actively maintained for quite a while and with the ever-changing Neovim ecosystem, new widely used tools such as [blink.cmp](https://github.com/Saghen/blink.cmp) or [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) were not supported. +With bugs, issues and pull requests piling up, people from the community decided to fork and maintain the project. +The fork aims to stay close to the original, but fix bugs, include and merge useful improvements, and ensure long term robustness. + ## Table of contents - 👉 [Features](#features) @@ -28,7 +34,7 @@ _Keep in mind this plugin is not meant to replace Obsidian, but to complement it ## Features -▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note references and tags via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) (triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for tags), powered by [`ripgrep`](https://github.com/BurntSushi/ripgrep). +▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note references and tags via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) or [blink.cmp](https://github.com/Saghen/blink.cmp) (triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for tags), powered by [`ripgrep`](https://github.com/BurntSushi/ripgrep). [![See this screenshot](https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1)](https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1) @@ -116,14 +122,20 @@ Search functionality (e.g. via the `:ObsidianSearch` and `:ObsidianQuickSwitch` To configure obsidian.nvim you just need to call `require("obsidian").setup({ ... })` with the desired options. Here are some examples using different plugin managers. The full set of [plugin dependencies](#plugin-dependencies) and [configuration options](#configuration-options) are listed below. -> ⚠️ WARNING: if you install from the latest release (recommended for stability) instead of `main`, be aware that the README on `main` may reference features that haven't been released yet. For that reason I recommend viewing the README on the tag for the [latest release](https://github.com/epwalsh/obsidian.nvim/releases) instead of `main`. +> ⚠️ WARNING: if you install from the latest release (recommended for stability) instead of `main`, be aware that the README on `main` may reference features that haven't been released yet. For that reason I recommend viewing the README on the tag for the [latest release](https://github.com/obsidian-nvim/obsidian.nvim/releases) instead of `main`. + +> [!NOTE] +> To see you installation status, run `:checkhealth obsidian` +> To try out or debug this plugin, use `minimal.lua` in the repo to run a clean instance of obsidian.nvim #### Using [`lazy.nvim`](https://github.com/folke/lazy.nvim) +
Click for install snippet + ```lua return { - "epwalsh/obsidian.nvim", - version = "*", -- recommended, use latest release instead of latest commit + "obsidian-nvim/obsidian.nvim", + version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", -- Replace the above line with this if you only want to load obsidian.nvim for markdown files in your vault: @@ -157,12 +169,28 @@ return { } ``` +
+ +#### Using [`rocks.nvim`](https://github.com/nvim-neorocks/rocks.nvim) + +
Click for install snippet + +```vim +:Rocks install obsidian +``` + +
+ #### Using [`packer.nvim`](https://github.com/wbthomason/packer.nvim) +It is not recommended because packer.nvim is currently unmaintained + +
Click for install snippet + ```lua -use({ - "epwalsh/obsidian.nvim", - tag = "*", -- recommended, use latest release instead of latest commit +use { + "obsidian-nvim/obsidian.nvim", + tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. "nvim-lua/plenary.nvim", @@ -170,7 +198,7 @@ use({ -- see below for full list of optional dependencies 👇 }, config = function() - require("obsidian").setup({ + require("obsidian").setup { workspaces = { { name = "personal", @@ -183,29 +211,39 @@ use({ }, -- see below for full list of options 👇 - }) + } end, -}) +} ``` +
+ ### Plugin dependencies The only **required** plugin dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), but there are a number of optional dependencies that enhance the obsidian.nvim experience. **Completion:** -- **[recommended]** [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp): for completion of note references. +- **[recommended]** [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) +- [blink.cmp](https://github.com/Saghen/blink.cmp) (new) **Pickers:** -- **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim): for search and quick-switch functionality. -- [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. -- [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua): another alternative to telescope for search and quick-switch functionality. +- **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) +- [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua) +- [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library +- [Snacks.Picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) from the snacks.nvim library **Syntax highlighting:** -- **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter): for base markdown syntax highlighting. See [syntax highlighting](#syntax-highlighting) for more details. -- [preservim/vim-markdown](https://github.com/preservim/vim-markdown): an alternative to nvim-treesitter for syntax highlighting (see [syntax highlighting](#syntax-highlighting) for more details), plus other cool features. +See [syntax highlighting](#syntax-highlighting) for more details. + +- For base syntax highlighting: + - **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) + - [preservim/vim-markdown](https://github.com/preservim/vim-markdown) +- For additional syntax features: + - [render-markdown.nvim](https://github.com/MeanderingProgrammer/render-markdown.nvim) + - [markview.nvim](https://github.com/OXY2DEV/markview.nvim) **Miscellaneous:** @@ -215,7 +253,7 @@ If you choose to use any of these you should include them in the "dependencies" ### Configuration options -This is a complete list of all of the options that can be passed to `require("obsidian").setup()`. The settings below are *not necessarily the defaults, but represent reasonable default settings*. Please read each option carefully and customize it to your needs: +This is a complete list of all of the options that can be passed to `require("obsidian").setup()`. The settings below are _not necessarily the defaults, but represent reasonable default settings_. Please read each option carefully and customize it to your needs: ```lua { @@ -266,8 +304,10 @@ This is a complete list of all of the options that can be passed to `require("ob -- Optional, completion of wiki links, local markdown links, and tags using nvim-cmp. completion = { - -- Set to false to disable completion. + -- Enables completion using nvim_cmp nvim_cmp = true, + -- Enables completion using blink.cmp + blink = false, -- Trigger completion at 2 chars. min_chars = 2, }, @@ -289,7 +329,7 @@ This is a complete list of all of the options that can be passed to `require("ob end, opts = { buffer = true }, }, - -- Smart action depending on context, either follow link or toggle checkbox. + -- Smart action depending on context: follow link, show notes with tag, or toggle checkbox. [""] = { action = function() return require("obsidian").util.smart_action() @@ -412,7 +452,7 @@ This is a complete list of all of the options that can be passed to `require("ob open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. @@ -560,7 +600,7 @@ config = { name = "personal", path = "~/vaults/personal", }, - } + }, } ``` @@ -587,13 +627,12 @@ config = { -- ... }, }, - } + }, } ``` obsidian.nvim also supports "dynamic" workspaces. These are simply workspaces where the `path` is set to a Lua function (that returns a path) instead of a hard-coded path. This can be useful in several scenarios, such as when you want a workspace whose `path` is always set to the parent directory of the current buffer: - ```lua config = { workspaces = { @@ -603,7 +642,7 @@ config = { return assert(vim.fs.dirname(vim.api.nvim_buf_get_name(0))) end, }, - } + }, } ``` @@ -612,7 +651,9 @@ See [using obsidian.nvim outside of a workspace / Obsidian vault](#usage-outside #### Completion -obsidian.nvim will set itself up as an nvim-cmp source automatically when you enter a markdown buffer within your vault directory, you do **not** need to specify this plugin as a cmp source manually. +obsidian.nvim supports nvim_cmp and blink.cmp completion plugins. + +obsidian.nvim will set itself up automatically when you enter a markdown buffer within your vault directory, you do **not** need to specify this plugin as a cmp source manually. Note that in order to trigger completion for tags _within YAML frontmatter_ you still need to type the "#" at the start of the tag. obsidian.nvim will remove the "#" when you hit enter on the tag completion item. @@ -621,12 +662,12 @@ Note that in order to trigger completion for tags _within YAML frontmatter_ you If you're using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter/blob/master/README.md) you're configuration should include both "markdown" and "markdown_inline" sources: ```lua -require("nvim-treesitter.configs").setup({ +require("nvim-treesitter.configs").setup { ensure_installed = { "markdown", "markdown_inline", ... }, highlight = { enable = true, }, -}) +} ``` If you use `vim-markdown` you'll probably want to disable its frontmatter syntax highlighting (`vim.g.vim_markdown_frontmatter = 1`) which I've found doesn't work very well. @@ -728,7 +769,7 @@ templates = { ### Usage outside of a workspace or vault -It's possible to configure obsidian.nvim to work on individual markdown files outside of a regular workspace / Obsidian vault by configuring a "dynamic" workspace. To do so you just need to add a special workspace with a function for the `path` field (instead of a string), which should return a *parent* directory of the current buffer. This tells obsidian.nvim to use that directory as the workspace `path` and `root` (vault root) when the buffer is not located inside another fixed workspace. +It's possible to configure obsidian.nvim to work on individual markdown files outside of a regular workspace / Obsidian vault by configuring a "dynamic" workspace. To do so you just need to add a special workspace with a function for the `path` field (instead of a string), which should return a _parent_ directory of the current buffer. This tells obsidian.nvim to use that directory as the workspace `path` and `root` (vault root) when the buffer is not located inside another fixed workspace. For example, to extend the configuration above this way: @@ -768,8 +809,8 @@ And keep in mind that to reset a configuration option to `nil` you'll have to us ## Contributing -Please read the [CONTRIBUTING](https://github.com/epwalsh/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. +Please read the [CONTRIBUTING](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. -And if you're feeling especially generous I always appreciate some coffee funds! ❤️ +## Acknowledgement -[![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/epwalsh) +We would like to thank [epwalsh](https://github.com/epwalsh) for creating this beautiful plugin. If you're feeling especially generous, [he still appreciates some coffee funds! ❤️](https://www.buymeacoffee.com/epwalsh). diff --git a/doc/obsidian.txt b/doc/obsidian.txt index fc053994..5648bb69 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -2,15 +2,16 @@ obsidian.nvim -A Neovim plugin for writing and navigating Obsidian -vaults, written in Lua. +A **community fork** of the Neovim plugin for writing and navigating Obsidian + vaults, written in Lua, created by epwalsh +. Built for people who love the concept of Obsidian – a simple, markdown-based notes app – but love Neovim too much to stand typing characters into anything else. -If you’re new to Obsidian I highly recommend watching this excellent YouTube -video for a great overview. +If you’re new to Obsidian we highly recommend watching this excellent YouTube +video for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile @@ -20,7 +21,20 @@ own as well. You don’t necessarily need to use it alongside the Obsidian app. ============================================================================== -1. Table of contents *obsidian-table-of-contents* +1. About the fork *obsidian-about-the-fork* + +The original project has not been actively maintained for quite a while and +with the ever-changing Neovim ecosystem, new widely used tools such as +blink.cmp or snacks.picker + were not +supported. With bugs, issues and pull requests piling up, people from the +community decided to fork and maintain the project. The fork aims to stay close +to the original, but fix bugs, include and merge useful improvements, and +ensure long term robustness. + + +============================================================================== +2. Table of contents *obsidian-table-of-contents* - 👉 |obsidian-features| - |obsidian-commands| @@ -37,12 +51,13 @@ own as well. You don’t necessarily need to use it alongside the Obsidian app. ============================================================================== -2. Features *obsidian-features* +3. Features *obsidian-features* ▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note -references and tags via nvim-cmp -(triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for -tags), powered by `ripgrep` . +references and tags via nvim-cmp or +blink.cmp (triggered by typing `[[` for +wiki links, `[` for markdown links, or `#` for tags), powered by `ripgrep` +. @@ -129,7 +144,7 @@ DEMO *obsidian-demo* ============================================================================== -3. Setup *obsidian-setup* +4. Setup *obsidian-setup* SYSTEM REQUIREMENTS *obsidian-system-requirements* @@ -163,14 +178,20 @@ managers. The full set of |obsidian-plugin-dependencies| and stability) instead of `main`, be aware that the README on `main` may reference features that haven’t been released yet. For that reason I recommend viewing the README on the tag for the latest release - instead of `main`. + instead of `main`. + + [!NOTE] To see you installation status, run `:checkhealth obsidian` To try out + or debug this plugin, use `minimal.lua` in the repo to run a clean instance of + obsidian.nvim USING LAZY.NVIM ~ +Click for install snippet ~ + >lua return { - "epwalsh/obsidian.nvim", - version = "*", -- recommended, use latest release instead of latest commit + "obsidian-nvim/obsidian.nvim", + version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", -- Replace the above line with this if you only want to load obsidian.nvim for markdown files in your vault: @@ -205,12 +226,25 @@ USING LAZY.NVIM ~ < +USING ROCKS.NVIM ~ + +Click for install snippet ~ + +>vim + :Rocks install obsidian +< + + USING PACKER.NVIM ~ +It is not recommended because packer.nvim is currently unmaintained + +Click for install snippet ~ + >lua - use({ - "epwalsh/obsidian.nvim", - tag = "*", -- recommended, use latest release instead of latest commit + use { + "obsidian-nvim/obsidian.nvim", + tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. "nvim-lua/plenary.nvim", @@ -218,7 +252,7 @@ USING PACKER.NVIM ~ -- see below for full list of optional dependencies 👇 }, config = function() - require("obsidian").setup({ + require("obsidian").setup { workspaces = { { name = "personal", @@ -231,9 +265,9 @@ USING PACKER.NVIM ~ }, -- see below for full list of options 👇 - }) + } end, - }) + } < @@ -245,18 +279,26 @@ dependencies that enhance the obsidian.nvim experience. **Completion:** -- **[recommended]** hrsh7th/nvim-cmp : for completion of note references. +- **[recommended]** hrsh7th/nvim-cmp +- blink.cmp (new) **Pickers:** -- **[recommended]** nvim-telescope/telescope.nvim : for search and quick-switch functionality. -- Mini.Pick from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. -- ibhagwan/fzf-lua : another alternative to telescope for search and quick-switch functionality. +- **[recommended]** nvim-telescope/telescope.nvim +- ibhagwan/fzf-lua +- Mini.Pick from the mini.nvim library +- Snacks.Picker from the snacks.nvim library **Syntax highlighting:** -- **[recommended]** nvim-treesitter : for base markdown syntax highlighting. See |obsidian-syntax-highlighting| for more details. -- preservim/vim-markdown : an alternative to nvim-treesitter for syntax highlighting (see |obsidian-syntax-highlighting| for more details), plus other cool features. +See |obsidian-syntax-highlighting| for more details. + +- For base syntax highlighting: + - **[recommended]** nvim-treesitter + - preservim/vim-markdown +- For additional syntax features: + - render-markdown.nvim + - markview.nvim **Miscellaneous:** @@ -322,8 +364,10 @@ carefully and customize it to your needs: -- Optional, completion of wiki links, local markdown links, and tags using nvim-cmp. completion = { - -- Set to false to disable completion. + -- Enables completion using nvim_cmp nvim_cmp = true, + -- Enables completion using blink.cmp + blink = false, -- Trigger completion at 2 chars. min_chars = 2, }, @@ -345,7 +389,7 @@ carefully and customize it to your needs: end, opts = { buffer = true }, }, - -- Smart action depending on context, either follow link or toggle checkbox. + -- Smart action depending on context: follow link, show notes with tag, or toggle checkbox. [""] = { action = function() return require("obsidian").util.smart_action() @@ -468,7 +512,7 @@ carefully and customize it to your needs: open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. @@ -621,7 +665,7 @@ the `workspaces` field in your config would look like this: name = "personal", path = "~/vaults/personal", }, - } + }, } < @@ -653,7 +697,7 @@ example: -- ... }, }, - } + }, } < @@ -672,7 +716,7 @@ buffer: return assert(vim.fs.dirname(vim.api.nvim_buf_get_name(0))) end, }, - } + }, } < @@ -683,9 +727,11 @@ plugin’s functionality on markdown files outside of your "fixed" vaults. See COMPLETION ~ -obsidian.nvim will set itself up as an nvim-cmp source automatically when you -enter a markdown buffer within your vault directory, you do **not** need to -specify this plugin as a cmp source manually. +obsidian.nvim supports nvim_cmp and blink.cmp completion plugins. + +obsidian.nvim will set itself up automatically when you enter a markdown buffer +within your vault directory, you do **not** need to specify this plugin as a +cmp source manually. Note that in order to trigger completion for tags _within YAML frontmatter_ you still need to type the "#" at the start of the tag. obsidian.nvim will remove @@ -700,12 +746,12 @@ you’re configuration should include both "markdown" and "markdown_inline" sources: >lua - require("nvim-treesitter.configs").setup({ + require("nvim-treesitter.configs").setup { ensure_installed = { "markdown", "markdown_inline", ... }, highlight = { enable = true, }, - }) + } < If you use `vim-markdown` you’ll probably want to disable its frontmatter @@ -882,24 +928,26 @@ option to `nil` you’ll have to use `vim.NIL` there instead of the builtin Lua ============================================================================== -4. Contributing *obsidian-contributing* +5. Contributing *obsidian-contributing* Please read the CONTRIBUTING - + guide before submitting a pull request. -And if you’re feeling especially generous I always appreciate some coffee -funds! ❤️ - +============================================================================== +6. Acknowledgement *obsidian-acknowledgement* + +We would like to thank epwalsh for creating this +beautiful plugin. If you’re feeling especially generous, he still appreciates +some coffee funds! ❤️ . ============================================================================== -5. Links *obsidian-links* +7. Links *obsidian-links* 1. *See this screenshot*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1 2. *See this screenshot*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/e74f5267-21b5-49bc-a3bb-3b9db5fa6687 3. *2024-01-31 14 22 52*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e -4. *BuyMeACoffee*: https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black Generated by panvimdoc diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 1a288ef6..00b72f1c 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -6,13 +6,13 @@ The Obsidian.nvim Lua API. Table of contents - obsidian.Client............................................|obsidian.Client| + obsidian.Client .......................................... |obsidian.Client| - obsidian.Note................................................|obsidian.Note| + obsidian.Note .............................................. |obsidian.Note| - obsidian.Workspace......................................|obsidian.Workspace| + obsidian.Workspace .................................... |obsidian.Workspace| - obsidian.Path................................................|obsidian.Path| + obsidian.Path .............................................. |obsidian.Path| ------------------------------------------------------------------------------ *obsidian.SearchOpts* @@ -21,9 +21,9 @@ Class ~ {obsidian.SearchOpts} : obsidian.ABC Fields ~ -{sort} `(boolean|?)` -{include_templates} `(boolean|?)` -{ignore_case} `(boolean|?)` +{sort} `(boolean|)`? +{include_templates} `(boolean|)`? +{ignore_case} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.SearchOpts.from_tbl()* @@ -32,13 +32,13 @@ Parameters ~ {opts} `(obsidian.SearchOpts|table)` Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.SearchOpts.default()* `SearchOpts.default`() Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.Client* @@ -52,13 +52,13 @@ Class ~ {obsidian.Client} : obsidian.ABC Fields ~ -{current_workspace} obsidian.Workspace The current workspace. -{dir} obsidian.Path The root of the vault for the current workspace. -{opts} obsidian.config.ClientOpts The client config. -{buf_dir} obsidian.Path|? The parent directory of the current buffer. -{callback_manager} obsidian.CallbackManager -{log} obsidian.Logger -{_default_opts} obsidian.config.ClientOpts +{current_workspace} `(obsidian.Workspace)` The current workspace. +{dir} `(obsidian.Path)` The root of the vault for the current workspace. +{opts} `(obsidian.config.ClientOpts)` The client config. +{buf_dir} `(obsidian.Path|? The)` parent directory of the current buffer. +{callback_manager} `(obsidian.CallbackManager)` +{log} `(obsidian.Logger)` +{_default_opts} `(obsidian.config.ClientOpts)` {_quiet} `(boolean)` ------------------------------------------------------------------------------ @@ -71,17 +71,17 @@ client through: `require("obsidian").get_client()` Parameters ~ -{opts} obsidian.config.ClientOpts +{opts} `(obsidian.config.ClientOpts)` Return ~ -obsidian.Client +`(obsidian.Client)` ------------------------------------------------------------------------------ *obsidian.Client.set_workspace()* `Client.set_workspace`({self}, {workspace}, {opts}) Parameters ~ -{workspace} obsidian.Workspace -{opts} { lock: `(boolean|?)` }|? +{workspace} `(obsidian.Workspace)` +{opts} `({ lock: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.opts_for_workspace()* @@ -89,10 +89,10 @@ Parameters ~ Get the normalize opts for a given workspace. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.config.ClientOpts +`(obsidian.config.ClientOpts)` ------------------------------------------------------------------------------ *obsidian.Client.switch_workspace()* @@ -101,7 +101,7 @@ Switch to a different workspace. Parameters ~ {workspace} `(obsidian.Workspace|string)` The workspace object or the name of an existing workspace. -{opts} { lock: `(boolean|?)` }|? +{opts} `({ lock: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.path_is_note()* @@ -110,7 +110,7 @@ Check if a path represents a note in the workspace. Parameters ~ {path} `(string|obsidian.Path)` -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ `(boolean)` @@ -122,10 +122,10 @@ Get the absolute path to the root of the Obsidian vault for the given workspace current workspace. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Client.vault_name()* @@ -142,10 +142,10 @@ Make a path relative to the vault root, if possible. Parameters ~ {path} `(string|obsidian.Path)` -{opts} { strict: `(boolean|?)` }|? +{opts} `({ strict: boolean|? }|)`? Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.templates_dir()* @@ -153,10 +153,10 @@ obsidian.Path| `(optional)` Get the templates folder. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.should_save_frontmatter()* @@ -164,7 +164,7 @@ obsidian.Path| `(optional)` Determines whether a note's frontmatter is managed by obsidian.nvim. Parameters ~ -{note} obsidian.Note +{note} `(obsidian.Note)` Return ~ `(boolean)` @@ -179,7 +179,7 @@ Usage ~ Parameters ~ {cmd_name} `(string)` The name of the command. -{cmd_data} `(table|?)` The payload for the command. +{cmd_data} `(table|? The)` payload for the command. ------------------------------------------------------------------------------ *obsidian.Client.search_defaults()* @@ -187,7 +187,7 @@ Parameters ~ Get the default search options. Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.Client.find_notes()* @@ -196,10 +196,10 @@ Find notes matching the given term. Notes are searched based on ID, title, filen Parameters ~ {term} `(string)` The term to search for -{opts} { search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|?, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|?, timeout: integer|? }|)`? Return ~ -obsidian.Note[] +`(obsidian.Note[])` ------------------------------------------------------------------------------ *obsidian.Client.find_notes_async()* @@ -209,7 +209,7 @@ An async version of `find_notes()` that runs the callback with an array of all m Parameters ~ {term} `(string)` The term to search for {callback} `(fun(notes: obsidian.Note[]))` -{opts} { search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|? }|? +{opts} `({ search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.find_files()* @@ -218,10 +218,10 @@ Find non-markdown files in the vault. Parameters ~ {term} `(string)` The search term. -{opts} { search: obsidian.SearchOpts, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts, timeout: integer|? }|)`? Return ~ -obsidian.Path[] +`(obsidian.Path[])` ------------------------------------------------------------------------------ *obsidian.Client.find_files_async()* @@ -231,7 +231,7 @@ An async version of `find_files`. Parameters ~ {term} `(string)` The search term. {callback} `(fun(paths: obsidian.Path[]))` -{opts} { search: obsidian.SearchOpts }|? +{opts} `({ search: obsidian.SearchOpts }|)`? ------------------------------------------------------------------------------ *obsidian.Client.resolve_note()* @@ -241,10 +241,10 @@ The 'query' can be a path, filename, note ID, alias, title, etc. Parameters ~ {query} `(string)` -{opts} { timeout: `(integer|?,)` notes: obsidian.note.LoadOpts|? }|? +{opts} `({ timeout: integer|?, notes: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note `(...)` +`(obsidian.Note)` ... ------------------------------------------------------------------------------ *obsidian.Client.resolve_note_async()* @@ -254,10 +254,10 @@ An async version of `resolve_note()`. Parameters ~ {query} `(string)` {callback} `(fun(...: obsidian.Note))` -{opts} { notes: obsidian.note.LoadOpts|? }|? +{opts} `({ notes: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.resolve_note_async_with_picker_fallback()* @@ -268,10 +268,10 @@ there are multiple matches. Parameters ~ {query} `(string)` {callback} `(fun(obsidian.Note))` -{opts} { notes: obsidian.note.LoadOpts|?, prompt_title: `(string|?)` }|? +{opts} `({ notes: obsidian.note.LoadOpts|?, prompt_title: string|? }|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -280,14 +280,14 @@ Class ~ Fields ~ {location} `(string)` {name} `(string)` -{link_type} obsidian.search.RefTypes -{path} obsidian.Path|? -{note} obsidian.Note|? -{url} `(string|?)` -{line} `(integer|?)` -{col} `(integer|?)` -{anchor} obsidian.note.HeaderAnchor|? -{block} obsidian.note.Block|? +{link_type} `(obsidian.search.RefTypes)` +{path} `(obsidian.Path|)`? +{note} `(obsidian.Note|)`? +{url} `(string|)`? +{line} `(integer|)`? +{col} `(integer|)`? +{anchor} `(obsidian.note.HeaderAnchor|)`? +{block} `(obsidian.note.Block|)`? ------------------------------------------------------------------------------ *obsidian.Client.resolve_link_async()* @@ -295,7 +295,7 @@ Fields ~ Resolve a link. If the link argument is `nil` we attempt to resolve a link under the cursor. Parameters ~ -{link} `(string|?)` +{link} `(string|)`? {callback} `(fun(...: obsidian.ResolveLinkResult))` ------------------------------------------------------------------------------ @@ -304,8 +304,8 @@ Parameters ~ Follow a link. If the link argument is `nil` we attempt to follow a link under the cursor. Parameters ~ -{link} `(string|?)` -{opts} { open_strategy: obsidian.config.OpenStrategy|? }|? +{link} `(string|)`? +{opts} `({ open_strategy: obsidian.config.OpenStrategy|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.open_note()* @@ -314,7 +314,7 @@ Open a note in a buffer. Parameters ~ {note_or_path} `(string|obsidian.Path|obsidian.Note)` -{opts} { line: `(integer|?,)` col: integer|?, open_strategy: obsidian.config.OpenStrategy|?, sync: boolean|?, callback: fun(bufnr: integer)|? }|? +{opts} `({ line: integer|?, col: integer|?, open_strategy: obsidian.config.OpenStrategy|?, sync: boolean|?, callback: fun(bufnr: integer)|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.current_note()* @@ -322,11 +322,11 @@ Parameters ~ Get the current note from a buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.note.LoadOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -334,12 +334,12 @@ Class ~ Fields ~ {tag} `(string)` The tag found. -{note} obsidian.Note The note instance where the tag was found. +{note} `(obsidian.Note)` The note instance where the tag was found. {path} `(string|obsidian.Path)` The path to the note where the tag was found. {line} `(integer)` The line number (1-indexed) where the tag was found. {text} `(string)` The text (with whitespace stripped) of the line where the tag was found. -{tag_start} `(integer|?)` The index within 'text' where the tag starts. -{tag_end} `(integer|?)` The index within 'text' where the tag ends. +{tag_start} `(integer|? The)` index within 'text' where the tag starts. +{tag_end} `(integer|? The)` index within 'text' where the tag ends. ------------------------------------------------------------------------------ *obsidian.Client.find_tags()* @@ -348,10 +348,10 @@ Find all tags starting with the given search term(s). Parameters ~ {term} `(string|string[])` The search term. -{opts} { search: obsidian.SearchOpts|?, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts|?, timeout: integer|? }|)`? Return ~ -obsidian.TagLocation[] +`(obsidian.TagLocation[])` ------------------------------------------------------------------------------ *obsidian.Client.find_tags_async()* @@ -361,16 +361,16 @@ An async version of 'find_tags()'. Parameters ~ {term} `(string|string[])` The search term. {callback} `(fun(tags: obsidian.TagLocation[]))` -{opts} { search: obsidian.SearchOpts }|? +{opts} `({ search: obsidian.SearchOpts }|)`? ------------------------------------------------------------------------------ Class ~ {obsidian.BacklinkMatches} Fields ~ -{note} obsidian.Note The note instance where the backlinks were found. +{note} `(obsidian.Note)` The note instance where the backlinks were found. {path} `(string|obsidian.Path)` The path to the note where the backlinks were found. -{matches} obsidian.BacklinkMatch[] The backlinks within the note. +{matches} `(obsidian.BacklinkMatch[])` The backlinks within the note. ------------------------------------------------------------------------------ Class ~ @@ -386,11 +386,11 @@ Fields ~ Find all backlinks to a note. Parameters ~ -{note} obsidian.Note The note to find backlinks for. -{opts} { search: obsidian.SearchOpts|?, timeout: `(integer|?,)` anchor: string|?, block: string|? }|? +{note} `(obsidian.Note)` The note to find backlinks for. +{opts} `({ search: obsidian.SearchOpts|?, timeout: integer|?, anchor: string|?, block: string|? }|)`? Return ~ -obsidian.BacklinkMatches[] +`(obsidian.BacklinkMatches[])` ------------------------------------------------------------------------------ *obsidian.Client.find_backlinks_async()* @@ -398,9 +398,9 @@ obsidian.BacklinkMatches[] An async version of 'find_backlinks()'. Parameters ~ -{note} obsidian.Note The note to find backlinks for. +{note} `(obsidian.Note)` The note to find backlinks for. {callback} `(fun(backlinks: obsidian.BacklinkMatches[]))` -{opts} { search: obsidian.SearchOpts, anchor: `(string|?,)` block: string|? }|? +{opts} `({ search: obsidian.SearchOpts, anchor: string|?, block: string|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.list_tags()* @@ -409,8 +409,8 @@ Gather a list of all tags in the vault. If 'term' is provided, only tags that pa term will be included. Parameters ~ -{term} `(string|?)` An optional search term to match tags -{timeout} `(integer|?)` Timeout in milliseconds +{term} `(string|? An)` optional search term to match tags +{timeout} `(integer|? Timeout)` in milliseconds Return ~ `(string[])` @@ -421,7 +421,7 @@ Return ~ An async version of 'list_tags()'. Parameters ~ -{term} `(string|?)` +{term} `(string|)`? {callback} `(fun(tags: string[]))` ------------------------------------------------------------------------------ @@ -431,7 +431,7 @@ Apply a function over all notes in the current vault. Parameters ~ {on_note} `(fun(note: obsidian.Note))` -{opts} { on_done: `(fun()|?,)` timeout: integer|?, pattern: string|? }|? +{opts} `({ on_done: fun()|?, timeout: integer|?, pattern: string|? }|)`? Options: - `on_done`: A function to call when all notes have been processed. @@ -445,7 +445,7 @@ Like apply, but the callback takes a path instead of a note instance. Parameters ~ {on_path} `(fun(path: string))` -{opts} { on_done: `(fun()|?,)` timeout: integer|?, pattern: string|? }|? +{opts} `({ on_done: fun()|?, timeout: integer|?, pattern: string|? }|)`? Options: - `on_done`: A function to call when all paths have been processed. @@ -459,7 +459,7 @@ Generate a unique ID for a new note. This respects the user's `note_id_func` if otherwise falls back to generated a Zettelkasten style ID. Parameters ~ -{title} `(string|?)` +{title} `(string|)`? Return ~ `(string)` @@ -472,10 +472,10 @@ This respects the user's `note_path_func` if configured, otherwise essentially f `spec.dir / (spec.id .. ".md")`. Parameters ~ -{spec} { id: `(string,)` dir: obsidian.Path, title: string|? } +{spec} `({ id: string, dir: obsidian.Path, title: string|? })` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Client.parse_title_id_path()* @@ -483,12 +483,12 @@ obsidian.Path Parse the title, ID, and path for a new note. Parameters ~ -{title} `(string|?)` -{id} `(string|?)` -{dir} `(string|obsidian.Path|?)` +{title} `(string|)`? +{id} `(string|)`? +{dir} `(string|obsidian.Path|)`? Return ~ -`(string|)` `(optional)`,string,obsidian.Path +`(string| `(optional))``,string,obsidian.Path ------------------------------------------------------------------------------ *obsidian.Client.new_note()* @@ -497,26 +497,26 @@ Create and save a new note. Deprecated: prefer `Client:create_note()` instead. Parameters ~ -{title} `(string|?)` The title for the note. -{id} `(string|?)` An optional ID for the note. If not provided one will be generated. -{dir} `(string|obsidian.Path|?)` An optional directory to place the note. If this is a relative path it will be interpreted relative the workspace / vault root. -{aliases} `(string[]|?)` Additional aliases to assign to the note. +{title} `(string|? The)` title for the note. +{id} `(string|? An)` optional ID for the note. If not provided one will be generated. +{dir} `(string|obsidian.Path|? An)` optional directory to place the note. If this is a relative path it will be interpreted relative the workspace / vault root. +{aliases} `(string[]|? Additional)` aliases to assign to the note. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ Class ~ {obsidian.CreateNoteOpts} Fields ~ -{title} `(string|?)` -{id} `(string|?)` -{dir} `(string|obsidian.Path|?)` -{aliases} `(string[]|?)` -{tags} `(string[]|?)` -{no_write} `(boolean|?)` -{template} `(string|?)` +{title} `(string|)`? +{id} `(string|)`? +{dir} `(string|obsidian.Path|)`? +{aliases} `(string[]|)`? +{tags} `(string[]|)`? +{no_write} `(boolean|)`? +{template} `(string|)`? ------------------------------------------------------------------------------ *obsidian.Client.create_note()* @@ -524,7 +524,7 @@ Fields ~ Create a new note with the following options. Parameters ~ -{opts} obsidian.CreateNoteOpts|? Options. +{opts} `(obsidian.CreateNoteOpts|? Options.)` Options: - `title`: A title to assign the note. @@ -538,7 +538,7 @@ Options: - `template`: The name of a template to apply when writing the note to disk. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.write_note()* @@ -546,8 +546,8 @@ obsidian.Note Write the note to disk. Parameters ~ -{note} obsidian.Note -{opts} { path: `(string|obsidian.Path,)` template: string|?, update_content: (fun(lines: string[]): string[])|? }|? Options. +{note} `(obsidian.Note)` +{opts} `({ path: string|obsidian.Path, template: string|?, update_content: (fun(lines: string[]): string[])|? }|? Options.)` Options: - `path`: Override the path to write to. @@ -557,7 +557,7 @@ Options: actually be written (again excluding frontmatter). Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.write_note_to_buffer()* @@ -565,8 +565,8 @@ obsidian.Note Write the note to a buffer. Parameters ~ -{note} obsidian.Note -{opts} { bufnr: `(integer|?,)` template: string|? }|? Options. +{note} `(obsidian.Note)` +{opts} `({ bufnr: integer|?, template: string|? }|? Options.)` Options: - `bufnr`: Override the buffer to write to. Defaults to current buffer. @@ -581,8 +581,8 @@ Return ~ Update the frontmatter in a buffer for the note. Parameters ~ -{note} obsidian.Note -{bufnr} `(integer|?)` +{note} `(obsidian.Note)` +{bufnr} `(integer|)`? Return ~ `(boolean)` updated If the the frontmatter was updated. @@ -593,10 +593,10 @@ Return ~ Get the path to a daily note. Parameters ~ -{datetime} `(integer|?)` +{datetime} `(integer|)`? Return ~ -obsidian.Path, `(string)` (Path, ID) The path and ID of the note. +`(obsidian.Path)`, string (Path, ID) The path and ID of the note. ------------------------------------------------------------------------------ *obsidian.Client.today()* @@ -604,7 +604,7 @@ obsidian.Path, `(string)` (Path, ID) The path and ID of the note. Open (or create) the daily note for today. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.yesterday()* @@ -612,7 +612,7 @@ obsidian.Note Open (or create) the daily note from the last weekday. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.tomorrow()* @@ -620,7 +620,7 @@ obsidian.Note Open (or create) the daily note for the next weekday. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.daily()* @@ -628,11 +628,11 @@ obsidian.Note Open (or create) the daily note for today + `offset_days`. Parameters ~ -{offset_days} `(integer|?)` -{opts} { no_write: `(boolean|?,)` load: obsidian.note.LoadOpts|? }|? +{offset_days} `(integer|)`? +{opts} `({ no_write: boolean|?, load: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.update_ui()* @@ -640,7 +640,7 @@ obsidian.Note Manually update extmarks in a buffer. Parameters ~ -{bufnr} `(integer|?)` +{bufnr} `(integer|)`? ------------------------------------------------------------------------------ *obsidian.Client.format_link()* @@ -649,7 +649,7 @@ Create a formatted markdown / wiki link for a note. Parameters ~ {note} `(obsidian.Note|obsidian.Path|string)` The note/path to link to. -{opts} { label: `(string|?,)` link_style: obsidian.config.LinkStyle|?, id: string|integer|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? Options. +{opts} `({ label: string|?, link_style: obsidian.config.LinkStyle|?, id: string|integer|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? Options.)` Return ~ `(string)` @@ -660,10 +660,10 @@ Return ~ Get the Picker. Parameters ~ -{picker_name} obsidian.config.Picker|? +{picker_name} `(obsidian.config.Picker|)`? Return ~ -obsidian.Picker| `(optional)` +`(obsidian.Picker| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -674,7 +674,7 @@ Fields ~ {header} `(string)` {level} `(integer)` {line} `(integer)` -{parent} obsidian.note.HeaderAnchor|? +{parent} `(obsidian.note.HeaderAnchor|)`? ------------------------------------------------------------------------------ Class ~ @@ -696,17 +696,17 @@ Class ~ Fields ~ {id} `(string|integer)` {aliases} `(string[])` -{title} `(string|?)` +{title} `(string|)`? {tags} `(string[])` -{path} obsidian.Path|? -{metadata} `(table|?)` -{has_frontmatter} `(boolean|?)` -{frontmatter_end_line} `(integer|?)` -{contents} `(string[]|?)` -{anchor_links} `(table|?)` +{path} `(obsidian.Path|)`? +{metadata} `(table|)`? +{has_frontmatter} `(boolean|)`? +{frontmatter_end_line} `(integer|)`? +{contents} `(string[]|)`? +{anchor_links} `(table|)`? {blocks} `(table?)` -{alt_alias} `(string|?)` -{bufnr} `(integer|?)` +{alt_alias} `(string|)`? +{bufnr} `(integer|)`? ------------------------------------------------------------------------------ *obsidian.Note.new()* @@ -719,10 +719,10 @@ Parameters ~ {id} `(string|number)` {aliases} `(string[])` {tags} `(string[])` -{path} `(string|obsidian.Path|?)` +{path} `(string|obsidian.Path|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.display_info()* @@ -730,7 +730,7 @@ obsidian.Note Get markdown display info about the note. Parameters ~ -{opts} { label: `(string|?,)` anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? +{opts} `({ label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|)`? Return ~ `(string)` @@ -749,7 +749,7 @@ Return ~ Get the filename associated with the note. Return ~ -`(string|)` `(optional)` +`(string| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Note.reference_ids()* @@ -757,7 +757,7 @@ Return ~ Get a list of all of the different string that can identify this note via references, including the ID, aliases, and filename. Parameters ~ -{opts} { lowercase: `(boolean|?)` }|? +{opts} `({ lowercase: boolean|? }|)`? Return ~ `(string[])` @@ -829,10 +829,10 @@ Return ~ Class ~ {obsidian.note.LoadOpts} Fields ~ -{max_lines} `(integer|?)` -{load_contents} `(boolean|?)` -{collect_anchor_links} `(boolean|?)` -{collect_blocks} `(boolean|?)` +{max_lines} `(integer|)`? +{load_contents} `(boolean|)`? +{collect_anchor_links} `(boolean|)`? +{collect_blocks} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.Note.from_file()* @@ -841,10 +841,10 @@ Initialize a note from a file. Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.from_file_async()* @@ -853,10 +853,10 @@ An async version of `.from_file()`, i.e. it needs to be called in an async conte Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.from_file_with_contents_async()* @@ -865,10 +865,10 @@ Like `.from_file_async()` but also returns the contents of the file as a list of Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -`(obsidian.Note,string[])` +`(obsidian.Note)`,string[] ------------------------------------------------------------------------------ *obsidian.Note.from_buffer()* @@ -876,11 +876,11 @@ Return ~ Initialize a note from a buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.note.LoadOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.display_name()* @@ -898,10 +898,10 @@ Initialize a note from an iterator of lines. Parameters ~ {lines} `(fun(): string|?)` {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.frontmatter()* @@ -917,8 +917,8 @@ Return ~ Get frontmatter lines that can be written to a buffer. Parameters ~ -{eol} `(boolean|?)` -{frontmatter} `(table|?)` +{eol} `(boolean|)`? +{frontmatter} `(table|)`? Return ~ `(string[])` @@ -931,7 +931,7 @@ In general this only updates the frontmatter and header, leaving the rest of the unless you use the `update_content()` callback. Parameters ~ -{opts} { path: `(string|obsidian.Path|?,)` insert_frontmatter: boolean|?, frontmatter: table|?, update_content: (fun(lines: string[]): string[])|? }|? Options. +{opts} `({ path: string|obsidian.Path|?, insert_frontmatter: boolean|?, frontmatter: table|?, update_content: (fun(lines: string[]): string[])|? }|? Options.)` Options: - `path`: Specify a path to save to. Defaults to `self.path`. @@ -947,7 +947,7 @@ Options: Save frontmatter to the given buffer. Parameters ~ -{opts} { bufnr: `(integer|?,)` insert_frontmatter: boolean|?, frontmatter: table|? }|? Options. +{opts} `({ bufnr: integer|?, insert_frontmatter: boolean|?, frontmatter: table|? }|? Options.)` Return ~ `(boolean)` updated True if the buffer lines were updated, false otherwise. @@ -960,7 +960,7 @@ Try to resolve an anchor link to a line number in the note's file. Parameters ~ {anchor_link} `(string)` Return ~ -obsidian.note.HeaderAnchor| `(optional)` +`(obsidian.note.HeaderAnchor| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Note.resolve_block()* @@ -971,26 +971,26 @@ Parameters ~ {block_id} `(string)` Return ~ -obsidian.note.Block| `(optional)` +`(obsidian.note.Block| `(optional))`` ------------------------------------------------------------------------------ Class ~ {obsidian.workspace.WorkspaceSpec} Fields ~ -{path} `(string|obsidian.Path|(fun():)` string|obsidian.Path) -{name} `(string|?)` -{strict} `(boolean|?)` If true, the workspace root will be fixed to 'path' instead of the vault root (if different). -{overrides} `(table|obsidian.config.ClientOpts|?)` +{path} `(string|obsidian.Path|(fun(): string|obsidian.Path))` +{name} `(string|)`? +{strict} `(boolean|? If)` true, the workspace root will be fixed to 'path' instead of the vault root (if different). +{overrides} `(table|obsidian.config.ClientOpts|)`? ------------------------------------------------------------------------------ Class ~ {obsidian.workspace.WorkspaceOpts} Fields ~ -{name} `(string|?)` -{strict} `(boolean|?)` If true, the workspace root will be fixed to 'path' instead of the vault root (if different). -{overrides} `(table|obsidian.config.ClientOpts|?)` +{name} `(string|)`? +{strict} `(boolean|? If)` true, the workspace root will be fixed to 'path' instead of the vault root (if different). +{overrides} `(table|obsidian.config.ClientOpts|)`? ------------------------------------------------------------------------------ *obsidian.Workspace* @@ -1006,10 +1006,10 @@ Class ~ Fields ~ {name} `(string)` An arbitrary name for the workspace. -{path} obsidian.Path The normalized path to the workspace. -{root} obsidian.Path The normalized path to the vault root of the workspace. This usually matches 'path'. -{overrides} `(table|obsidian.config.ClientOpts|?)` -{locked} `(boolean|?)` +{path} `(obsidian.Path)` The normalized path to the workspace. +{root} `(obsidian.Path)` The normalized path to the vault root of the workspace. This usually matches 'path'. +{overrides} `(table|obsidian.config.ClientOpts|)`? +{locked} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.find_vault_root()* @@ -1023,7 +1023,7 @@ Parameters ~ {base_dir} `(string|obsidian.Path)` Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.new()* @@ -1032,10 +1032,10 @@ Create a new 'Workspace' object. This assumes the workspace already exists on th Parameters ~ {path} `(string|obsidian.Path)` Workspace path. -{opts} obsidian.workspace.WorkspaceOpts|? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_spec()* @@ -1043,10 +1043,10 @@ obsidian.Workspace Initialize a new 'Workspace' object from a workspace spec. Parameters ~ -{spec} obsidian.workspace.WorkspaceSpec +{spec} `(obsidian.workspace.WorkspaceSpec)` Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_cwd()* @@ -1054,10 +1054,10 @@ obsidian.Workspace Initialize a 'Workspace' object from the current working directory. Parameters ~ -{opts} obsidian.workspace.WorkspaceOpts|? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_buf()* @@ -1065,11 +1065,11 @@ obsidian.Workspace Initialize a 'Workspace' object from the parent directory of the current buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.workspace.WorkspaceOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.lock()* @@ -1089,10 +1089,10 @@ is one. Parameters ~ {cur_dir} `(string|obsidian.Path)` -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.get_workspace_for_cwd()* @@ -1101,10 +1101,10 @@ Get the workspace corresponding to the current working directory (or a parent of is one. Parameters ~ -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.get_default_workspace()* @@ -1112,7 +1112,7 @@ obsidian.Workspace| `(optional)` Returns the default workspace. Parameters ~ -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ `(obsidian.Workspace|nil)` @@ -1123,10 +1123,10 @@ Return ~ Resolves current workspace from the client config. Parameters ~ -{opts} obsidian.config.ClientOpts +{opts} `(obsidian.config.ClientOpts)` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.cached_get()* @@ -1147,10 +1147,10 @@ Class ~ Fields ~ {filename} `(string)` The underlying filename as a string. -{name} `(string|?)` The final path component, if any. -{suffix} `(string|?)` The final extension of the path, if any. +{name} `(string|? The)` final path component, if any. +{suffix} `(string|? The)` final extension of the path, if any. {suffixes} `(string[])` A list of all of the path's extensions. -{stem} `(string|?)` The final path component, without its suffix. +{stem} `(string|? The)` final path component, without its suffix. ------------------------------------------------------------------------------ *obsidian.Path.is_path_obj()* @@ -1176,7 +1176,7 @@ Parameters ~ {...} `(string|obsidian.Path)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.temp()* @@ -1184,10 +1184,10 @@ obsidian.Path Get a temporary path with a unique name. Parameters ~ -{opts} { suffix: `(string|?)` }|? +{opts} `({ suffix: string|? }|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.cwd()* @@ -1195,7 +1195,7 @@ obsidian.Path Get a path corresponding to the current working directory as given by `vim.loop.cwd()`. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.buffer()* @@ -1203,10 +1203,10 @@ obsidian.Path Get a path corresponding to a buffer. Parameters ~ -{bufnr} `(integer|?)` The buffer number or `0` / `nil` for the current buffer. +{bufnr} `(integer|? The)` buffer number or `0` / `nil` for the current buffer. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.buf_dir()* @@ -1214,10 +1214,10 @@ obsidian.Path Get a path corresponding to the parent of a buffer. Parameters ~ -{bufnr} `(integer|?)` The buffer number or `0` / `nil` for the current buffer. +{bufnr} `(integer|? The)` buffer number or `0` / `nil` for the current buffer. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ @@ -1232,7 +1232,7 @@ Parameters ~ {suffix} `(string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.is_absolute()* @@ -1248,7 +1248,7 @@ Return ~ Parameters ~ {...} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.relative_to()* @@ -1260,7 +1260,7 @@ Parameters ~ {other} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.parent()* @@ -1268,7 +1268,7 @@ obsidian.Path The logical parent of the path. Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Path.parents()* @@ -1276,7 +1276,7 @@ obsidian.Path| `(optional)` Get a list of the parent directories. Return ~ -obsidian.Path[] +`(obsidian.Path[])` ------------------------------------------------------------------------------ *obsidian.Path.is_parent_of()* @@ -1302,10 +1302,10 @@ Make the path absolute, resolving any symlinks. If `strict` is true and the path doesn't exist, an error is raised. Parameters ~ -{opts} { strict: `(boolean)` }|? +{opts} `({ strict: boolean }|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.stat()* @@ -1313,7 +1313,7 @@ obsidian.Path Get OS stat results. Return ~ -`(table|)` `(optional)` +`(table| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Path.exists()* @@ -1345,7 +1345,7 @@ Return ~ Create a new directory at the given path. Parameters ~ -{opts} { mode: `(integer|?,)` parents: boolean|?, exist_ok: boolean|? }|? +{opts} `({ mode: integer|?, parents: boolean|?, exist_ok: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Path.rmdir()* @@ -1363,7 +1363,7 @@ Recursively remove an entire directory and its contents. Create a file at this given path. Parameters ~ -{opts} { mode: `(integer|?,)` exist_ok: boolean|? }|? +{opts} `({ mode: integer|?, exist_ok: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Path.rename()* @@ -1374,7 +1374,7 @@ Parameters ~ {target} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.unlink()* @@ -1382,6 +1382,6 @@ obsidian.Path Remove the file. Parameters ~ -{opts} { missing_ok: `(boolean|?)` }|? +{opts} `({ missing_ok: boolean|? }|)`? vim:tw=78:ts=8:noet:ft=help:norl: \ No newline at end of file diff --git a/lua/cmp_obsidian.lua b/lua/cmp_obsidian.lua deleted file mode 100644 index 9d940585..00000000 --- a/lua/cmp_obsidian.lua +++ /dev/null @@ -1,309 +0,0 @@ -local abc = require "obsidian.abc" -local completion = require "obsidian.completion.refs" -local obsidian = require "obsidian" -local util = require "obsidian.util" -local iter = require("obsidian.itertools").iter -local LinkStyle = require("obsidian.config").LinkStyle - ----@class cmp_obsidian.CompletionItem ----@field label string ----@field new_text string ----@field sort_text string ----@field documentation table|? - ----@class cmp_obsidian.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - -source.complete = function(_, request, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request) - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - callback { isIncomplete = true } - return - end - - local in_buffer_only = false - - ---@type string|? - local block_link - search, block_link = util.strip_block_links(search) - - ---@type string|? - local anchor_link - search, anchor_link = util.strip_anchor_links(search) - - -- If block link is incomplete, we'll match against all block links. - if not block_link and vim.endswith(search, "#^") then - block_link = "#^" - search = string.sub(search, 1, -3) - end - - -- If anchor link is incomplete, we'll match against all anchor links. - if not anchor_link and vim.endswith(search, "#") then - anchor_link = "#" - search = string.sub(search, 1, -2) - end - - if (anchor_link or block_link) and string.len(search) == 0 then - -- Search over headers/blocks in current buffer only. - in_buffer_only = true - end - - ---@param results obsidian.Note[] - local function search_callback(results) - -- Completion items. - local items = {} - - ---@type table - local new_text_to_option = {} - - for note in iter(results) do - ---@cast note obsidian.Note - - -- Collect matching block links. - ---@type obsidian.note.Block[]|? - local matching_blocks - if block_link then - assert(note.blocks) - matching_blocks = {} - for block_id, block_data in pairs(note.blocks) do - if vim.startswith("#" .. block_id, block_link) then - table.insert(matching_blocks, block_data) - end - end - - if #matching_blocks == 0 then - -- Unmatched, create a mock one. - table.insert(matching_blocks, { id = util.standardize_block(block_link), line = 1 }) - end - end - - -- Collect matching anchor links. - ---@type obsidian.note.HeaderAnchor[]|? - local matching_anchors - if anchor_link then - assert(note.anchor_links) - matching_anchors = {} - for anchor, anchor_data in pairs(note.anchor_links) do - if vim.startswith(anchor, anchor_link) then - table.insert(matching_anchors, anchor_data) - end - end - - if #matching_anchors == 0 then - -- Unmatched, create a mock one. - table.insert( - matching_anchors, - { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } - ) - end - end - - ---@param label string|? - ---@param alt_label string|? - local function update_completion_options(label, alt_label) - ---@type { label: string|?, alt_label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }[] - local new_options = {} - if matching_anchors ~= nil then - for anchor in iter(matching_anchors) do - table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor }) - end - elseif matching_blocks ~= nil then - for block in iter(matching_blocks) do - table.insert(new_options, { label = label, alt_label = alt_label, block = block }) - end - else - if label then - table.insert(new_options, { label = label, alt_label = alt_label }) - end - - -- Add all blocks and anchors, let cmp sort it out. - for _, anchor_data in pairs(note.anchor_links or {}) do - table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor_data }) - end - for _, block_data in pairs(note.blocks or {}) do - table.insert(new_options, { label = label, alt_label = alt_label, block = block_data }) - end - end - - -- De-duplicate options relative to their `new_text`. - for _, option in ipairs(new_options) do - ---@type obsidian.config.LinkStyle - local link_style - if ref_type == completion.RefType.Wiki then - link_style = LinkStyle.wiki - elseif ref_type == completion.RefType.Markdown then - link_style = LinkStyle.markdown - else - error "not implemented" - end - - ---@type string, string, string, table|? - local final_label, sort_text, new_text, documentation - if option.label then - new_text = client:format_link( - note, - { label = option.label, link_style = link_style, anchor = option.anchor, block = option.block } - ) - - final_label = assert(option.alt_label or option.label) - if option.anchor then - final_label = final_label .. option.anchor.anchor - elseif option.block then - final_label = final_label .. "#" .. option.block.id - end - sort_text = final_label - - documentation = { - kind = "markdown", - value = note:display_info { - label = new_text, - anchor = option.anchor, - block = option.block, - }, - } - elseif option.anchor then - -- In buffer anchor link. - -- TODO: allow users to customize this? - if ref_type == completion.RefType.Wiki then - new_text = "[[#" .. option.anchor.header .. "]]" - elseif ref_type == completion.RefType.Markdown then - new_text = "[#" .. option.anchor.header .. "](" .. option.anchor.anchor .. ")" - else - error "not implemented" - end - - final_label = option.anchor.anchor - sort_text = final_label - - documentation = { - kind = "markdown", - value = string.format("`%s`", new_text), - } - elseif option.block then - -- In buffer block link. - -- TODO: allow users to customize this? - if ref_type == completion.RefType.Wiki then - new_text = "[[#" .. option.block.id .. "]]" - elseif ref_type == completion.RefType.Markdown then - new_text = "[#" .. option.block.id .. "](#" .. option.block.id .. ")" - else - error "not implemented" - end - - final_label = "#" .. option.block.id - sort_text = final_label - - documentation = { - kind = "markdown", - value = string.format("`%s`", new_text), - } - else - error "should not happen" - end - - if new_text_to_option[new_text] then - new_text_to_option[new_text].sort_text = new_text_to_option[new_text].sort_text .. " " .. sort_text - else - new_text_to_option[new_text] = - { label = final_label, new_text = new_text, sort_text = sort_text, documentation = documentation } - end - end - end - - if in_buffer_only then - update_completion_options() - else - -- Collect all valid aliases for the note, including ID, title, and filename. - ---@type string[] - local aliases - if not in_buffer_only then - aliases = util.tbl_unique { tostring(note.id), note:display_name(), unpack(note.aliases) } - if note.title ~= nil then - table.insert(aliases, note.title) - end - end - - for alias in iter(aliases) do - update_completion_options(alias) - local alias_case_matched = util.match_case(search, alias) - - if - alias_case_matched ~= nil - and alias_case_matched ~= alias - and not util.tbl_contains(note.aliases, alias_case_matched) - then - update_completion_options(alias_case_matched) - end - end - - if note.alt_alias ~= nil then - update_completion_options(note:display_name(), note.alt_alias) - end - end - end - - for _, option in pairs(new_text_to_option) do - -- TODO: need a better label, maybe just the note's display name? - ---@type string - local label - if ref_type == completion.RefType.Wiki then - label = string.format("[[%s]]", option.label) - elseif ref_type == completion.RefType.Markdown then - label = string.format("[%s](…)", option.label) - else - error "not implemented" - end - - table.insert(items, { - documentation = option.documentation, - sortText = option.sort_text, - label = label, - kind = 18, -- "Reference" - textEdit = { - newText = option.new_text, - range = { - start = { - line = request.context.cursor.row - 1, - character = insert_start, - }, - ["end"] = { - line = request.context.cursor.row - 1, - character = insert_end, - }, - }, - }, - }) - end - - callback { - items = items, - isIncomplete = true, - } - end - - if in_buffer_only then - local note = client:current_note(0, { collect_anchor_links = true, collect_blocks = true }) - if note then - search_callback { note } - else - callback { isIncomplete = true } - end - else - client:find_notes_async(search, search_callback, { - search = { ignore_case = true }, - notes = { collect_anchor_links = anchor_link ~= nil, collect_blocks = block_link ~= nil }, - }) - end -end - -return source diff --git a/lua/cmp_obsidian_new.lua b/lua/cmp_obsidian_new.lua deleted file mode 100644 index 2889b0d3..00000000 --- a/lua/cmp_obsidian_new.lua +++ /dev/null @@ -1,167 +0,0 @@ -local abc = require "obsidian.abc" -local completion = require "obsidian.completion.refs" -local obsidian = require "obsidian" -local util = require "obsidian.util" -local LinkStyle = require("obsidian.config").LinkStyle - ----@class cmp_obsidian_new.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - -source.complete = function(_, request, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request) - - if search ~= nil then - search = util.lstrip_whitespace(search) - end - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - callback { isIncomplete = true } - return - end - - ---@type string|? - local block_link - search, block_link = util.strip_block_links(search) - - ---@type string|? - local anchor_link - search, anchor_link = util.strip_anchor_links(search) - - -- If block link is incomplete, do nothing. - if not block_link and vim.endswith(search, "#^") then - callback { isIncomplete = true } - return - end - - -- If anchor link is incomplete, do nothing. - if not anchor_link and vim.endswith(search, "#") then - callback { isIncomplete = true } - return - end - - -- Probably just a block/anchor link within current note. - if string.len(search) == 0 then - callback { isIncomplete = false } - return - end - - -- Create a mock block. - ---@type obsidian.note.Block|? - local block - if block_link then - block = { block = "", id = util.standardize_block(block_link), line = 1 } - end - - -- Create a mock anchor. - ---@type obsidian.note.HeaderAnchor|? - local anchor - if anchor_link then - anchor = { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } - end - - ---@type { label: string, note: obsidian.Note, template: string|? }[] - local new_notes_opts = {} - - local note = client:create_note { title = search, no_write = true } - if note.title and string.len(note.title) > 0 then - new_notes_opts[#new_notes_opts + 1] = { label = search, note = note } - end - - -- Check for datetime macros. - for _, dt_offset in ipairs(util.resolve_date_macro(search)) do - if dt_offset.cadence == "daily" then - note = client:daily(dt_offset.offset, { no_write = true }) - if not note:exists() then - new_notes_opts[#new_notes_opts + 1] = - { label = dt_offset.macro, note = note, template = client.opts.daily_notes.template } - end - end - end - - -- Completion items. - local items = {} - - for _, new_note_opts in ipairs(new_notes_opts) do - local new_note = new_note_opts.note - - assert(new_note.path) - - ---@type obsidian.config.LinkStyle, string - local link_style, label - if ref_type == completion.RefType.Wiki then - link_style = LinkStyle.wiki - label = string.format("[[%s]] (create)", new_note_opts.label) - elseif ref_type == completion.RefType.Markdown then - link_style = LinkStyle.markdown - label = string.format("[%s](…) (create)", new_note_opts.label) - else - error "not implemented" - end - - local new_text = client:format_link(new_note, { link_style = link_style, anchor = anchor, block = block }) - local documentation = { - kind = "markdown", - value = new_note:display_info { - label = "Create: " .. new_text, - }, - } - - items[#items + 1] = { - documentation = documentation, - sortText = new_note_opts.label, - label = label, - kind = 18, - textEdit = { - newText = new_text, - range = { - start = { - line = request.context.cursor.row - 1, - character = insert_start, - }, - ["end"] = { - line = request.context.cursor.row - 1, - character = insert_end, - }, - }, - }, - data = { - note = new_note, - template = new_note_opts.template, - }, - } - end - - return callback { - items = items, - isIncomplete = true, - } -end - -source.execute = function(_, item, callback) - local Note = require "obsidian.note" - local Path = require "obsidian.path" - - local client = assert(obsidian.get_client()) - local data = item.data - - -- Make sure `data.note` is actually an `obsidian.Note` object. If it gets serialized at some - -- point (seems to happen on Linux), it will lose its metatable. - if not Note.is_note_obj(data.note) then - data.note = setmetatable(data.note, Note.mt) - data.note.path = setmetatable(data.note.path, Path.mt) - end - - client:write_note(data.note, { template = data.template }) - return callback {} -end - -return source diff --git a/lua/cmp_obsidian_tags.lua b/lua/cmp_obsidian_tags.lua deleted file mode 100644 index 4033f227..00000000 --- a/lua/cmp_obsidian_tags.lua +++ /dev/null @@ -1,67 +0,0 @@ -local abc = require "obsidian.abc" -local obsidian = require "obsidian" -local completion = require "obsidian.completion.tags" -local util = require "obsidian.util" -local iter = require("obsidian.itertools").iter - ----@class cmp_obsidian_tags.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - -source.complete = function(_, request, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, in_frontmatter = completion.can_complete(request) - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - return callback { isIncomplete = true } - end - - client:find_tags_async(search, function(tag_locs) - local tags = {} - for tag_loc in iter(tag_locs) do - tags[tag_loc.tag] = true - end - - local items = {} - for tag, _ in pairs(tags) do - items[#items + 1] = { - sortText = "#" .. tag, - label = "Tag: #" .. tag, - kind = 1, -- "Text" - insertText = "#" .. tag, - data = { - bufnr = request.context.bufnr, - in_frontmatter = in_frontmatter, - line = request.context.cursor.line, - tag = tag, - }, - } - end - - return callback { - items = items, - isIncomplete = false, - } - end, { search = { sort = false } }) -end - -source.execute = function(_, item, callback) - if item.data.in_frontmatter then - -- Remove the '#' at the start of the tag. - -- TODO: ideally we should be able to do this by specifying the completion item in the right way, - -- but I haven't figured out how to do that. - local line = vim.api.nvim_buf_get_lines(item.data.bufnr, item.data.line, item.data.line + 1, true)[1] - line = util.string_replace(line, "#" .. item.data.tag, item.data.tag, 1) - vim.api.nvim_buf_set_lines(item.data.bufnr, item.data.line, item.data.line + 1, true, { line }) - end - return callback {} -end - -return source diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 00c09c77..8e11adbd 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -125,7 +125,8 @@ Client.set_workspace = function(self, workspace, opts) self.callback_manager = CallbackManager.new(self, self.opts.callbacks) -- Setup UI add-ons. - if self.opts.ui.enable then + local has_no_renderer = not (util.get_plugin_info "render-markdown.nvim" or util.get_plugin_info "markview.nvim") + if has_no_renderer and self.opts.ui.enable then require("obsidian.ui").setup(self.current_workspace, self.opts.ui) end @@ -265,7 +266,7 @@ Client.templates_dir = function(self, workspace) return nil end - local paths_to_check = { Path.new(opts.templates.folder), self:vault_root(workspace) / opts.templates.folder } + local paths_to_check = { self:vault_root(workspace) / opts.templates.folder, Path.new(opts.templates.folder) } for _, path in ipairs(paths_to_check) do if path:is_dir() then return path @@ -1875,6 +1876,7 @@ Client.write_note_to_buffer = function(self, note, opts) if opts.template and util.buffer_is_empty(opts.bufnr) then note = insert_template { + note = note, template_name = opts.template, client = self, location = util.get_active_window_cursor_location(), diff --git a/lua/obsidian/commands/debug.lua b/lua/obsidian/commands/debug.lua index 3013bc38..2e5a8bbd 100644 --- a/lua/obsidian/commands/debug.lua +++ b/lua/obsidian/commands/debug.lua @@ -5,7 +5,7 @@ local util = require "obsidian.util" local VERSION = require "obsidian.version" ---@return { available: boolean, refs: boolean|?, tags: boolean|?, new: boolean|?, sources: string[]|? } -local function check_completion() +local function check_completion_with_nvim_cmp() local ok, cmp = pcall(require, "cmp") if not ok then return { available = false } @@ -30,6 +30,28 @@ local function check_completion() return { available = true, refs = cmp_refs, tags = cmp_tags, new = cmp_new, sources = sources } end +---@return { available: boolean, refs: boolean|?, tags: boolean|?, new: boolean|?, sources: string[]|? } +local function check_completion_with_blink() + local require_ok, blink_sources_lib = pcall(require, "blink.cmp.sources.lib") + if not require_ok then + return { available = false } + end + + local cmp_refs = pcall(blink_sources_lib.get_provider_by_id, "obsidian") + local cmp_tags = pcall(blink_sources_lib.get_provider_by_id, "obsidian_tags") + local cmp_new = pcall(blink_sources_lib.get_provider_by_id, "obsidian_new") + + local sources = {} + local get_providers_ok, providers = pcall(blink_sources_lib.get_all_providers) + if get_providers_ok then + vim.tbl_map(function(provider) + table.insert(sources, provider.name) + end, providers) + end + + return { available = true, refs = cmp_refs ~= nil, tags = cmp_tags, new = cmp_new, sources = sources } +end + ---@param client obsidian.Client return function(client, data) data = data or {} @@ -51,7 +73,16 @@ return function(client, data) end log.lazy_info "Dependencies:" - for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick" } do + + for _, plugin in ipairs { + "plenary.nvim", + "nvim-cmp", + "blink.cmp", + "telescope.nvim", + "fzf-lua", + "mini.pick", + "snacks.pick", + } do local plugin_info = util.get_plugin_info(plugin) if plugin_info ~= nil then log.lazy_info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") @@ -62,18 +93,37 @@ return function(client, data) log.lazy_info(" ✓ picker: %s", client:picker()) if client.opts.completion.nvim_cmp then - local cmp_status = check_completion() - if cmp_status.available then + local nvim_cmp_status = check_completion_with_nvim_cmp() + if nvim_cmp_status.available then log.lazy_info( " ✓ completion: enabled (nvim-cmp) %s refs, %s tags, %s new", - cmp_status.refs and "✓" or "✗", - cmp_status.tags and "✓" or "✗", - cmp_status.new and "✓" or "✗" + nvim_cmp_status.refs and "✓" or "✗", + nvim_cmp_status.tags and "✓" or "✗", + nvim_cmp_status.new and "✓" or "✗" + ) + + if nvim_cmp_status.sources then + log.lazy_info " all sources:" + for _, source in ipairs(nvim_cmp_status.sources) do + log.lazy_info(" • %s", source) + end + end + else + log.lazy_info " ✓ completion: unavailable" + end + elseif client.opts.completion.blink then + local blink_status = check_completion_with_blink() + if blink_status.available then + log.lazy_info( + " ✓ completion: enabled (blink) %s refs, %s tags, %s new", + blink_status.refs and "✓" or "✗", + blink_status.tags and "✓" or "✗", + blink_status.new and "✓" or "✗" ) - if cmp_status.sources then + if blink_status.sources then log.lazy_info " all sources:" - for _, source in ipairs(cmp_status.sources) do + for _, source in ipairs(blink_status.sources) do log.lazy_info(" • %s", source) end end diff --git a/lua/obsidian/completion/plugin_initializers/blink.lua b/lua/obsidian/completion/plugin_initializers/blink.lua new file mode 100644 index 00000000..5d01886f --- /dev/null +++ b/lua/obsidian/completion/plugin_initializers/blink.lua @@ -0,0 +1,190 @@ +local util = require "obsidian.util" +local obsidian = require "obsidian" + +local M = {} + +M.injected_once = false + +M.providers = { + { name = "obsidian", module = "obsidian.completion.sources.blink.refs" }, + { name = "obsidian_tags", module = "obsidian.completion.sources.blink.tags" }, + { name = "obsidian_new", module = "obsidian.completion.sources.blink.new" }, +} + +local function add_provider(blink, provider_name, proivder_module) + local add_source_provider = blink.add_source_provider or blink.add_provider + add_source_provider(provider_name, { + name = provider_name, + module = proivder_module, + async = true, + opts = {}, + enabled = function() + -- Enable only in markdown buffers. + return vim.tbl_contains({ "markdown" }, vim.bo.filetype) + and vim.bo.buftype ~= "prompt" + and vim.b.completion ~= false + end, + }) +end + +-- Ran once on the plugin startup +function M.register_providers() + local blink = require "blink.cmp" + + for _, provider in pairs(M.providers) do + add_provider(blink, provider.name, provider.module) + end +end + +local function add_element_to_list_if_not_exists(list, element) + if not vim.tbl_contains(list, element) then + table.insert(list, 1, element) + end +end + +local function should_return_if_not_in_workspace() + local current_file_path = vim.api.nvim_buf_get_name(0) + local buf_dir = vim.fs.dirname(current_file_path) + + local obsidian_client = assert(obsidian.get_client()) + local workspace = obsidian.Workspace.get_workspace_for_dir(buf_dir, obsidian_client.opts.workspaces) + if not workspace then + return true + else + return false + end +end + +local function log_unexpected_type(config_path, unexpected_type, expected_type) + vim.notify( + "blink.cmp's `" + .. config_path + .. "` configuration appears to be an '" + .. unexpected_type + .. "' type, but it " + .. "should be '" + .. expected_type + .. "'. Obsidian won't update this configuration, and " + .. "completion won't work with blink.cmp", + vim.log.levels.ERROR + ) +end + +---Attempts to inject the Obsidian sources into per_filetype if that's what the user seems to use for markdown +---@param blink_sources_per_filetype table +---@return boolean true if it obsidian sources were injected into the sources.per_filetype +local function try_inject_blink_sources_into_per_filetype(blink_sources_per_filetype) + -- If the per_filetype is an empty object, then it's probably not utilized by the user + if vim.deep_equal(blink_sources_per_filetype, {}) then + return false + end + + local markdown_config = blink_sources_per_filetype["markdown"] + + -- If the markdown key is not used, then per_filetype it's probably not utilized by the user + if markdown_config == nil then + return false + end + + local markdown_config_type = type(markdown_config) + if markdown_config_type == "table" and util.tbl_is_array(markdown_config) then + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(markdown_config, provider.name) + end + return true + elseif markdown_config_type == "function" then + local original_func = markdown_config + markdown_config = function() + local original_results = original_func() + + if should_return_if_not_in_workspace() then + return original_results + end + + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(original_results, provider.name) + end + return original_results + end + + -- Overwrite the original config function with the newly generated one + require("blink.cmp.config").sources.per_filetype["markdown"] = markdown_config + return true + else + log_unexpected_type( + ".sources.per_filetype['markdown']", + markdown_config_type, + "a list or a function that returns a list of sources" + ) + return true -- logged the error, returns as if this was successful to avoid further errors + end +end + +---Attempts to inject the Obsidian sources into default if that's what the user seems to use for markdown +---@param blink_sources_default (fun():string[])|(string[]) +---@return boolean true if it obsidian sources were injected into the sources.default +local function try_inject_blink_sources_into_default(blink_sources_default) + local blink_default_type = type(blink_sources_default) + if blink_default_type == "function" then + local original_func = blink_sources_default + blink_sources_default = function() + local original_results = original_func() + + if should_return_if_not_in_workspace() then + return original_results + end + + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(original_results, provider.name) + end + return original_results + end + + -- Overwrite the original config function with the newly generated one + require("blink.cmp.config").sources.default = blink_sources_default + return true + elseif blink_default_type == "table" and util.tbl_is_array(blink_sources_default) then + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(blink_sources_default, provider.name) + end + + return true + elseif blink_default_type == "table" then + log_unexpected_type(".sources.default", blink_default_type, "a list") + return true -- logged the error, returns as if this was successful to avoid further errors + else + log_unexpected_type(".sources.default", blink_default_type, "a list or a function that returns a list") + return true -- logged the error, returns as if this was successful to avoid further errors + end +end + +-- Triggered for each opened markdown buffer that's in a workspace. nvm_cmp had the capability to configure the sources +-- per buffer, but blink.cmp doesn't have that capability. Instead, we have to inject the sources into the global +-- configuration and set a boolean on the module to return early the next time this function is called. +-- +-- In-case the user used functions to configure their sources, the completion will properly work just for the markdown +-- files that are in a workspace. Otherwise, the completion will work for all markdown files. +function M.inject_sources() + if M.injected_once then + return + end + + M.injected_once = true + + local blink_config = require "blink.cmp.config" + -- 'per_filetype' sources has priority over 'default' sources. + -- 'per_filetype' can be a table or a function which returns a table (["filetype"] = { "a", "b" }) + -- 'per_filetype' has the default value of {} (even if it's not configured by the user) + local blink_sources_per_filetype = blink_config.sources.per_filetype + if try_inject_blink_sources_into_per_filetype(blink_sources_per_filetype) then + return + end + + -- 'default' can be a list/array or a function which returns a list/array ({ "a", "b"}) + local blink_sources_default = blink_config.sources["default"] + if try_inject_blink_sources_into_default(blink_sources_default) then + return + end +end + +return M diff --git a/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua b/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua new file mode 100644 index 00000000..a5c75c7a --- /dev/null +++ b/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua @@ -0,0 +1,30 @@ +local M = {} + +-- Ran once on the plugin startup +function M.register_sources() + local cmp = require "cmp" + + cmp.register_source("obsidian", require("obsidian.completion.sources.nvim_cmp.refs").new()) + cmp.register_source("obsidian_new", require("obsidian.completion.sources.nvim_cmp.new").new()) + cmp.register_source("obsidian_tags", require("obsidian.completion.sources.nvim_cmp.tags").new()) +end + +-- Triggered for each opened markdown buffer that's in a workspace and configures nvim_cmp sources for the current buffer. +function M.inject_sources() + local cmp = require "cmp" + + local sources = { + { name = "obsidian" }, + { name = "obsidian_new" }, + { name = "obsidian_tags" }, + } + for _, source in pairs(cmp.get_config().sources) do + if source.name ~= "obsidian" and source.name ~= "obsidian_new" and source.name ~= "obsidian_tags" then + table.insert(sources, source) + end + end + ---@diagnostic disable-next-line: missing-fields + cmp.setup.buffer { sources = sources } +end + +return M diff --git a/lua/obsidian/completion/sources/base/new.lua b/lua/obsidian/completion/sources/base/new.lua new file mode 100644 index 00000000..bb19575b --- /dev/null +++ b/lua/obsidian/completion/sources/base/new.lua @@ -0,0 +1,217 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local obsidian = require "obsidian" +local util = require "obsidian.util" +local LinkStyle = require("obsidian.config").LinkStyle +local Note = require "obsidian.note" +local Path = require "obsidian.path" + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.NewNoteSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field search string|? +---@field insert_start integer|? +---@field insert_end integer|? +---@field ref_type obsidian.completion.RefType|? +local NewNoteSourceCompletionContext = abc.new_class() + +NewNoteSourceCompletionContext.new = function() + return NewNoteSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.NewNoteSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local NewNoteSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.NewNoteSourceBase +NewNoteSourceBase.new = function() + return NewNoteSourceBase.init() +end + +NewNoteSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.NewNoteSourceCompletionContext +function NewNoteSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = NewNoteSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.NewNoteSourceCompletionContext +function NewNoteSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + ---@type string|? + local block_link + cc.search, block_link = util.strip_block_links(cc.search) + + ---@type string|? + local anchor_link + cc.search, anchor_link = util.strip_anchor_links(cc.search) + + -- If block link is incomplete, do nothing. + if not block_link and vim.endswith(cc.search, "#^") then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- If anchor link is incomplete, do nothing. + if not anchor_link and vim.endswith(cc.search, "#") then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- Probably just a block/anchor link within current note. + if string.len(cc.search) == 0 then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- Create a mock block. + ---@type obsidian.note.Block|? + local block + if block_link then + block = { block = "", id = util.standardize_block(block_link), line = 1 } + end + + -- Create a mock anchor. + ---@type obsidian.note.HeaderAnchor|? + local anchor + if anchor_link then + anchor = { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } + end + + ---@type { label: string, note: obsidian.Note, template: string|? }[] + local new_notes_opts = {} + + local note = cc.client:create_note { title = cc.search, no_write = true } + if note.title and string.len(note.title) > 0 then + new_notes_opts[#new_notes_opts + 1] = { label = cc.search, note = note } + end + + -- Check for datetime macros. + for _, dt_offset in ipairs(util.resolve_date_macro(cc.search)) do + if dt_offset.cadence == "daily" then + note = cc.client:daily(dt_offset.offset, { no_write = true }) + if not note:exists() then + new_notes_opts[#new_notes_opts + 1] = + { label = dt_offset.macro, note = note, template = cc.client.opts.daily_notes.template } + end + end + end + + -- Completion items. + local items = {} + + for _, new_note_opts in ipairs(new_notes_opts) do + local new_note = new_note_opts.note + + assert(new_note.path) + + ---@type obsidian.config.LinkStyle, string + local link_style, label + if cc.ref_type == completion.RefType.Wiki then + link_style = LinkStyle.wiki + label = string.format("[[%s]] (create)", new_note_opts.label) + elseif cc.ref_type == completion.RefType.Markdown then + link_style = LinkStyle.markdown + label = string.format("[%s](…) (create)", new_note_opts.label) + else + error "not implemented" + end + + local new_text = cc.client:format_link(new_note, { link_style = link_style, anchor = anchor, block = block }) + local documentation = { + kind = "markdown", + value = new_note:display_info { + label = "Create: " .. new_text, + }, + } + + items[#items + 1] = { + documentation = documentation, + sortText = new_note_opts.label, + label = label, + kind = vim.lsp.protocol.CompletionItemKind.Reference, + textEdit = { + newText = new_text, + range = { + start = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_start, + }, + ["end"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_end, + }, + }, + }, + data = { + note = new_note, + template = new_note_opts.template, + }, + } + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = items })) +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.NewNoteSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function NewNoteSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.insert_start, cc.insert_end, cc.ref_type = completion.can_complete(cc.request) + + if cc.search ~= nil then + cc.search = util.lstrip_whitespace(cc.search) + end + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + return true +end + +--- Runs a generalized version of the execute method +---@param item any +---@return table|? callback_return_value +function NewNoteSourceBase:process_execute(item) + local client = assert(obsidian.get_client()) + local data = item.data + + if data == nil then + return nil + end + + -- Make sure `data.note` is actually an `obsidian.Note` object. If it gets serialized at some + -- point (seems to happen on Linux), it will lose its metatable. + if not Note.is_note_obj(data.note) then + data.note = setmetatable(data.note, Note.mt) + data.note.path = setmetatable(data.note.path, Path.mt) + end + + client:write_note(data.note, { template = data.template }) + return {} +end + +return NewNoteSourceBase diff --git a/lua/obsidian/completion/sources/base/refs.lua b/lua/obsidian/completion/sources/base/refs.lua new file mode 100644 index 00000000..602ada96 --- /dev/null +++ b/lua/obsidian/completion/sources/base/refs.lua @@ -0,0 +1,381 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local LinkStyle = require("obsidian.config").LinkStyle +local obsidian = require "obsidian" +local util = require "obsidian.util" +local iter = require("obsidian.itertools").iter + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.RefsSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field in_buffer_only boolean +---@field search string|? +---@field insert_start integer|? +---@field insert_end integer|? +---@field ref_type obsidian.completion.RefType|? +---@field block_link string|? +---@field anchor_link string|? +---@field new_text_to_option table +local RefsSourceCompletionContext = abc.new_class() + +RefsSourceCompletionContext.new = function() + return RefsSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.RefsSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local RefsSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.RefsSourceBase +RefsSourceBase.new = function() + return RefsSourceBase.init() +end + +RefsSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = RefsSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + completion_context.in_buffer_only = false + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + self:strip_links(cc) + self:determine_buffer_only_search_scope(cc) + + if cc.in_buffer_only then + local note = cc.client:current_note(0, { collect_anchor_links = true, collect_blocks = true }) + if note then + self:process_search_results(cc, { note }) + else + cc.completion_resolve_callback(self.incomplete_response) + end + else + local search_ops = cc.client.search_defaults() + search_ops.ignore_case = true + + cc.client:find_notes_async(cc.search, function(results) + self:process_search_results(cc, results) + end, { + search = search_ops, + notes = { collect_anchor_links = cc.anchor_link ~= nil, collect_blocks = cc.block_link ~= nil }, + }) + end +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function RefsSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.insert_start, cc.insert_end, cc.ref_type = completion.can_complete(cc.request) + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + + return true +end + +---Collect matching block links. +---@param note obsidian.Note +---@param block_link string? +---@return obsidian.note.Block[]|? +function RefsSourceBase:collect_matching_blocks(note, block_link) + ---@type obsidian.note.Block[]|? + local matching_blocks + if block_link then + assert(note.blocks) + matching_blocks = {} + for block_id, block_data in pairs(note.blocks) do + if vim.startswith("#" .. block_id, block_link) then + table.insert(matching_blocks, block_data) + end + end + + if #matching_blocks == 0 then + -- Unmatched, create a mock one. + table.insert(matching_blocks, { id = util.standardize_block(block_link), line = 1 }) + end + end + + return matching_blocks +end + +---Collect matching anchor links. +---@param note obsidian.Note +---@param anchor_link string? +---@return obsidian.note.HeaderAnchor[]? +function RefsSourceBase:collect_matching_anchors(note, anchor_link) + ---@type obsidian.note.HeaderAnchor[]|? + local matching_anchors + if anchor_link then + assert(note.anchor_links) + matching_anchors = {} + for anchor, anchor_data in pairs(note.anchor_links) do + if vim.startswith(anchor, anchor_link) then + table.insert(matching_anchors, anchor_data) + end + end + + if #matching_anchors == 0 then + -- Unmatched, create a mock one. + table.insert(matching_anchors, { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 }) + end + end + + return matching_anchors +end + +--- Strips block and anchor links from the current search string +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:strip_links(cc) + cc.search, cc.block_link = util.strip_block_links(cc.search) + cc.search, cc.anchor_link = util.strip_anchor_links(cc.search) + + -- If block link is incomplete, we'll match against all block links. + if not cc.block_link and vim.endswith(cc.search, "#^") then + cc.block_link = "#^" + cc.search = string.sub(cc.search, 1, -3) + end + + -- If anchor link is incomplete, we'll match against all anchor links. + if not cc.anchor_link and vim.endswith(cc.search, "#") then + cc.anchor_link = "#" + cc.search = string.sub(cc.search, 1, -2) + end +end + +--- Determines whatever the in_buffer_only should be enabled +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:determine_buffer_only_search_scope(cc) + if (cc.anchor_link or cc.block_link) and string.len(cc.search) == 0 then + -- Search over headers/blocks in current buffer only. + cc.in_buffer_only = true + end +end + +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@param results obsidian.Note[] +function RefsSourceBase:process_search_results(cc, results) + assert(cc) + assert(results) + + local completion_items = {} + + cc.new_text_to_option = {} + + for note in iter(results) do + ---@cast note obsidian.Note + + local matching_blocks = self:collect_matching_blocks(note, cc.block_link) + local matching_anchors = self:collect_matching_anchors(note, cc.anchor_link) + + if cc.in_buffer_only then + self:update_completion_options(cc, nil, nil, matching_anchors, matching_blocks, note) + else + -- Collect all valid aliases for the note, including ID, title, and filename. + ---@type string[] + local aliases + if not cc.in_buffer_only then + aliases = util.tbl_unique { tostring(note.id), note:display_name(), unpack(note.aliases) } + if note.title ~= nil then + table.insert(aliases, note.title) + end + end + + for alias in iter(aliases) do + self:update_completion_options(cc, alias, nil, matching_anchors, matching_blocks, note) + local alias_case_matched = util.match_case(cc.search, alias) + + if + alias_case_matched ~= nil + and alias_case_matched ~= alias + and not util.tbl_contains(note.aliases, alias_case_matched) + then + self:update_completion_options(cc, alias_case_matched, nil, matching_anchors, matching_blocks, note) + end + end + + if note.alt_alias ~= nil then + self:update_completion_options(cc, note:display_name(), note.alt_alias, matching_anchors, matching_blocks, note) + end + end + end + + for _, option in pairs(cc.new_text_to_option) do + -- TODO: need a better label, maybe just the note's display name? + ---@type string + local label + if cc.ref_type == completion.RefType.Wiki then + label = string.format("[[%s]]", option.label) + elseif cc.ref_type == completion.RefType.Markdown then + label = string.format("[%s](…)", option.label) + else + error "not implemented" + end + + table.insert(completion_items, { + documentation = option.documentation, + sortText = option.sort_text, + label = label, + kind = vim.lsp.protocol.CompletionItemKind.Reference, + textEdit = { + newText = option.new_text, + range = { + ["start"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_start, + }, + ["end"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_end + 1, + }, + }, + }, + }) + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = completion_items })) +end + +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@param label string|? +---@param alt_label string|? +---@param note obsidian.Note +function RefsSourceBase:update_completion_options(cc, label, alt_label, matching_anchors, matching_blocks, note) + ---@type { label: string|?, alt_label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }[] + local new_options = {} + if matching_anchors ~= nil then + for anchor in iter(matching_anchors) do + table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor }) + end + elseif matching_blocks ~= nil then + for block in iter(matching_blocks) do + table.insert(new_options, { label = label, alt_label = alt_label, block = block }) + end + else + if label then + table.insert(new_options, { label = label, alt_label = alt_label }) + end + + -- Add all blocks and anchors, let cmp sort it out. + for _, anchor_data in pairs(note.anchor_links or {}) do + table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor_data }) + end + for _, block_data in pairs(note.blocks or {}) do + table.insert(new_options, { label = label, alt_label = alt_label, block = block_data }) + end + end + + -- De-duplicate options relative to their `new_text`. + for _, option in ipairs(new_options) do + ---@type obsidian.config.LinkStyle + local link_style + if cc.ref_type == completion.RefType.Wiki then + link_style = LinkStyle.wiki + elseif cc.ref_type == completion.RefType.Markdown then + link_style = LinkStyle.markdown + else + error "not implemented" + end + + ---@type string, string, string, table|? + local final_label, sort_text, new_text, documentation + if option.label then + new_text = cc.client:format_link( + note, + { label = option.label, link_style = link_style, anchor = option.anchor, block = option.block } + ) + + final_label = assert(option.alt_label or option.label) + if option.anchor then + final_label = final_label .. option.anchor.anchor + elseif option.block then + final_label = final_label .. "#" .. option.block.id + end + sort_text = final_label + + documentation = { + kind = "markdown", + value = note:display_info { + label = new_text, + anchor = option.anchor, + block = option.block, + }, + } + elseif option.anchor then + -- In buffer anchor link. + -- TODO: allow users to customize this? + if cc.ref_type == completion.RefType.Wiki then + new_text = "[[#" .. option.anchor.header .. "]]" + elseif cc.ref_type == completion.RefType.Markdown then + new_text = "[#" .. option.anchor.header .. "](" .. option.anchor.anchor .. ")" + else + error "not implemented" + end + + final_label = option.anchor.anchor + sort_text = final_label + + documentation = { + kind = "markdown", + value = string.format("`%s`", new_text), + } + elseif option.block then + -- In buffer block link. + -- TODO: allow users to customize this? + if cc.ref_type == completion.RefType.Wiki then + new_text = "[[#" .. option.block.id .. "]]" + elseif cc.ref_type == completion.RefType.Markdown then + new_text = "[#" .. option.block.id .. "](#" .. option.block.id .. ")" + else + error "not implemented" + end + + final_label = "#" .. option.block.id + sort_text = final_label + + documentation = { + kind = "markdown", + value = string.format("`%s`", new_text), + } + else + error "should not happen" + end + + if cc.new_text_to_option[new_text] then + cc.new_text_to_option[new_text].sort_text = cc.new_text_to_option[new_text].sort_text .. " " .. sort_text + else + cc.new_text_to_option[new_text] = + { label = final_label, new_text = new_text, sort_text = sort_text, documentation = documentation } + end + end +end + +return RefsSourceBase diff --git a/lua/obsidian/completion/sources/base/tags.lua b/lua/obsidian/completion/sources/base/tags.lua new file mode 100644 index 00000000..db01a139 --- /dev/null +++ b/lua/obsidian/completion/sources/base/tags.lua @@ -0,0 +1,115 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.tags" +local iter = require("obsidian.itertools").iter +local obsidian = require "obsidian" +local util = require "obsidian.util" + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.TagsSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field search string|? +---@field in_frontmatter boolean|? +local TagsSourceCompletionContext = abc.new_class() + +TagsSourceCompletionContext.new = function() + return TagsSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.TagsSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local TagsSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.TagsSourceBase +TagsSourceBase.new = function() + return TagsSourceBase.init() +end + +TagsSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.TagsSourceCompletionContext +function TagsSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = TagsSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.TagsSourceCompletionContext +function TagsSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + local search_opts = cc.client.search_defaults() + search_opts.sort = false + + cc.client:find_tags_async(cc.search, function(tag_locs) + local tags = {} + for tag_loc in iter(tag_locs) do + tags[tag_loc.tag] = true + end + + local items = {} + for tag, _ in pairs(tags) do + items[#items + 1] = { + sortText = "#" .. tag, + label = "Tag: #" .. tag, + kind = vim.lsp.protocol.CompletionItemKind.Text, + insertText = "#" .. tag, + data = { + bufnr = cc.request.context.bufnr, + in_frontmatter = cc.in_frontmatter, + line = cc.request.context.cursor.line, + tag = tag, + }, + } + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = items })) + end, { search = search_opts }) +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.TagsSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function TagsSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.in_frontmatter = completion.can_complete(cc.request) + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + + return true +end + +--- Runs a generalized version of the execute method +---@param item any +function TagsSourceBase:process_execute(item) + if item.data.in_frontmatter then + -- Remove the '#' at the start of the tag. + -- TODO: ideally we should be able to do this by specifying the completion item in the right way, + -- but I haven't figured out how to do that. + local line = vim.api.nvim_buf_get_lines(item.data.bufnr, item.data.line, item.data.line + 1, true)[1] + line = util.string_replace(line, "#" .. item.data.tag, item.data.tag, 1) + vim.api.nvim_buf_set_lines(item.data.bufnr, item.data.line, item.data.line + 1, true, { line }) + end +end + +return TagsSourceBase diff --git a/lua/obsidian/completion/sources/base/types.lua b/lua/obsidian/completion/sources/base/types.lua new file mode 100644 index 00000000..16e1ce14 --- /dev/null +++ b/lua/obsidian/completion/sources/base/types.lua @@ -0,0 +1,14 @@ +---@class obsidian.completion.sources.base.Request.Context.Position +---@field public col integer +---@field public row integer + +---A request context class that partially matches cmp.Context to serve as a common interface for completion sources +---@class obsidian.completion.sources.base.Request.Context +---@field public bufnr integer +---@field public cursor obsidian.completion.sources.base.Request.Context.Position|lsp.Position +---@field public cursor_after_line string +---@field public cursor_before_line string + +---A request class that partially matches cmp.Request to serve as a common interface for completion sources +---@class obsidian.completion.sources.base.Request +---@field public context obsidian.completion.sources.base.Request.Context diff --git a/lua/obsidian/completion/sources/blink/new.lua b/lua/obsidian/completion/sources/blink/new.lua new file mode 100644 index 00000000..05f2edca --- /dev/null +++ b/lua/obsidian/completion/sources/blink/new.lua @@ -0,0 +1,31 @@ +local NewNoteSourceBase = require "obsidian.completion.sources.base.new" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.NewNoteSource : obsidian.completion.sources.base.NewNoteSourceBase +local NewNoteSource = abc.new_class() + +NewNoteSource.incomplete_response = blink_util.incomplete_response +NewNoteSource.complete_response = blink_util.complete_response + +function NewNoteSource.new() + return NewNoteSource.init(NewNoteSourceBase) +end + +---Implement the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function NewNoteSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +---Implements the execute method of the completion provider +---@param _ blink.cmp.Context +---@param item blink.cmp.CompletionItem +function NewNoteSource:execute(_, item) + self:process_execute(item) +end + +return NewNoteSource diff --git a/lua/obsidian/completion/sources/blink/refs.lua b/lua/obsidian/completion/sources/blink/refs.lua new file mode 100644 index 00000000..1740e316 --- /dev/null +++ b/lua/obsidian/completion/sources/blink/refs.lua @@ -0,0 +1,30 @@ +local RefsSourceBase = require "obsidian.completion.sources.base.refs" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.CompletionItem +---@field label string +---@field new_text string +---@field sort_text string +---@field documentation table|? + +---@class obsidian.completion.sources.blink.RefsSource : obsidian.completion.sources.base.RefsSourceBase +local RefsSource = abc.new_class() + +RefsSource.incomplete_response = blink_util.incomplete_response +RefsSource.complete_response = blink_util.complete_response + +function RefsSource.new() + return RefsSource.init(RefsSourceBase) +end + +---Implement the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function RefsSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +return RefsSource diff --git a/lua/obsidian/completion/sources/blink/tags.lua b/lua/obsidian/completion/sources/blink/tags.lua new file mode 100644 index 00000000..864dc4ac --- /dev/null +++ b/lua/obsidian/completion/sources/blink/tags.lua @@ -0,0 +1,31 @@ +local TagsSourceBase = require "obsidian.completion.sources.base.tags" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.TagsSource : obsidian.completion.sources.base.TagsSourceBase +local TagsSource = abc.new_class() + +TagsSource.incomplete_response = blink_util.incomplete_response +TagsSource.complete_response = blink_util.complete_response + +function TagsSource.new() + return TagsSource.init(TagsSourceBase) +end + +---Implements the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function TagsSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +---Implements the execute method of the completion provider +---@param _ blink.cmp.Context +---@param item blink.cmp.CompletionItem +function TagsSource:execute(_, item) + self:process_execute(item) +end + +return TagsSource diff --git a/lua/obsidian/completion/sources/blink/util.lua b/lua/obsidian/completion/sources/blink/util.lua new file mode 100644 index 00000000..79b1b43a --- /dev/null +++ b/lua/obsidian/completion/sources/blink/util.lua @@ -0,0 +1,38 @@ +local M = {} + +---Generates the completion request from a blink context +---@param context blink.cmp.Context +---@return obsidian.completion.sources.base.Request +M.generate_completion_request_from_editor_state = function(context) + local row = context.cursor[1] + local col = context.cursor[2] + local cursor_before_line = context.line:sub(1, col) + local cursor_after_line = context.line:sub(col + 1) + + return { + context = { + bufnr = context.bufnr, + cursor_before_line = cursor_before_line, + cursor_after_line = cursor_after_line, + cursor = { + row = row, + col = col, + line = row + 1, + }, + }, + } +end + +M.incomplete_response = { + is_incomplete_forward = true, + is_incomplete_backward = true, + items = {}, +} + +M.complete_response = { + is_incomplete_forward = true, + is_incomplete_backward = false, + items = {}, +} + +return M diff --git a/lua/obsidian/completion/sources/nvim_cmp/new.lua b/lua/obsidian/completion/sources/nvim_cmp/new.lua new file mode 100644 index 00000000..62e8952f --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/new.lua @@ -0,0 +1,34 @@ +local NewNoteSourceBase = require "obsidian.completion.sources.base.new" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.NewNoteSource : obsidian.completion.sources.base.NewNoteSourceBase +local NewNoteSource = abc.new_class() + +NewNoteSource.new = function() + return NewNoteSource.init(NewNoteSourceBase) +end + +NewNoteSource.get_keyword_pattern = completion.get_keyword_pattern + +NewNoteSource.incomplete_response = nvim_cmp_util.incomplete_response +NewNoteSource.complete_response = nvim_cmp_util.complete_response + +---Invoke completion (required). +---@param request cmp.SourceCompletionApiParams +---@param callback fun(response: lsp.CompletionResponse|nil) +function NewNoteSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +---Creates a new note using the default template for the completion item. +---Executed after the item was selected. +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +function NewNoteSource:execute(completion_item, callback) + return callback(self:process_execute(completion_item)) +end + +return NewNoteSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/refs.lua b/lua/obsidian/completion/sources/nvim_cmp/refs.lua new file mode 100644 index 00000000..95f54348 --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/refs.lua @@ -0,0 +1,29 @@ +local RefsSourceBase = require "obsidian.completion.sources.base.refs" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.CompletionItem +---@field label string +---@field new_text string +---@field sort_text string +---@field documentation table|? + +---@class obsidian.completion.sources.nvim_cmp.RefsSource : obsidian.completion.sources.base.RefsSourceBase +local RefsSource = abc.new_class() + +RefsSource.new = function() + return RefsSource.init(RefsSourceBase) +end + +RefsSource.get_keyword_pattern = completion.get_keyword_pattern + +RefsSource.incomplete_response = nvim_cmp_util.incomplete_response +RefsSource.complete_response = nvim_cmp_util.complete_response + +function RefsSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +return RefsSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/tags.lua b/lua/obsidian/completion/sources/nvim_cmp/tags.lua new file mode 100644 index 00000000..476e8707 --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/tags.lua @@ -0,0 +1,28 @@ +local TagsSourceBase = require "obsidian.completion.sources.base.tags" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.tags" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.TagsSource : obsidian.completion.sources.base.TagsSourceBase +local TagsSource = abc.new_class() + +TagsSource.new = function() + return TagsSource.init(TagsSourceBase) +end + +TagsSource.get_keyword_pattern = completion.get_keyword_pattern + +TagsSource.incomplete_response = nvim_cmp_util.incomplete_response +TagsSource.complete_response = nvim_cmp_util.complete_response + +function TagsSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +function TagsSource:execute(item, callback) + self:process_execute(item) + return callback {} +end + +return TagsSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/util.lua b/lua/obsidian/completion/sources/nvim_cmp/util.lua new file mode 100644 index 00000000..1c2e16bd --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/util.lua @@ -0,0 +1,10 @@ +local M = {} + +M.incomplete_response = { isIncomplete = true } + +M.complete_response = { + isIncomplete = true, + items = {}, +} + +return M diff --git a/lua/obsidian/completion/tags.lua b/lua/obsidian/completion/tags.lua index 0f70476e..a757e595 100644 --- a/lua/obsidian/completion/tags.lua +++ b/lua/obsidian/completion/tags.lua @@ -39,7 +39,12 @@ M.can_complete = function(request) local in_frontmatter = false local line = request.context.cursor.line local frontmatter_start, frontmatter_end = get_frontmatter_boundaries(request.context.bufnr) - if frontmatter_start ~= nil and frontmatter_start <= line and frontmatter_end ~= nil and line <= frontmatter_end then + if + frontmatter_start ~= nil + and frontmatter_start <= (line + 1) + and frontmatter_end ~= nil + and (line + 1) <= frontmatter_end + then in_frontmatter = true end diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 4f3ae84d..3a78caad 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -280,6 +280,7 @@ config.LinkStyle = { ---@class obsidian.config.CompletionOpts --- ---@field nvim_cmp boolean +---@field blink boolean ---@field min_chars integer config.CompletionOpts = {} @@ -343,6 +344,7 @@ config.Picker = { telescope = "telescope.nvim", fzf_lua = "fzf-lua", mini = "mini.pick", + snacks = "snacks.pick", } ---@class obsidian.config.PickerOpts diff --git a/lua/obsidian/health.lua b/lua/obsidian/health.lua new file mode 100644 index 00000000..e6bd07a8 --- /dev/null +++ b/lua/obsidian/health.lua @@ -0,0 +1,90 @@ +local M = {} +local VERSION = require "obsidian.version" +local util = require "obsidian.util" + +local error = vim.health.error +local warn = vim.health.warn +local ok = vim.health.ok + +local function info(...) + local t = { ... } + local format = table.remove(t, 1) + local str = #t == 0 and format or string.format(format, unpack(t)) + return ok(str) +end + +---@private +---@param name string +local function start(name) + vim.health.start(string.format("obsidian.nvim [%s]", name)) +end + +---@param plugin string +---@param optional boolean +---@return boolean +local function has_plugin(plugin, optional) + local plugin_info = util.get_plugin_info(plugin) + if plugin_info then + info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") + return true + else + if not optional then + vim.health.error(" " .. plugin .. " not installed") + end + return false + end +end + +---@param plugins string[] +local function has_one_of(plugins) + local found + for _, plugin in ipairs(plugins) do + if has_plugin(plugin, true) then + found = true + end + end + if not found then + vim.health.warn("It is recommended to install at least one of " .. vim.inspect(plugins)) + end +end + +---@param minimum string +---@param recommended string +local function neovim(minimum, recommended) + if vim.fn.has("nvim-" .. minimum) == 0 then + error("neovim < " .. minimum) + elseif vim.fn.has("nvim-" .. recommended) == 0 then + warn("neovim < " .. recommended .. " some features will not work") + else + ok("neovim >= " .. recommended) + end +end + +function M.check() + neovim("0.8", "0.11") + start "Version" + info("Obsidian.nvim v%s (%s)", VERSION, util.get_plugin_info("obsidian.nvim").commit) + + start "Pickers" + + has_one_of { + "telescope.nvim", + "fzf-lua", + "mini.nvim", + "mini.pick", + "snacks.nvim", + } + + start "Completion" + + has_one_of { + "nvim-cmp", + "blink.cmp", + } + + start "Dependencies" + info(" ✓ rg: %s", util.get_external_dependency_info "rg" or "not found") + has_plugin("plenary.nvim", false) +end + +return M diff --git a/lua/obsidian/img_paste.lua b/lua/obsidian/img_paste.lua index edd5df90..5cdda53e 100644 --- a/lua/obsidian/img_paste.lua +++ b/lua/obsidian/img_paste.lua @@ -40,7 +40,13 @@ local function clipboard_is_img() -- See: [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) local this_os = util.get_os() if this_os == util.OSType.Linux or this_os == util.OSType.FreeBSD then - return vim.tbl_contains(content, "image/png") + if vim.tbl_contains(content, "image/png") then + return true + elseif vim.tbl_contains(content, "text/uri-list") then + local success = + os.execute "wl-paste --type text/uri-list | sed 's|file://||' | head -n1 | tr -d '[:space:]' | xargs -I{} sh -c 'wl-copy < \"$1\"' _ {}" + return success == 0 + end elseif this_os == util.OSType.Darwin then return string.sub(content[1], 1, 9) == "iVBORw0KG" -- Magic png number in base64 elseif this_os == util.OSType.Windows or this_os == util.OSType.Wsl then diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index b5e13910..7cc8dab8 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -99,13 +99,11 @@ obsidian.setup = function(opts) -- These will be available across all buffers, not just note buffers in the vault. obsidian.commands.install(client) - -- Register cmp sources. + -- Register completion sources, providers if opts.completion.nvim_cmp then - local cmp = require "cmp" - - cmp.register_source("obsidian", require("cmp_obsidian").new()) - cmp.register_source("obsidian_new", require("cmp_obsidian_new").new()) - cmp.register_source("obsidian_tags", require("cmp_obsidian_tags").new()) + require("obsidian.completion.plugin_initializers.nvim_cmp").register_sources() + elseif opts.completion.blink then + require("obsidian.completion.plugin_initializers.blink").register_providers() end local group = vim.api.nvim_create_augroup("obsidian_setup", { clear = true }) @@ -139,22 +137,11 @@ obsidian.setup = function(opts) vim.keymap.set("n", mapping_keys, mapping_config.action, mapping_config.opts) end - -- Inject Obsidian as a cmp source. + -- Inject completion sources, providers to their plugin configurations if opts.completion.nvim_cmp then - local cmp = require "cmp" - - local sources = { - { name = "obsidian" }, - { name = "obsidian_new" }, - { name = "obsidian_tags" }, - } - for _, source in pairs(cmp.get_config().sources) do - if source.name ~= "obsidian" and source.name ~= "obsidian_new" and source.name ~= "obsidian_tags" then - table.insert(sources, source) - end - end - ---@diagnostic disable-next-line: missing-fields - cmp.setup.buffer { sources = sources } + require("obsidian.completion.plugin_initializers.nvim_cmp").inject_sources() + elseif opts.completion.blink then + require("obsidian.completion.plugin_initializers.blink").inject_sources() end -- Run enter-note callback. diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua new file mode 100644 index 00000000..d01e42a0 --- /dev/null +++ b/lua/obsidian/pickers/_snacks.lua @@ -0,0 +1,159 @@ +local snacks_picker = require "snacks.picker" + +local Path = require "obsidian.path" +local abc = require "obsidian.abc" +local Picker = require "obsidian.pickers.picker" + +local function debug_once(msg, ...) + -- vim.notify(msg .. vim.inspect(...)) +end + +---@param mapping table +---@return table +local function notes_mappings(mapping) + if type(mapping) == "table" then + local opts = { win = { input = { keys = {} } }, actions = {} } + for k, v in pairs(mapping) do + local name = string.gsub(v.desc, " ", "_") + opts.win.input.keys = { + [k] = { name, mode = { "n", "i" }, desc = v.desc }, + } + opts.actions[name] = function(picker, item) + debug_once("mappings :", item) + picker:close() + vim.schedule(function() + v.callback(item.value or item._path) + end) + end + end + return opts + end + return {} +end + +---@class obsidian.pickers.SnacksPicker : obsidian.Picker +local SnacksPicker = abc.new_class({ + ---@diagnostic disable-next-line: unused-local + __tostring = function(self) + return "SnacksPicker()" + end, +}, Picker) + +---@param opts obsidian.PickerFindOpts|? Options. +SnacksPicker.find_files = function(self, opts) + opts = opts or {} + + ---@type obsidian.Path + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "files", + title = opts.prompt_title, + cwd = tostring(dir), + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("find files callback: ", item) + opts.callback(item._path) + else + debug_once("find files jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) + snacks_picker.pick(pick_opts) +end + +---@param opts obsidian.PickerGrepOpts|? Options. +SnacksPicker.grep = function(self, opts) + opts = opts or {} + + debug_once("grep opts : ", opts) + + ---@type obsidian.Path + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "grep", + title = opts.prompt_title, + cwd = tostring(dir), + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("grep callback: ", item) + opts.callback(item._path or item.filename) + else + debug_once("grep jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) + snacks_picker.pick(pick_opts) +end + +---@param values string[]|obsidian.PickerEntry[] +---@param opts obsidian.PickerPickOpts|? Options. +---@diagnostic disable-next-line: unused-local +SnacksPicker.pick = function(self, values, opts) + self.calling_bufnr = vim.api.nvim_get_current_buf() + + opts = opts or {} + + debug_once("pick opts: ", opts) + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { + text = name, + buf = buf, + filename = value.filename, + value = value.value, + pos = { value.lnum, value.col or 0 }, + }) + end + end + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + tilte = opts.prompt_title, + items = entries, + layout = { + preview = false, + }, + format = "text", + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("pick callback: ", item) + opts.callback(item.value) + else + debug_once("pick jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) + + snacks_picker.pick(pick_opts) +end + +return SnacksPicker diff --git a/lua/obsidian/pickers/init.lua b/lua/obsidian/pickers/init.lua index 7b2decd3..51d5ad88 100644 --- a/lua/obsidian/pickers/init.lua +++ b/lua/obsidian/pickers/init.lua @@ -13,7 +13,7 @@ M.get = function(client, picker_name) if picker_name then picker_name = string.lower(picker_name) else - for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini } do + for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini, PickerName.snacks } do local ok, res = pcall(M.get, client, name) if ok then return res @@ -28,6 +28,8 @@ M.get = function(client, picker_name) return require("obsidian.pickers._mini").new(client) elseif picker_name == string.lower(PickerName.fzf_lua) then return require("obsidian.pickers._fzf").new(client) + elseif picker_name == string.lower(PickerName.snacks) then + return require("obsidian.pickers._snacks").new(client) elseif picker_name then error("not implemented for " .. picker_name) end diff --git a/lua/obsidian/templates.lua b/lua/obsidian/templates.lua index ac8a83f2..d7e9f3e7 100644 --- a/lua/obsidian/templates.lua +++ b/lua/obsidian/templates.lua @@ -151,12 +151,14 @@ end ---Insert a template at the given location. --- ----@param opts { template_name: string|obsidian.Path, client: obsidian.Client, location: { [1]: integer, [2]: integer, [3]: integer, [4]: integer } } Options. +---@param opts { note: obsidian.Note|?, template_name: string|obsidian.Path, client: obsidian.Client, location: { [1]: integer, [2]: integer, [3]: integer, [4]: integer } } Options. --- ---@return obsidian.Note M.insert_template = function(opts) local buf, win, row, _ = unpack(opts.location) - local note = Note.from_buffer(buf) + if opts.note == nil then + opts.note = Note.from_buffer(buf) + end local template_path = resolve_template(opts.template_name, opts.client) @@ -165,7 +167,7 @@ M.insert_template = function(opts) if template_file then local lines = template_file:lines() for line in lines do - local new_lines = M.substitute_template_variables(line, opts.client, note) + local new_lines = M.substitute_template_variables(line, opts.client, opts.note) if string.find(new_lines, "[\r\n]") then local line_start = 1 for line_end in util.gfind(new_lines, "[\r\n]") do diff --git a/lua/obsidian/util.lua b/lua/obsidian/util.lua index 1db3d044..a737ea81 100644 --- a/lua/obsidian/util.lua +++ b/lua/obsidian/util.lua @@ -176,6 +176,30 @@ util.escape_magic_characters = function(text) return text:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") end +---Check if a string is a checkbox list item +--- +---Supported checboox lists: +--- - [ ] foo +--- - [x] foo +--- + [x] foo +--- * [ ] foo +--- 1. [ ] foo +--- 1) [ ] foo +--- +---@param s string +---@return boolean +util.is_checkbox = function(s) + -- - [ ] and * [ ] and + [ ] + if string.match(s, "^%s*[-+*]%s+%[.%]") ~= nil then + return true + end + -- 1. [ ] and 1) [ ] + if string.match(s, "^%s*%d+[%.%)]%s+%[.%]") ~= nil then + return true + end + return false +end + ---Check if a string is a valid URL. ---@param s string ---@return boolean @@ -193,6 +217,10 @@ util.is_url = function(s) end end +---Checks if a given string represents an image file based on its suffix. +--- +---@param s string: The input string to check. +---@return boolean: Returns true if the string ends with a supported image suffix, false otherwise. util.is_img = function(s) for _, suffix in ipairs { ".png", ".jpg", ".jpeg", ".heic", ".gif", ".svg", ".ico" } do if vim.endswith(s, suffix) then @@ -503,32 +531,32 @@ util.zettel_id = function() return tostring(os.time()) .. "-" .. suffix end ----Toggle the checkbox on the line that the cursor is on. +---Toggle the checkbox on the current line. +--- +---@param opts table|nil Optional table containing checkbox states (e.g., {" ", "x"}). +---@param line_num number|nil Optional line number to toggle the checkbox on. Defaults to the current line. util.toggle_checkbox = function(opts, line_num) -- Allow line_num to be optional, defaulting to the current line if not provided line_num = line_num or unpack(vim.api.nvim_win_get_cursor(0)) local line = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] - local checkbox_pattern = "^%s*- %[.] " local checkboxes = opts or { " ", "x" } - if not string.match(line, checkbox_pattern) then + if util.is_checkbox(line) then + for i, check_char in enumerate(checkboxes) do + if string.match(line, "^.* %[" .. util.escape_magic_characters(check_char) .. "%].*") then + i = i % #checkboxes + line = util.string_replace(line, "[" .. check_char .. "]", "[" .. checkboxes[i + 1] .. "]", 1) + break + end + end + else local unordered_list_pattern = "^(%s*)[-*+] (.*)" if string.match(line, unordered_list_pattern) then line = string.gsub(line, unordered_list_pattern, "%1- [ ] %2") else line = string.gsub(line, "^(%s*)", "%1- [ ] ") end - else - for i, check_char in enumerate(checkboxes) do - if string.match(line, "^%s*- %[" .. util.escape_magic_characters(check_char) .. "%].*") then - if i == #checkboxes then - i = 0 - end - line = util.string_replace(line, "- [" .. check_char .. "]", "- [" .. checkboxes[i + 1] .. "]", 1) - break - end - end end -- 0-indexed vim.api.nvim_buf_set_lines(0, line_num - 1, line_num, true, { line }) @@ -757,6 +785,11 @@ util.smart_action = function() return "ObsidianFollowLink" end + -- show notes with tag if possible + if util.cursor_tag(nil, nil) then + return "ObsidianTag" + end + -- toggle task if possible -- cycles through your custom UI checkboxes, default: [ ] [~] [>] [x] return "ObsidianToggleCheckbox" @@ -1341,4 +1374,12 @@ util.buffer_is_empty = function(bufnr) end end +---Check if a string is NaN +--- +---@param v any +---@return boolean +util.isNan = function(v) + return tostring(v) == tostring(0 / 0) +end + return util diff --git a/lua/obsidian/version.lua b/lua/obsidian/version.lua index 96d97ee7..8123fcca 100644 --- a/lua/obsidian/version.lua +++ b/lua/obsidian/version.lua @@ -1 +1 @@ -return "3.9.0" +return "3.10.0" diff --git a/lua/obsidian/yaml/parser.lua b/lua/obsidian/yaml/parser.lua index 3373ae38..a8e3a53e 100644 --- a/lua/obsidian/yaml/parser.lua +++ b/lua/obsidian/yaml/parser.lua @@ -555,7 +555,7 @@ end ---@diagnostic disable-next-line: unused-local Parser._parse_number = function(self, i, text) local out = tonumber(text) - if out == nil then + if out == nil or util.isNan(out) then return false, nil, nil else return true, nil, out diff --git a/minimal.lua b/minimal.lua new file mode 100644 index 00000000..74fa4410 --- /dev/null +++ b/minimal.lua @@ -0,0 +1,61 @@ +vim.env.LAZY_STDPATH = ".repro" +load(vim.fn.system "curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua")() + +vim.fn.mkdir(".repro/vault", "p") + +vim.o.conceallevel = 2 + +local plugins = { + { + "obsidian-nvim/obsidian.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + opts = { + completion = { + blink = true, + nvim_cmp = false, + }, + workspaces = { + { + name = "test", + path = vim.fs.joinpath(vim.uv.cwd(), ".repro", "vault"), + }, + }, + }, + }, + + -- **Choose your renderer** + { "MeanderingProgrammer/render-markdown.nvim", dependencies = { "echasnovski/mini.icons" }, opts = {} }, + -- { "OXY2DEV/markview.nvim", lazy = false }, + + -- **Choose your picker** + "nvim-telescope/telescope.nvim", + -- "folke/snacks.nvim", + -- "ibhagwan/fzf-lua", + -- "echasnovski/mini.pick", + + { + "hrsh7th/nvim-cmp", + config = function() + local cmp = require "cmp" + cmp.setup { + mapping = cmp.mapping.preset.insert { + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm { select = true }, + }, + } + end, + }, + { + "saghen/blink.cmp", + opts = { + fuzzy = { implementation = "lua" }, -- no need to build binary + keymap = { + preset = "default", + }, + }, + }, +} + +require("lazy.minit").repro { spec = plugins } + +vim.cmd "checkhealth obsidian" diff --git a/scripts/prepare_changelog.py b/scripts/prepare_changelog.py index 5ba56ff0..9feda4b9 100644 --- a/scripts/prepare_changelog.py +++ b/scripts/prepare_changelog.py @@ -30,7 +30,7 @@ def main(): lines.insert(insert_index, "\n") lines.insert( insert_index + 1, - f"## [v{VERSION}](https://github.com/epwalsh/obsidian.nvim/releases/tag/v{VERSION}) - " + f"## [v{VERSION}](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v{VERSION}) - " f"{datetime.now().strftime('%Y-%m-%d')}\n", ) diff --git a/test/manual/client_spec.lua b/test/manual/client_spec.lua index 304c5369..8538c40b 100644 --- a/test/manual/client_spec.lua +++ b/test/manual/client_spec.lua @@ -4,7 +4,7 @@ local obsidian = require "obsidian" -local client = obsidian.setup { dir = "~/epwalsh-notes/notes" } ---@diagnostic disable-line: missing-fields +local client = obsidian.setup { dir = "~/obsidian-nvim/notes" } ---@diagnostic disable-line: missing-fields for _, note in ipairs(client:find_notes("allennlp", { search = { sort = false } })) do print(note.id) end diff --git a/test/obsidian/util_spec.lua b/test/obsidian/util_spec.lua index c4cb7e0e..3bcc10ee 100644 --- a/test/obsidian/util_spec.lua +++ b/test/obsidian/util_spec.lua @@ -389,3 +389,114 @@ describe("util.markdown_link()", function() ) end) end) + +describe("util.toggle_checkbox", function() + before_each(function() + vim.cmd "bwipeout!" -- wipe out the buffer to avoid unsaved changes + vim.cmd "enew" -- create a new empty buffer + vim.bo.bufhidden = "wipe" -- and wipe it after use + end) + + it("should toggle between default states with - lists", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "- [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("- [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with * lists", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "* [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("* [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("* [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with numbered lists with .", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "1. [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("1. [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("1. [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with numbered lists with )", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "1) [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("1) [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("1) [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should use custom states if provided", function() + local custom_states = { " ", "!", "x" } + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "- [ ] dummy" }) + + util.toggle_checkbox(custom_states) + assert.equals("- [!] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [ ] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [!] dummy", vim.api.nvim_get_current_line()) + end) +end) + +describe("util.is_checkbox", function() + it("should return true for valid checkbox list items", function() + assert.is_true(util.is_checkbox "- [ ] Task 1") + assert.is_true(util.is_checkbox "- [x] Task 1") + assert.is_true(util.is_checkbox "+ [ ] Task 1") + assert.is_true(util.is_checkbox "+ [x] Task 1") + assert.is_true(util.is_checkbox "* [ ] Task 2") + assert.is_true(util.is_checkbox "* [x] Task 2") + assert.is_true(util.is_checkbox "1. [ ] Task 3") + assert.is_true(util.is_checkbox "1. [x] Task 3") + assert.is_true(util.is_checkbox "2. [ ] Task 3") + assert.is_true(util.is_checkbox "10. [ ] Task 3") + assert.is_true(util.is_checkbox "1) [ ] Task") + assert.is_true(util.is_checkbox "10) [ ] Task") + end) + + it("should return false for non-checkbox list items", function() + assert.is_false(util.is_checkbox "- Task 1") + assert.is_false(util.is_checkbox "-- Task 1") + assert.is_false(util.is_checkbox "-- [ ] Task 1") + assert.is_false(util.is_checkbox "* Task 2") + assert.is_false(util.is_checkbox "++ [ ] Task 2") + assert.is_false(util.is_checkbox "1. Task 3") + assert.is_false(util.is_checkbox "1.1 Task 3") + assert.is_false(util.is_checkbox "1.1 [ ] Task 3") + assert.is_false(util.is_checkbox "1)1 Task 3") + assert.is_false(util.is_checkbox "Random text") + end) + + it("should handle leading spaces correctly", function() + -- true + assert.is_true(util.is_checkbox " - [ ] Task 1") + assert.is_true(util.is_checkbox " * [ ] Task 2") + assert.is_true(util.is_checkbox " 5. [ ] Task 2") + + -- false + assert.is_false(util.is_checkbox " - Task 1") + assert.is_false(util.is_checkbox " * Task 1") + assert.is_false(util.is_checkbox " 1. Task 1") + end) +end) diff --git a/test/obsidian/yaml/parser_spec.lua b/test/obsidian/yaml/parser_spec.lua index a96103c2..618f78d9 100644 --- a/test/obsidian/yaml/parser_spec.lua +++ b/test/obsidian/yaml/parser_spec.lua @@ -30,6 +30,10 @@ describe("Parser class", function() return parser:parse_number(str) end, "foo") assert.is_false(ok) + ok, _ = pcall(function(str) + return parser:parse_number(str) + end, "Nan") + assert.is_false(ok) end) it("should parse booleans while trimming whitespace", function()