Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
fail-fast: false
matrix:
neovim: [stable, nightly]
picker: [snacks, fzf-lua]

steps:
- name: Checkout code
Expand All @@ -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 }}
204 changes: 134 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,47 @@
<em>(Demo recorded in version v0.0.4)</em>
</p>

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:

## Installation
* [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua) and the `fzf` binary
* [folke/snacks.nvim](https://github.com/folke/snacks.nvim)

Install with [lazy.nvim](https://github.com/folke/lazy.nvim).
Install the external tools however you normally do:

Use `version = "*"` to follow the latest stable release.
```sh
brew install cppman sqlite fzf
# or
sudo apt install cppman sqlite3 fzf
```

## Installation

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 = {
{
"<leader>cu",
Expand All @@ -46,111 +67,154 @@ 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("<cword>"))
```

## Moving Around

The docs viewer is just a normal Neovim buffer in a float. Regular movement
still works: `j`, `k`, `<C-d>`, `<C-u>`, `/`, `n`, and so on.

cppman.nvim adds a few mappings on top:

* `K`, `<C-]>`, or double-click: follow the word under the cursor
* `K` on a table-of-contents entry: jump to that section
* `<C-T>` or right-click: go back to the previous cppman page/search
* `<Tab>`: go forward again after going back
* `q`: close the viewer

With the lazy.nvim spec above, the plugin also loads on these keymaps:
* `<leader>cu` - open CPPMan for the word under the cursor.
* `<leader>ck` - open the CPPMan search picker.
Visual mode works too:

* Select text, then press `K` or `<C-]>` 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 = {
{
"<leader>mu",
function()
require("cppman").open_for(vim.fn.expand("<cword>"))
end,
desc = "[C++] open under cursor",
},
{
"<leader>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
If you only want one docs source:

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 `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.
```lua
require("cppman").setup({
source = "cppreference.com",
})
```

If you prefer one site only, set `source = "cppreference.com"` or `source = "cplusplus.com"`.
## How Search Works

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.
`cppman.nvim` reads cppman's local `index.db` with `sqlite3` and keeps the
keyword list in memory for the current Neovim session.

## Performance
The `cppman` command itself is only used when opening a documentation page.

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.
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.

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.
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.

Rendered pages are also cached in memory for the current Neovim session, so reopening the same page at the same width is effectively instant.
## Page Rendering

## Navigation
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.

In normal mode, the docs viewer uses the same navigation as standalone `cppman`:
Rendered pages are also cached in memory for the current Neovim session.

* **K**, **\<C-]\>**, **\<2-LeftMouse\>** - follow the word under the cursor (when the cursor is on a TOC entry, jumps to that section instead)
* **\<C-T\>**, **\<RightMouse\>** - go back to the previous page (cursor position is restored)
* **\<Tab\>** - go forward (undo a back navigation; cursor position is restored)
* **q** - close the viewer
## Health Check

In visual mode inside the viewer:
Run:

* **K**, **\<C-]\>** - follow the selected text
## 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`.

## Repo analytics
## Repo Analytics

![Alt](https://repobeats.axiom.co/api/embed/5df93c3d0004a00ff8f890d50b098386ed326e26.svg "Repobeats analytics image")
Binary file removed assets/demo.mp4
Binary file not shown.
Loading
Loading