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
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Features
--------
- Browser UI with preview for exercises
- Deterministic navigation: next, skip, previous
- Code runners for Python (and Rust if enabled)
- Extensible engine registry: interpreted and compiled runners
- Theory questions with answer checking
- Results window and solution viewer
- LLM-powered exercise generation (see Tools below)
Expand Down Expand Up @@ -62,16 +62,15 @@ Requirements
- Neovim 0.10+
- MunifTanjim/nui.nvim
- kkharji/sqlite.lua
- python3 (for Python exercises)
- cargo (optional, for Rust exercises)
- Engine executables for each enabled engine (run `:checkhealth code-practice`)

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

Commands
--------
Expand Down Expand Up @@ -120,18 +119,19 @@ Global Keymaps

Exercise Buffer Keymaps
-----------------------
These are active only inside exercise buffers (set via `keymaps.exercise` config):

| Key | Action |
|----------------|---------------------------------|
| `<leader>r` | Run tests |
| `<leader>h` | Show hints |
| `<leader>s` | View solution (split) |
| `<leader>d` | Show description |
| `<leader>n` | Next exercise |
| `<leader>p` | Previous exercise |
| `<leader>k` | Skip exercise |
| `<leader>m` | Open browser (menu) |
Active in normal mode inside exercise buffers. All use Ctrl shortcuts for
single-chord access (configurable via `keymaps.exercise`):

| Key | Action |
|-----------|---------------------------------|
| `Ctrl-t` | Run tests |
| `Ctrl-n` | Next exercise |
| `Ctrl-p` | Previous exercise |
| `Ctrl-k` | Skip exercise |
| `Ctrl-i` | Show hints |
| `Ctrl-l` | View solution (split) |
| `Ctrl-d` | Show description |
| `Ctrl-b` | Open browser |

Tools
-----
Expand Down
44 changes: 23 additions & 21 deletions doc/code-practice.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ After installing, populate the exercise database with |:CPImport| or
1. Open the browser |:CP| or `<leader>cp`
2. Navigate with `j`/`k` Press `Enter` to open an exercise
3. Write your solution Edit the buffer
4. Run tests |:CPRun| or `<leader>r`
5. Move on |:CPNext| or `<leader>n`
4. Run tests |:CPRun| or `Ctrl-t`
5. Move on |:CPNext| or `Ctrl-n`

==============================================================================
4. COMMANDS *code-practice-commands*
Expand All @@ -85,8 +85,8 @@ All commands support tab completion. Type `:CP<Tab>` to explore.

*:CPRun*
:CPRun Run tests for the current exercise. For theory
exercises, expects a line like `Answer: 2` in the
buffer.
exercises, reads the answer from the `Answer:` line
in the buffer (press 1-4 to select).

*:CPNext*
:CPNext Open the next unsolved exercise. Skips exercises
Expand Down Expand Up @@ -161,17 +161,19 @@ Active inside the browser window opened by |:CP|.
EXERCISE BUFFER KEYMAPS ~
*code-practice-keymaps-exercise*

Active inside exercise buffers (set via `keymaps.exercise` in config).
Active inside exercise buffers (normal mode only). All exercise keymaps
use Ctrl shortcuts for single-chord access. Keymaps are configurable
via `keymaps.exercise` in config.

Key Action ~
`<leader>r` Run tests (|:CPRun|)
`<leader>h` Show hints (|:CPHint|)
`<leader>s` View solution (|:CPSolution|)
`<leader>d` Show description (|:CPDesc|)
`<leader>n` Next exercise (|:CPNext|)
`<leader>p` Previous exercise (|:CPPrev|)
`<leader>k` Skip exercise (|:CPSkip|)
`<leader>m` Open browser (|:CP|)
`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*
Expand Down Expand Up @@ -227,14 +229,14 @@ Options are merged with |vim.tbl_deep_extend()|.
close = "q",
},
exercise = {
run_tests = "<leader>r",
show_hint = "<leader>h",
view_solution = "<leader>s",
show_description = "<leader>d",
next_exercise = "<leader>n",
prev_exercise = "<leader>p",
skip_exercise = "<leader>k",
open_browser = "<leader>m",
run_tests = "<C-t>",
show_hint = "<C-i>",
view_solution = "<C-l>",
show_description = "<C-d>",
next_exercise = "<C-n>",
prev_exercise = "<C-p>",
skip_exercise = "<C-k>",
open_browser = "<C-b>",
},
},
})
Expand Down
26 changes: 17 additions & 9 deletions lua/code-practice/browser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ function browser.render_exercise_list()
table.insert(lines, " No exercises found.")
end

local km = config.get("keymaps.browser") or {}
table.insert(lines, "")
table.insert(lines, " " .. string.rep("─", 30))
table.insert(lines, " j/k:nav Enter:open ?:help q:close a:all")
table.insert(
lines,
" j/k:nav Enter:open ?:help " .. (km.close or "q") .. ":close " .. (km.filter_all or "a") .. ":all"
)

return lines
end
Expand Down Expand Up @@ -244,10 +248,10 @@ function browser.setup_keymaps()
map("<CR>", "<cmd>lua require('code-practice.browser').open_selected()<CR>")
end
map("o", "<cmd>lua require('code-practice.browser').open_selected()<CR>")
map("e", "<cmd>lua require('code-practice.browser').filter_by_difficulty('easy')<CR>")
map("m", "<cmd>lua require('code-practice.browser').filter_by_difficulty('medium')<CR>")
map("h", "<cmd>lua require('code-practice.browser').filter_by_difficulty('hard')<CR>")
map("a", "<cmd>lua require('code-practice.browser').clear_filters()<CR>")
map(keymaps.filter_easy or "e", "<cmd>lua require('code-practice.browser').filter_by_difficulty('easy')<CR>")
map(keymaps.filter_medium or "m", "<cmd>lua require('code-practice.browser').filter_by_difficulty('medium')<CR>")
map(keymaps.filter_hard or "h", "<cmd>lua require('code-practice.browser').filter_by_difficulty('hard')<CR>")
map(keymaps.filter_all or "a", "<cmd>lua require('code-practice.browser').clear_filters()<CR>")

for _, name in ipairs(engines.list()) do
local eng = engines.get(name)
Expand All @@ -256,8 +260,11 @@ function browser.setup_keymaps()
end
end

map("q", "<cmd>lua require('code-practice.browser').close()<CR>")
map("<esc>", "<cmd>lua require('code-practice.browser').close()<CR>")
local close_key = keymaps.close or "q"
map(close_key, "<cmd>lua require('code-practice.browser').close()<CR>")
if close_key ~= "<esc>" then
map("<esc>", "<cmd>lua require('code-practice.browser').close()<CR>")
end
map("?", "<cmd>lua require('code-practice.help').show()<CR>")
end

Expand Down Expand Up @@ -298,6 +305,9 @@ end

function browser.refresh()
fetch_exercises()
if state.selected_index > #state.exercises then
state.selected_index = math.max(1, #state.exercises)
end
update_display()
end

Expand Down Expand Up @@ -376,8 +386,6 @@ function browser.close()
end

function browser.open()
state.selected_index = 1
state.current_filter = { difficulty = nil, engine = nil, search = "" }
browser.create_popup()
end

Expand Down
16 changes: 8 additions & 8 deletions lua/code-practice/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ M.defaults = {
close = "q",
},
exercise = {
run_tests = "<leader>r",
show_hint = "<leader>h",
view_solution = "<leader>s",
show_description = "<leader>d",
next_exercise = "<leader>n",
prev_exercise = "<leader>p",
skip_exercise = "<leader>k",
open_browser = "<leader>m",
run_tests = "<C-t>",
show_hint = "<C-i>",
view_solution = "<C-l>",
show_description = "<C-d>",
next_exercise = "<C-n>",
prev_exercise = "<C-p>",
skip_exercise = "<C-k>",
open_browser = "<C-b>",
},
},
}
Expand Down
11 changes: 4 additions & 7 deletions lua/code-practice/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ end

local config = require("code-practice.config")
local engines = require("code-practice.engines")
local popup_util = require("code-practice.popup")

local help = {}

Expand Down Expand Up @@ -113,7 +114,7 @@ function help.show()
"",
" See :help code-practice for full documentation",
"",
" Press q or <Esc> to close",
" Press q, <Esc>, or <Enter> to close",
"",
}
for _, el in ipairs(exercise_lines) do
Expand All @@ -131,15 +132,11 @@ function help.show()
end
end

local function close()
popup_util.map_close(popup.bufnr, function()
if popup and popup.winid and vim.api.nvim_win_is_valid(popup.winid) then
popup:unmount()
end
end

vim.keymap.set({ "n", "i" }, "q", close, { buffer = popup.bufnr, silent = true, nowait = true })
vim.keymap.set({ "n", "i" }, "<Esc>", close, { buffer = popup.bufnr, silent = true, nowait = true })
vim.keymap.set({ "n", "i" }, "<CR>", close, { buffer = popup.bufnr, silent = true, nowait = true })
end)
end

return help
68 changes: 33 additions & 35 deletions lua/code-practice/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,15 @@ function code_practice.run_tests()

if engine_name == "theory" then
local answer = nil
local has_answer_line = false
for _, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
if line:match("^[Aa]nswer:") then
has_answer_line = true
answer = line:match("^[Aa]nswer:%s*(%d+)")
if answer then
break
end
answer = line:match("^Answer:%s*(%d+)")
if answer then
break
end
end

if not answer then
if has_answer_line then
utils.notify("Please set 'Answer: <number>' before running tests", "error")
else
utils.notify("Missing answer. Add a line like 'Answer: 2'", "error")
end
utils.notify("Select an answer first (press 1-4)", "error")
return
end

Expand Down Expand Up @@ -243,26 +235,25 @@ end
function code_practice.show_stats()
local stats = manager.get_stats()

local msg = string.format(
[[
Code Practice Statistics
========================
Total Exercises: %d
Solved: %d

By Difficulty:
Easy: %d
Medium: %d
Hard: %d
]],
stats.total or 0,
stats.solved or 0,
stats.by_difficulty and stats.by_difficulty.easy or 0,
stats.by_difficulty and stats.by_difficulty.medium or 0,
stats.by_difficulty and stats.by_difficulty.hard or 0
)

vim.api.nvim_echo({ { msg, "Normal" } }, true, {})
local lines = {
"",
" Total Exercises: " .. (stats.total or 0),
" Solved: " .. (stats.solved or 0),
"",
" By Difficulty:",
" Easy: " .. (stats.by_difficulty and stats.by_difficulty.easy or 0),
" Medium: " .. (stats.by_difficulty and stats.by_difficulty.medium or 0),
" Hard: " .. (stats.by_difficulty and stats.by_difficulty.hard or 0),
"",
}

local bufnr, winid = popup.open_float({ width = 0.3, height = 0.3, title = " Statistics " })
popup.set_lines(bufnr, lines)
popup.map_close(bufnr, function()
if winid and vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_close(winid, true)
end
end)
end

function code_practice.get_current_exercise_id()
Expand All @@ -288,12 +279,19 @@ function code_practice.show_hints()
return
end

local msg = "Hints:\n"
local lines = { "" }
for i, hint in ipairs(hints) do
msg = msg .. string.format("%d. %s\n", i, hint)
table.insert(lines, string.format(" %d. %s", i, hint))
table.insert(lines, "")
end

vim.api.nvim_echo({ { msg, "Normal" } }, true, {})
local bufnr, winid = popup.open_float({ width = 0.5, height = 0.4, title = " Hints " })
popup.set_lines(bufnr, lines)
popup.map_close(bufnr, function()
if winid and vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_close(winid, true)
end
end)
end

function code_practice.show_solution()
Expand Down
Loading