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
56 changes: 24 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Then populate the exercise database. The simplest way is to import a JSON file
(see [`test/example_exercises.json`](test/example_exercises.json) for the expected schema):

```vim
:CPImport /path/to/exercises.json
:CP import /path/to/exercises.json
```

Or set `exercises_json` in your config to auto-import on first run:
Expand All @@ -55,7 +55,7 @@ export HF_TOKEN=your_token
uv run tools/generate_exercises.py tools/syllabus.toml
```

Or from Neovim: `:CPGenerate`.
Or from Neovim: `:CP generate`.

Requirements
------------
Expand All @@ -66,31 +66,30 @@ Requirements

Quick Start
-----------
1. Open browser: `:CP` (or `<leader>cp`)
1. Open browser: `:CP`
2. Navigate with `j`/`k`, open with `Enter`
3. Write your solution in the buffer
4. Run tests: `Ctrl-t` (or `:CPRun`)
5. Move on: `Ctrl-n` (or `:CPNext`)
4. Run tests: `Ctrl-t`
5. Move on: `Ctrl-n`

Commands
--------
| Command | Description |
|----------------|--------------------------------------|
| `:CP [action]` | Open/close/refresh browser or stats |
| `:CPRun` | Run tests for the current exercise |
| `:CPNext` | Open the next unsolved exercise |
| `:CPPrev` | Go back to the previous exercise |
| `:CPSkip` | Skip current exercise and move on |
| `:CPDesc` | Show exercise description popup |
| `:CPHint` | Show hints for the current exercise |
| `:CPSolution` | Show reference solution in a split |
| `:CPStats` | Show practice statistics |
| `:CPHelp` | Show the in-editor quick guide |
| `:CPImport` | Import exercises from a JSON file |
| `:CPImport!` | Replace all exercises from JSON |
| `:CPGenerate` | Generate exercises via LLM |

All commands support tab completion -- type `:CP<Tab>` to explore.
Everything goes through a single `:CP` command with subcommands.
Tab completion is supported: type `:CP <Tab>` to explore.

| Command | Description |
|-----------------------|--------------------------------------|
| `:CP` or `:CP open` | Open exercise browser |
| `:CP close` | Close the browser |
| `:CP refresh` | Refresh the browser list |
| `:CP stats` | Show practice statistics |
| `:CP help` | Show the in-editor quick guide |
| `:CP import <path>` | Import exercises from a JSON file |
| `:CP! import <path>` | Replace all exercises from JSON |
| `:CP generate` | Generate exercises via LLM |

Exercise-level actions (run tests, next, skip, hints, solution, etc.) are
available via buffer-local keymaps only (see below).

Browser Keymaps
---------------
Expand All @@ -110,13 +109,6 @@ Browser Keymaps
| `Esc` | Close browser |
| `?` | Show help guide |

Global Keymaps
--------------
| Key | Action |
|----------------|--------------------------|
| `<leader>cp` | Open exercise browser |
| `<leader>cps` | Show statistics |

Exercise Buffer Keymaps
-----------------------
Active in normal mode inside exercise buffers. All use Ctrl shortcuts for
Expand Down Expand Up @@ -162,17 +154,17 @@ uv run tools/generate_exercises.py tools/syllabus.toml --dry-run
uv run tools/generate_exercises.py tools/syllabus.toml --engines my_engines.toml
```

Or from Neovim: `:CPGenerate` (prompts for topic, count, difficulty, and engine).
Or from Neovim: `:CP generate` (prompts for topic, count, difficulty, and engine).

Data
----
Exercises are stored in an SQLite database at `stdpath("data")/code-practice/exercises.db`.
Import exercises from a JSON file with `:CPImport <path>`, or use `:CPImport!` to
Import exercises from a JSON file with `:CP import <path>`, or use `:CP! import <path>` to
replace existing data. The database path is configurable via `storage.db_path`.

Roadmap
-------
- [ ] Random exercise (`:CPRandom`)
- [ ] Random exercise (`:CP random`)
- [ ] Search widget in browser
- [ ] Bug-finding exercise type
- [ ] Context-aware LLM hint based on current buffer code
Expand Down
8 changes: 1 addition & 7 deletions dev/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,5 @@ require("code-practice").setup({
enabled = true,
},
},
keymaps = {
browser = {
open = "<leader>cp",
run = "<leader>cpr",
stats = "<leader>cps",
},
},
keymaps = {},
})
105 changes: 35 additions & 70 deletions doc/code-practice.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,79 +59,54 @@ Requirements:

Run |:checkhealth| |code-practice| to verify all dependencies.

After installing, populate the exercise database with |:CPImport| or
|:CPGenerate|.
After installing, populate the exercise database with |:CP| `import` or
|:CP| `generate`.

==============================================================================
3. QUICK START *code-practice-quickstart*

1. Open the browser |:CP| or `<leader>cp`
1. Open the browser |:CP|
2. Navigate with `j`/`k` Press `Enter` to open an exercise
3. Write your solution Edit the buffer
4. Run tests |:CPRun| or `Ctrl-t`
5. Move on |:CPNext| or `Ctrl-n`
4. Run tests `Ctrl-t`
5. Move on `Ctrl-n`

==============================================================================
4. COMMANDS *code-practice-commands*

All commands support tab completion. Type `:CP<Tab>` to explore.
Everything goes through a single `:CP` command with subcommands.
Tab completion is supported: type `:CP <Tab>` to explore.

*:CP*
:CP [action] Open, close, or refresh the browser, or show stats.
Actions: `open` (default), `close`, `refresh`, `stats`.
:CP Open the exercise browser (same as `:CP open`).

*:CodePractice*
:CodePractice [action] Long form of |:CP|.
:CP open Open the exercise browser.

*:CPRun*
:CPRun Run tests for the current exercise. For theory
exercises, reads the answer from the `Answer:` line
in the buffer (press 1-4 to select).
:CP close Close the exercise browser.

*:CPNext*
:CPNext Open the next unsolved exercise. Skips exercises
marked with |:CPSkip| in the current session.
:CP refresh Refresh the exercise browser list.

*:CPPrev*
:CPPrev Go back to the previous exercise in the session
history.

*:CPSkip*
:CPSkip Skip the current exercise and advance to the next
unsolved one. Skipped exercises are remembered for
the current session only.

*:CPDesc*
:CPDesc Show the exercise description in a floating popup.

*:CPHint*
:CPHint Show hints for the current exercise (if available).

*:CPSolution*
:CPSolution Show the reference solution in a vertical split.

*:CPStats*
:CPStats Show practice statistics (total, solved, by
:CP stats Show practice statistics (total, solved, by
difficulty).

*:CPHelp*
:CPHelp Show an in-editor keymap cheat-sheet reflecting your
:CP help Show an in-editor keymap cheat-sheet reflecting your
current configuration. For full documentation, see
this help file.

*:CPImport*
:CPImport {path} Import exercises from a JSON file. The expected
:CP import {path} Import exercises from a JSON file. The expected
schema matches `test/example_exercises.json` in the
plugin source.

:CPImport! {path} Replace all existing exercises, test cases, attempts,
:CP import! {path} Replace all existing exercises, test cases, attempts,
and theory options with the contents of {path}.

*:CPGenerate*
:CPGenerate Interactively generate exercises via Hugging Face
:CP generate Interactively generate exercises via Hugging Face
LLM. Prompts for topic, count, difficulty, and
engine. See |code-practice-generate| for setup.

Exercise-level actions (run tests, next, skip, hints, solution, etc.) are
available via buffer-local keymaps only. See |code-practice-keymaps-exercise|.

==============================================================================
5. KEYMAPS *code-practice-keymaps*

Expand All @@ -156,7 +131,7 @@ Active inside the browser window opened by |:CP|.
`gg` Go to top of list
`G` Go to bottom of list
`q` / `Esc` Close browser
`?` Show keymap cheat-sheet (|:CPHelp|)
`?` Show keymap cheat-sheet (|:CP| `help`)

EXERCISE BUFFER KEYMAPS ~
*code-practice-keymaps-exercise*
Expand All @@ -166,23 +141,14 @@ use Ctrl shortcuts for single-chord access. Keymaps are configurable
via `keymaps.exercise` in config.

Key Action ~
`Ctrl-t` Run tests (|:CPRun|)
`Ctrl-n` Next exercise (|:CPNext|)
`Ctrl-p` Previous exercise (|:CPPrev|)
`Ctrl-k` Skip exercise (|:CPSkip|)
`Ctrl-i` Show hints (|:CPHint|)
`Ctrl-l` View solution (|:CPSolution|)
`Ctrl-d` Show description (|:CPDesc|)
`Ctrl-b` Open browser (|:CP|)

GLOBAL KEYMAPS ~
*code-practice-keymaps-global*

Set during |code-practice.setup()|.

Key Action ~
`<leader>cp` Open exercise browser
`<leader>cps` Show statistics
`Ctrl-t` Run tests
`Ctrl-n` Next exercise
`Ctrl-p` Previous exercise
`Ctrl-k` Skip exercise
`Ctrl-i` Show hints
`Ctrl-l` View solution
`Ctrl-d` Show description
`Ctrl-b` Open browser

==============================================================================
6. CONFIGURATION *code-practice-config*
Expand All @@ -202,7 +168,7 @@ Options are merged with |vim.tbl_deep_extend()|.
width = 0.8, -- fraction of editor width
height = 0.8, -- fraction of editor height
border = "rounded",
show_numbers = true,
show_numbers = true, -- show exercise index in browser list
},

-- Engine defaults are built from the registry (see |code-practice-engines|).
Expand All @@ -220,7 +186,6 @@ Options are merged with |vim.tbl_deep_extend()|.

keymaps = {
browser = {
open = "<leader>cp",
open_item = "<CR>",
filter_easy = "e",
filter_medium = "m",
Expand Down Expand Up @@ -290,7 +255,7 @@ Run |:checkhealth| |code-practice| to verify both are available.

FROM NEOVIM ~

Use |:CPGenerate|. It will prompt for topic, count, difficulty, and engine.
Use |:CP| `generate`. It will prompt for topic, count, difficulty, and engine.

FROM THE COMMAND LINE ~

Expand Down Expand Up @@ -330,11 +295,11 @@ its test cases, attempts, and options.

IMPORTING ~

Use |:CPImport| to load exercises from a JSON file: >vim
:CPImport /path/to/exercises.json
Use |:CP| `import` to load exercises from a JSON file: >vim
:CP import /path/to/exercises.json
<
Use the bang form to wipe all existing data and replace: >vim
:CPImport! /path/to/exercises.json
:CP! import /path/to/exercises.json
<
Set `storage.exercises_json` in your config to auto-import on first run
when the database is empty.
Expand All @@ -350,8 +315,8 @@ Run `:checkhealth code-practice` to verify:
- Neovim version (0.10+ required)
- nui.nvim and sqlite.lua plugins
- Engine executables based on enabled config (iterates the registry)
- uv executable (needed for |:CPGenerate|)
- HF_TOKEN environment variable (needed for |:CPGenerate|)
- uv executable (needed for |:CP| `generate`)
- HF_TOKEN environment variable (needed for |:CP| `generate`)
- Database file existence

==============================================================================
Expand Down
8 changes: 6 additions & 2 deletions lua/code-practice/browser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ function browser.render_exercise_list()
local engine_icon = engines.icon(ex.engine)
local solved_icon = state.solved_ids[ex.id] and "✓ " or " "

local line = string.format("%s%s %s %s%s", prefix, diff_icon, engine_icon, solved_icon, ex.title)
local num_str = ""
if config.get("ui.show_numbers") then
num_str = string.format("%d. ", i)
end
local line = string.format("%s%s %s %s%s%s", prefix, diff_icon, engine_icon, solved_icon, num_str, ex.title)
table.insert(lines, line)
end

Expand Down Expand Up @@ -150,7 +154,7 @@ function browser.render_preview()
end

table.insert(lines, "")
table.insert(lines, "Press Enter to open, then :CPRun to test")
table.insert(lines, "Press Enter to open, then <C-t> to run tests")

state.preview_cache[exercise.id] = lines
return lines
Expand Down
1 change: 0 additions & 1 deletion lua/code-practice/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ M.defaults = {

keymaps = {
browser = {
open = "<leader>cp",
open_item = "<CR>",
filter_easy = "e",
filter_medium = "m",
Expand Down
8 changes: 4 additions & 4 deletions lua/code-practice/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ function M.check()
end
end

-- :CPGenerate dependencies
-- :CP generate dependencies
if vim.fn.executable("uv") == 1 then
vim.health.ok("uv found (needed for :CPGenerate)")
vim.health.ok("uv found (needed for :CP generate)")
else
vim.health.warn("uv not found", { "Install uv (https://github.com/astral-sh/uv) to use :CPGenerate" })
vim.health.warn("uv not found", { "Install uv (https://github.com/astral-sh/uv) to use :CP generate" })
end

if vim.env.HF_TOKEN and vim.env.HF_TOKEN ~= "" then
vim.health.ok("HF_TOKEN is set")
else
vim.health.warn(
"HF_TOKEN not set",
{ "Set the HF_TOKEN environment variable to use :CPGenerate with Hugging Face models" }
{ "Set the HF_TOKEN environment variable to use :CP generate with Hugging Face models" }
)
end

Expand Down
1 change: 1 addition & 0 deletions lua/code-practice/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function help.show()
17
) .. "Open browser",
"",
" Commands: :CP open | stats | help | import | generate",
" See :help code-practice for full documentation",
"",
" Press q, <Esc>, or <Enter> to close",
Expand Down
12 changes: 1 addition & 11 deletions lua/code-practice/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,14 @@ function code_practice.setup(opts)
utils.notify("Auto-import failed: " .. (err or "unknown error"), "error")
end
else
utils.notify("No exercises found. Run :CPImport <path> to load exercises from a JSON file.", "warn")
utils.notify("No exercises found. Run :CP import <path> to load exercises from a JSON file.", "warn")
end
end

browser.set_on_open(function(id)
code_practice.open_exercise(id)
end)

local keymaps = config.get("keymaps.browser")

vim.keymap.set("n", keymaps.open or "<leader>cp", function()
code_practice.open_browser()
end, { desc = "Open Code Practice browser" })

vim.keymap.set("n", keymaps.stats or "<leader>cps", function()
vim.cmd("CPStats")
end, { desc = "Show statistics" })

utils.delete_temp_files()

utils.notify("Code Practice initialized!")
Expand Down
Loading