From 3419c4f0e00a56c5a64271c3fa96555113840b19 Mon Sep 17 00:00:00 2001
From: Simon Winther
Date: Sat, 6 Jun 2026 06:37:46 +0200
Subject: [PATCH 1/2] feat(ci): add support for multiple picker options in
smoke test workflow
---
.github/workflows/smoke.yml | 15 ++-
README.md | 201 +++++++++++++++++++-----------
assets/demo.mp4 | Bin 8701914 -> 0 bytes
doc/cppman.txt | 54 ++++++--
lua/cppman/config.lua | 3 +
lua/cppman/health.lua | 44 +++++--
lua/cppman/picker.lua | 219 +++++++++++++++++++--------------
lua/cppman/pickers/common.lua | 37 ++++++
lua/cppman/pickers/fzf_lua.lua | 116 +++++++++++++++++
lua/cppman/pickers/snacks.lua | 108 ++++++++++++++++
scripts/smoke_test.lua | 79 +++++++++++-
11 files changed, 687 insertions(+), 189 deletions(-)
delete mode 100644 assets/demo.mp4
create mode 100644 lua/cppman/pickers/common.lua
create mode 100644 lua/cppman/pickers/fzf_lua.lua
create mode 100644 lua/cppman/pickers/snacks.lua
diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml
index 91a9fd8..141c76d 100644
--- a/.github/workflows/smoke.yml
+++ b/.github/workflows/smoke.yml
@@ -13,6 +13,7 @@ jobs:
fail-fast: false
matrix:
neovim: [stable, nightly]
+ picker: [snacks, fzf-lua]
steps:
- name: Checkout code
@@ -24,10 +25,18 @@ jobs:
neovim: true
version: ${{ matrix.neovim }}
- - name: Install snacks.nvim
+ - name: Install picker dependency
run: |
mkdir -p .tests/deps
- git clone --depth=1 https://github.com/folke/snacks.nvim .tests/deps/snacks.nvim
+ if [ "${{ matrix.picker }}" = "snacks" ]; then
+ git clone --depth=1 https://github.com/folke/snacks.nvim .tests/deps/snacks.nvim
+ else
+ sudo apt-get update
+ sudo apt-get install -y fzf
+ git clone --depth=1 https://github.com/ibhagwan/fzf-lua .tests/deps/fzf-lua
+ fi
- name: Run smoke test
- run: nvim --headless -u NONE -c "set rtp+=$PWD,$PWD/.tests/deps/snacks.nvim" -l scripts/smoke_test.lua
+ run: nvim --headless -u NONE -l scripts/smoke_test.lua
+ env:
+ CPPMAN_PICKER_PROVIDER: ${{ matrix.picker }}
diff --git a/README.md b/README.md
index 736fce4..d80f4f7 100644
--- a/README.md
+++ b/README.md
@@ -10,26 +10,46 @@
(Demo recorded in version v0.0.4)
-A Neovim plugin for the [cppman CLI](https://github.com/aitjcize/cppman), so you can search cplusplus.com and cppreference.com without leaving Neovim.
+A small Neovim plugin for the [cppman CLI](https://github.com/aitjcize/cppman).
+
+It lets you search C++ docs from inside Neovim and opens the result in a
+floating viewer. Search reads cppman's local SQLite index, so the picker does
+not need to call the `cppman` command for every query.
## Requirements
-- Neovim 0.10+ (uses `vim.keycode`, `vim.system`, `vim.uv`)
-- [cppman](https://github.com/aitjcize/cppman) - for rendering documentation pages
-- `sqlite3` CLI - for querying the keyword index (`apt install sqlite3`, `brew install sqlite`)
-- [folke/snacks.nvim](https://github.com/folke/snacks.nvim) - for the fuzzy picker UI
+- Neovim 0.10+
+- [cppman](https://github.com/aitjcize/cppman)
+- `sqlite3`
+- One picker backend:
+ - [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua) and the `fzf` binary
+ - [folke/snacks.nvim](https://github.com/folke/snacks.nvim)
-## Installation
+Install the external tools however you normally do:
-Install with [lazy.nvim](https://github.com/folke/lazy.nvim).
+```sh
+brew install cppman sqlite fzf
+# or
+sudo apt install cppman sqlite3 fzf
+```
+
+## Installation
-Use `version = "*"` to follow the latest stable release.
+With [lazy.nvim](https://github.com/folke/lazy.nvim), using `fzf-lua`:
```lua
-{
+return {
"simonwinther/cppman.nvim",
version = "*",
cmd = "CPPMan",
+ dependencies = {
+ "ibhagwan/fzf-lua",
+ },
+ opts = {
+ picker = {
+ provider = "fzf-lua",
+ },
+ },
keys = {
{
"cu",
@@ -46,107 +66,150 @@ Use `version = "*"` to follow the latest stable release.
desc = "[C++] keyword search",
},
},
+}
+```
+
+If you already use `snacks.nvim`, use that instead:
+
+```lua
+return {
+ "simonwinther/cppman.nvim",
+ version = "*",
+ cmd = "CPPMan",
dependencies = {
"folke/snacks.nvim",
},
- opts = {},
+ opts = {
+ picker = {
+ provider = "snacks",
+ },
+ },
}
```
+You can also leave the provider as `"auto"`. It tries `snacks.nvim` first, then
+`fzf-lua`.
+
## Usage
-Run `:CPPMan` to open the keyword search picker, or pass a term directly: `:CPPMan std::array`
+Open the search picker:
+
+```vim
+:CPPMan
+```
+
+Open a page directly:
+
+```vim
+:CPPMan std::vector
+```
+
+From Lua:
+
+```lua
+require("cppman").search()
+require("cppman").open_for(vim.fn.expand(""))
+```
+
+## Moving Around
+
+The docs viewer is just a normal Neovim buffer in a float. Regular movement
+still works: `j`, `k`, ``, ``, `/`, `n`, and so on.
+
+cppman.nvim adds a few mappings on top:
+
+- `K`, ``, or double-click: follow the word under the cursor
+- `K` on a table-of-contents entry: jump to that section
+- `` or right-click: go back to the previous cppman page/search
+- ``: go forward again after going back
+- `q`: close the viewer
+
+Visual mode works too:
-With the lazy.nvim spec above, the plugin also loads on these keymaps:
-* `cu` - open CPPMan for the word under the cursor.
-* `ck` - open the CPPMan search picker.
+- Select text, then press `K` or `` to open docs for the selection
## Configuration
-All options with their defaults:
+Defaults:
```lua
require("cppman").setup({
- -- Documentation source: "both", "cppreference.com", or "cplusplus.com"
+ -- "both", "cppreference.com", or "cplusplus.com"
source = "both",
+
index = {
- -- Explicit path to cppman's index.db (optional override)
- -- If nil, the plugin auto-discovers a valid DB
+ -- Set this if cppman's index.db is somewhere unusual.
+ -- If nil, cppman.nvim tries to find a usable one.
db_path = nil,
},
+
picker = {
- width = 0.4, -- fraction of editor width
- height = 0.4, -- fraction of editor height
+ -- "auto", "fzf-lua", or "snacks"
+ provider = "auto",
+
+ width = 0.4,
+ height = 0.4,
+
+ -- Passed through to the picker backend.
+ snacks = {},
+ fzf_lua = {},
},
+
viewer = {
- width = 0.8, -- fraction of editor width
- height = 0.6, -- fraction of editor height
+ width = 0.8,
+ height = 0.6,
},
})
```
-### Customizing keymaps
-
-Keymaps live in your lazy.nvim spec instead of `setup()` so they can trigger
-lazy loading:
+Example with a slightly larger `fzf-lua` picker:
```lua
-{
- "simonwinther/cppman.nvim",
- version = "*",
- cmd = "CPPMan",
- keys = {
- {
- "mu",
- function()
- require("cppman").open_for(vim.fn.expand(""))
- end,
- desc = "[C++] open under cursor",
- },
- {
- "mk",
- function()
- require("cppman").search()
- end,
- desc = "[C++] keyword search",
- },
+require("cppman").setup({
+ picker = {
+ provider = "fzf-lua",
+ width = 0.5,
+ height = 0.5,
},
- dependencies = { "folke/snacks.nvim" },
- opts = {},
-}
+})
```
-## How it works
-
-Search is powered by a local SQLite index that ships with cppman. The plugin queries it once per session via `sqlite3` and passes the keyword list to `snacks.picker` for fast in-memory fuzzy matching. The `cppman` CLI is only used to render documentation pages, not to search.
+If you only want one docs source:
-If `source = "both"`, the plugin merges cppreference and cplusplus results into one in-memory picker list. Exact identical matches are deduplicated, and the picker shows a small source badge so you can see which site a result will open from.
-
-If you prefer one site only, set `source = "cppreference.com"` or `source = "cplusplus.com"`.
+```lua
+require("cppman").setup({
+ source = "cppreference.com",
+})
+```
-If the user's local `~/.cache/cppman/index.db` is empty or corrupt (which can happen when `cppman -r` fails due to missing Python dependencies), the plugin falls back to the packaged index.db bundled with the cppman Python package.
+## How Search Works
-## Performance
+`cppman.nvim` reads cppman's local `index.db` with `sqlite3` and keeps the
+keyword list in memory for the current Neovim session.
-Search is fast because the plugin loads cppman's SQLite index once and keeps the results in memory for picker matching. In `source = "both"` mode, the merged cross-source list is also built once per session and then reused.
+The `cppman` command itself is only used when opening a documentation page.
-Documentation rendering is optimized for cached pages. If a man page already exists in cppman's `.3.gz` cache, the plugin renders that file directly through cppman's pager script instead of starting the full `cppman` CLI search path again. That avoids most of the Python startup and lookup overhead while keeping the same rendered output. If the page is not cached yet, the plugin falls back to normal `cppman` behavior.
+When `source = "both"`, results from cppreference and cplusplus are merged.
+Duplicate exact matches are removed where possible, and the picker shows a small
+source label so you know where the page will open from.
-Rendered pages are also cached in memory for the current Neovim session, so reopening the same page at the same width is effectively instant.
+If your local `~/.cache/cppman/index.db` is empty or broken, the plugin tries to
+use the packaged `index.db` from the cppman Python package instead.
-## Navigation
+## Page Rendering
-In normal mode, the docs viewer uses the same navigation as standalone `cppman`:
+If cppman already has the `.3.gz` page cached, `cppman.nvim` renders that file
+through cppman's pager script. If not, it falls back to normal cppman behavior.
-* **K**, **\**, **\<2-LeftMouse\>** - follow the word under the cursor (when the cursor is on a TOC entry, jumps to that section instead)
-* **\**, **\** - go back to the previous page (cursor position is restored)
-* **\** - go forward (undo a back navigation; cursor position is restored)
-* **q** - close the viewer
+Rendered pages are also cached in memory for the current Neovim session.
-In visual mode inside the viewer:
+## Health Check
-* **K**, **\** - follow the selected text
+Run:
-## Health check
+```vim
+:checkhealth cppman
+```
-Run `:checkhealth cppman` to verify that `cppman`, `sqlite3`, `snacks.nvim`, and a valid `index.db` are all present.
+It checks Neovim, `cppman`, `sqlite3`, your picker backend, and the resolved
+`index.db`.
diff --git a/assets/demo.mp4 b/assets/demo.mp4
deleted file mode 100644
index 7f893f3542a7e2824d55d87d8871ff54b4dd5af8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 8701914
zcmV(@K-Rwi001Cnba`-Tb8l?`00IDMb8l^Fb8j+Xc4IMZa5OOh000PPa%E)zgp#vu
zWMOmw000XH00)6QXaFN9K!5-M00;&F1ONaC5P$#xRh(tG1RMX4R6AcNweTpLomups
z3ucb9^9aYm(syF}27J(wtESF^lHD~RJ~5Zj^<~7d?=%9xl+<7OW@37aAart*E$dli
zDo#9^9sKK`W3gTXWIj#qdAXAOFG-;xw{N-UZhuPBwbl#lyBhCGUlyU(HTt;BN77RB
zW?fbHb_F2ye;J|8-XcXOR2>q{ClfW8l>B9wyXW3ZO~!dQ3JjZob)AZzt)>6V!HlPH
z2!mvPk)ZE){f#jhPu#Q#TKx$)=fT~0x?krrNdlKYDch}*v55#DTf|d|jNi#n
z`YP?6Xin{&MHa9Z!jzM{M{cseMr~IigcE%-(l4$vZFoXosKQip`J`}8M(SV;my+Ep
z!eU4*iATebf^{Cv(8>1qMXKUAC8-PaNidK}zao?SH63k9Z2+kV>hS1d=6|0YwxNxl
zJAoPLv>g7mb*$J)laByu#r(sN7J0KS|lqEC(>SWqN{wlR^+%b0j#j(jL
z(pST!S2S4@e22#Gm9kK<*8^Fv9pmoYli>hPn$)8j2Ccp%kCGG+j_AXFRx1TE_}q-z
z#nLHEDlxBBOwLxw!(`JEE^r!SeM$a?fX$uqWlhZkO#v{mYk-9}h1&>8R@e@^TU(b7
zt6bd&gLMlxi2fqbQe(7RM=+-uk~718CPciIgQbtD>qILeps#nvw-{cThB6#!+mCi3
z&dUq_Am5iH&e@^@m9?4nehb|i3%p9Fw0HmsvH;@0n<2A76)O^zL27$9E(0ywbDSfo
zqsckm5>~?WK&Py}tEu&e
z?d@-(0b#z=WC4jH2%>YCtQtKaaUzB-yf(&o_4Zq=Oq&FoOa9i8oQyWiEA4Tvnxzk3
zLU!CFpD(j~KT=$yo~qp&gm)*gE$T5-(4(kXd9RUU1c`3tcwA@+3iAiAbP%=%kCxX7
zFfJj-=en7&k;vdgKA)=Njx3Ylr2yqtwc)O)@|(%*Q@mJbgVCO;KWSkNLM8f=seo1k
zoGtA<={50|jT}^C=b>sniYDQsAA6JIRuu4q>FA9>wBCRPzDZ7SDH4*JQ0}i9>08le
zb6gv}SeGdWL6y6E04fGYB|PTMydmM>g>ByrdMjr98+t5SF5AIikegZYBh$MwV|xy7
za8mqZ_AvwgD0Z;GJ078QE|(R@K?6QQBd#fq7-|AiehvXNYIG(N9Apu*tu&|rh)ruB
z>1;Xlrh3Mhf67F9Fss$h7PZwZNaK|Sitt@9{{%$tD`{?o3Bmj9#z?ba9x{~zy)4*J
z)1<{#wNWf{jiehEa#~pGUHIK~Kl8=h)O6|N`v`16-uE&Dd!o?{1sCf&_QO}}qhQ`>
zmCFc^R%tRQtIPZ1sUT$YsNjsXsV0ENwjBu!9q{2uml&nD(fKXmSUT+D@w!Qk)<13|
zuMZ}R!To{77OW@JglZf@fC+@Hw{?e2?3N2ljs46!}VC#
z`Y-C+JLS*tm53<=@*>#n1*!i~Z*oO2;2Y)8bW6o~Q|+~#I86}jc8N`!lv{Qq8n2AMdI?ToR
z;F|{>d+}P4<5i!MXxC~~Kx47#7~qfGhSEha|87HdWpeRY&kYyh1rsM+aMbozp3Tc2PqYnb#uSCQ~OAw&v`za6Mb4fp`h(!&Hk!vKl
zb8{+cSXbrh9HN(Z_fwl&WsbT0k*1`~iLcE;UR|ZmN=hwOFBpECc_3Hf9>e9DIrJK;
z!$^|qF2kap=BuPWW{1hmeil*(fALv;Fbv4Z=v1nHx;VwZf=HfW|9~vpF6ENl{+^xp
zTHy$|?&`{-tMq;Y5|O_}*58nv$GjH26PR%kGcpEBQ2fDVK)3se%Wy&5B*nx}^+HWY
ztpd>tOdrA?e#Gou8whaY@=x|9KfHv+fTj=suqc%f3NFhP)JwklRtq%?i5NA+ES$DH
z6Hk%ixSt?ib2HCKs_|%4ApN%JN1ogI_1G@o@$`RMA&JiVLms%KS)+7I7${g>bUKGm
z$$nffZ>ZJ?Eyn|aajD%jD>LLwP+nq?NI);LGwVTVO|!AAI?q>#_QTc^H2&{ly>cr;
zbwrnqR?#6MGnP}>hyeQ9%?JAS>+G|!?|M>~)-^RJ>{xB=f-Z&rny#KA+B*gNbkDAs
z?%G&vE{^2p0?iQ?Os9Q`zs<}8z;|JA5p8^tKGl7C1-!`2(^%LH@-SXqN8^i6wOdwx
z^DFV@(>6r(iE(}r=6mIi+@Qk$ngBI0S7m}3?l9u3x8|kPCSls~*X(unxayg07G<_XDcCcp?=TAJS-BCu)!2F=Wy6}pZP8YrCeGB{>oMeoe>-_(
zYzSn_WyOkEtKudE3&X7pX%EBei$VMhxj&!40TE#9f{!{T_@)DJ%HzUa>T8NqZ(o4X
zA-Wk^xA>ufiEO*ubYo{;EgV&l7(b?P*hAlKzy2nqawerXHt{Uwxr}AB0?7^Fg}0_{
zK8mEK#q;!O_*ME1K_j|0T=N`GyGgi58<3Bh>Gb|*T3D9@cfEeBlqBL-Z~J3Y%%4tM
zYBopR@ci)e?{ub!b;SC8o%$8T`F;?Ry%*Z7*wWIglNEX`Ry8aRm#%DH$II~b_K8(^
zAkAJUu6N(vQ)LTOXudpkaQyFFXLLV)*dft@OWdEfgcUgq*7>b60YPDYNfTw3`ID#b
z++7q0n~bG$yW8b%L~!BvOc@ktcvX@5>4n?y7ycs3TcfB*7qM~|gSOp-`^hkWNd
zu)#kLBl^1?utN`ZVI#>DmwfU-M`^@B$bjKa-QB9&IZ;M6SyEaRK}6*_5&{-8XEI2R
zC>A^US+{xKW}U_zW&n4ccoi>+H3VA`xk)}CL>kUZU9m5t>p`ZD1f%&$$_6f%`gSsQ
z=Dms*;?_i3AySL-Tg&pn1HePRHs@7;Ha?aVejubN{EggPlX)#hdhCh$Mx<8~KRh-9
zT{c^FGyV;i5Dx%q^X7KYJgwP==j`)>#0rcvE*(tZC)AGoq4XI7{9rp>ob*Ek
z$kjy@UhxeL!<*muSr2$PUY*Et-$Cw1oM%*=gh1fcm`3|#;?kwO8VU^E0iyEca~_GC
znoV^J&9HpxTuV&PColsv+4krg_8*y}zm8V>w7DGSV!QfZPCPt*hA1aWe$1cbH}%&K
zTKb+fFNg+Gb2mWkt>HN~C@#8qN7Tz^TD`uJs23VZC;sOSwMJxm4R&vG>H`Vx?|?d<
z7%WvqC8@eXAeIoq-R>SikElSP$8oWWsCO==IyKA|D7eCdO;HKlsE@6e(-EFz@%+33
zxGfy3_C4LSy`u_8e}3!QBt#j_i*CvT0t`WLM~bV#rM-CMImm(@;z^-xVNh`wzl^3q_C>YmgW*v+m8ug#aI&(my
zE@dGEP0&<9mp^S^2Hoi_4qZ^iBPL8IaM;EPTlL(*I$~;8Nb`f`#%bt4aXwL_;`Rbf
ziM|^A_CO5P#s$wKjRiauRca?igm&*M1)fkhC!XWslJKY9JS(2)V2s2G$PM1dBK4_A
zHS+;B$tV!R{eY>e7QX>#%Xsp9gY|!x6}FM>2Ye2$