From aeaa0e1bb9c7c758702c4bd4e2be84b0f95e3022 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:06:06 +0100 Subject: [PATCH 1/8] Improve UX for multiple choice theory questions --- lua/code-practice/init.lua | 16 +--- lua/code-practice/manager.lua | 28 +++++++ test/test_flow.lua | 136 ++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 12 deletions(-) diff --git a/lua/code-practice/init.lua b/lua/code-practice/init.lua index d967c15..2dfea31 100644 --- a/lua/code-practice/init.lua +++ b/lua/code-practice/init.lua @@ -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: ' 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 diff --git a/lua/code-practice/manager.lua b/lua/code-practice/manager.lua index 31cc530..3f0ceb6 100644 --- a/lua/code-practice/manager.lua +++ b/lua/code-practice/manager.lua @@ -129,6 +129,34 @@ function manager.open_exercise(id) vim.b[bufnr].code_practice_exercise_id = id vim.b[bufnr].code_practice_engine = exercise.engine + if exercise.engine == "theory" then + local opts_by_num = {} + for _, opt in ipairs(exercise.options or {}) do + opts_by_num[opt.option_number] = opt.option_text + end + + for num, text in pairs(opts_by_num) do + vim.api.nvim_buf_set_keymap(bufnr, "n", tostring(num), "", { + noremap = true, + nowait = true, + callback = function() + local line_count = vim.api.nvim_buf_line_count(bufnr) + for i = 0, line_count - 1 do + local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1] + if line and line:match("^Answer:") then + vim.bo[bufnr].modifiable = true + vim.api.nvim_buf_set_lines(bufnr, i, i + 1, false, { + string.format("Answer: %d [%s]", num, text), + }) + utils.notify(string.format("Selected option %d: %s", num, text), "info") + return + end + end + end, + }) + end + end + local current_win = vim.api.nvim_get_current_win() local function is_floating(win) local ok, cfg = pcall(vim.api.nvim_win_get_config, win) diff --git a/test/test_flow.lua b/test/test_flow.lua index 6bedf95..b7788e7 100644 --- a/test/test_flow.lua +++ b/test/test_flow.lua @@ -641,6 +641,142 @@ test("Engine registry: helpers return defaults for unknown engine", function() assert_eq(engines.get("nonexistent"), nil, "get returns nil") end) +-- 36. Theory UI: keymap selects correct answer, run_tests passes +test("Theory UI: keymap correct answer passes", function() + local db = require("code-practice.db") + local cp = require("code-practice.init") + local theory = db.get_all_exercises({ engine = "theory" }) + if #theory == 0 then + skip("no theory exercises in seed data") + end + + local ex_id = theory[1].id + local opts = db.get_theory_options(ex_id) + local correct_num + for _, o in ipairs(opts) do + if o.is_correct == 1 then + correct_num = o.option_number + break + end + end + if not correct_num then + skip("no correct option marked for theory exercise " .. ex_id) + end + + cp.open_exercise(ex_id) + vim.api.nvim_feedkeys(tostring(correct_num), "x", false) + + local bufnr = vim.api.nvim_get_current_buf() + local found_answer + for _, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do + found_answer = line:match("^Answer:%s*(%d+)") + if found_answer then + break + end + end + assert_eq(found_answer, tostring(correct_num), "keymap should set answer in buffer") + + local done = false + local original_show = require("code-practice.results").show + local captured_result + require("code-practice.results").show = function(result, _) + captured_result = result + done = true + end + + cp.run_tests() + + vim.wait(5000, function() + return done + end, 50) + + require("code-practice.results").show = original_show + + assert_truthy(captured_result, "result nil") + assert_truthy(captured_result.passed, "correct theory answer via keymap should pass") +end) + +-- 37. Theory UI: keymap selects wrong answer, run_tests fails +test("Theory UI: keymap wrong answer fails", function() + local db = require("code-practice.db") + local cp = require("code-practice.init") + local theory = db.get_all_exercises({ engine = "theory" }) + if #theory == 0 then + skip("no theory exercises in seed data") + end + + local ex_id = theory[1].id + local opts = db.get_theory_options(ex_id) + local correct_num + for _, o in ipairs(opts) do + if o.is_correct == 1 then + correct_num = o.option_number + break + end + end + if not correct_num then + skip("no correct option marked for theory exercise " .. ex_id) + end + + local wrong = correct_num == 1 and 2 or 1 + + cp.open_exercise(ex_id) + vim.api.nvim_feedkeys(tostring(wrong), "x", false) + + local bufnr = vim.api.nvim_get_current_buf() + local found_answer + for _, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do + found_answer = line:match("^Answer:%s*(%d+)") + if found_answer then + break + end + end + assert_eq(found_answer, tostring(wrong), "keymap should set wrong answer in buffer") + + local done = false + local original_show = require("code-practice.results").show + local captured_result + require("code-practice.results").show = function(result, _) + captured_result = result + done = true + end + + cp.run_tests() + + vim.wait(5000, function() + return done + end, 50) + + require("code-practice.results").show = original_show + + assert_truthy(captured_result, "result nil") + assert_truthy(not captured_result.passed, "wrong theory answer via keymap should fail") +end) + +-- 38. Importer: theory options have correct is_correct values after import +test("Importer: only correct theory option has is_correct=1", function() + local importer = require("code-practice.importer") + local db_mod = require("code-practice.db") + + local plugin_root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":h:h") + local fixture = plugin_root .. "/test/example_exercises.json" + + local counts, err = importer.import(fixture, { replace = true }) + assert_truthy(counts, "import returned nil: " .. tostring(err)) + + local conn = db_mod.connect() + local rows = conn:eval("SELECT exercise_id, COUNT(*) as cnt FROM theory_options WHERE is_correct = 1 GROUP BY exercise_id") + if not rows or (not rows[1] and next(rows) == nil) then + return + end + if rows[1] == nil and next(rows) ~= nil then + rows = { rows } + end + for _, row in ipairs(rows) do + assert_eq(row.cnt, 1, "exercise " .. row.exercise_id .. " should have exactly 1 correct option, got " .. row.cnt) + end +end) + -- Summary io.write("\n" .. string.rep("=", 44) .. "\n") io.write(string.format(" Results: %d passed, %d failed, %d skipped\n", passed, failed, skipped)) From 72df6a5eb043ed67f9d199aa0487743b00036311 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:14:43 +0100 Subject: [PATCH 2/8] Add guidance on answer selection --- lua/code-practice/manager.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/code-practice/manager.lua b/lua/code-practice/manager.lua index 3f0ceb6..435e6f6 100644 --- a/lua/code-practice/manager.lua +++ b/lua/code-practice/manager.lua @@ -89,6 +89,8 @@ function manager.open_exercise(id) for _, opt in ipairs(theory_options) do add_meta(string.format("%d. %s", opt.option_number, opt.option_text)) end + add_meta("") + add_meta("Press 1-" .. #theory_options .. " to select your answer, then run tests.") end end From e8ea83add0a9b003a13fa75e53747b74620a08d8 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:27:24 +0100 Subject: [PATCH 3/8] Try to improve ergonomics --- README.md | 29 ++++++++++++----------- doc/code-practice.txt | 44 ++++++++++++++++++----------------- lua/code-practice/config.lua | 16 ++++++------- lua/code-practice/manager.lua | 2 +- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index af4eeac..fff2fb3 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ Quick Start 1. Open browser: `:CP` (or `cp`) 2. Navigate with `j`/`k`, open with `Enter` 3. Write your solution in the buffer -4. Run tests: `r` (or `:CPRun`) -5. Move on: `n` (or `:CPNext`) +4. Run tests: `Ctrl-t` (or `:CPRun`) +5. Move on: `Ctrl-n` (or `:CPNext`) Commands -------- @@ -120,18 +120,19 @@ Global Keymaps Exercise Buffer Keymaps ----------------------- -These are active only inside exercise buffers (set via `keymaps.exercise` config): - -| Key | Action | -|----------------|---------------------------------| -| `r` | Run tests | -| `h` | Show hints | -| `s` | View solution (split) | -| `d` | Show description | -| `n` | Next exercise | -| `p` | Previous exercise | -| `k` | Skip exercise | -| `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-s` | Skip exercise | +| `Ctrl-h` | Show hints | +| `Ctrl-l` | View solution (split) | +| `Ctrl-d` | Show description | +| `Ctrl-b` | Open browser | Tools ----- diff --git a/doc/code-practice.txt b/doc/code-practice.txt index 66b8307..0b13399 100644 --- a/doc/code-practice.txt +++ b/doc/code-practice.txt @@ -68,8 +68,8 @@ After installing, populate the exercise database with |:CPImport| or 1. Open the browser |:CP| or `cp` 2. Navigate with `j`/`k` Press `Enter` to open an exercise 3. Write your solution Edit the buffer -4. Run tests |:CPRun| or `r` -5. Move on |:CPNext| or `n` +4. Run tests |:CPRun| or `Ctrl-t` +5. Move on |:CPNext| or `Ctrl-n` ============================================================================== 4. COMMANDS *code-practice-commands* @@ -85,8 +85,8 @@ All commands support tab completion. Type `:CP` 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 @@ -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 ~ - `r` Run tests (|:CPRun|) - `h` Show hints (|:CPHint|) - `s` View solution (|:CPSolution|) - `d` Show description (|:CPDesc|) - `n` Next exercise (|:CPNext|) - `p` Previous exercise (|:CPPrev|) - `k` Skip exercise (|:CPSkip|) - `m` Open browser (|:CP|) + `Ctrl-t` Run tests (|:CPRun|) + `Ctrl-n` Next exercise (|:CPNext|) + `Ctrl-p` Previous exercise (|:CPPrev|) + `Ctrl-s` Skip exercise (|:CPSkip|) + `Ctrl-h` Show hints (|:CPHint|) + `Ctrl-l` View solution (|:CPSolution|) + `Ctrl-d` Show description (|:CPDesc|) + `Ctrl-b` Open browser (|:CP|) GLOBAL KEYMAPS ~ *code-practice-keymaps-global* @@ -227,14 +229,14 @@ Options are merged with |vim.tbl_deep_extend()|. close = "q", }, exercise = { - run_tests = "r", - show_hint = "h", - view_solution = "s", - show_description = "d", - next_exercise = "n", - prev_exercise = "p", - skip_exercise = "k", - open_browser = "m", + run_tests = "", + show_hint = "", + view_solution = "", + show_description = "", + next_exercise = "", + prev_exercise = "", + skip_exercise = "", + open_browser = "", }, }, }) diff --git a/lua/code-practice/config.lua b/lua/code-practice/config.lua index ce6474f..c1ef3de 100644 --- a/lua/code-practice/config.lua +++ b/lua/code-practice/config.lua @@ -46,14 +46,14 @@ M.defaults = { close = "q", }, exercise = { - run_tests = "r", - show_hint = "h", - view_solution = "s", - show_description = "d", - next_exercise = "n", - prev_exercise = "p", - skip_exercise = "k", - open_browser = "m", + run_tests = "", + show_hint = "", + view_solution = "", + show_description = "", + next_exercise = "", + prev_exercise = "", + skip_exercise = "", + open_browser = "", }, }, } diff --git a/lua/code-practice/manager.lua b/lua/code-practice/manager.lua index 435e6f6..e9ed4ce 100644 --- a/lua/code-practice/manager.lua +++ b/lua/code-practice/manager.lua @@ -90,7 +90,7 @@ function manager.open_exercise(id) add_meta(string.format("%d. %s", opt.option_number, opt.option_text)) end add_meta("") - add_meta("Press 1-" .. #theory_options .. " to select your answer, then run tests.") + add_meta("Press 1-" .. #theory_options .. " to select your answer, then Ctrl-t to run tests.") end end From 2955194f064606fc5e3bf2e2ba4207491397ffe5 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:39:42 +0100 Subject: [PATCH 4/8] Don't hide exercises already viewed --- lua/code-practice/manager.lua | 6 +++++- test/test_flow.lua | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lua/code-practice/manager.lua b/lua/code-practice/manager.lua index e9ed4ce..72c26b5 100644 --- a/lua/code-practice/manager.lua +++ b/lua/code-practice/manager.lua @@ -41,18 +41,22 @@ function manager.open_exercise(id) local bufname = string.format("code-practice://exercise/%d", id) local bufnr = vim.fn.bufnr(bufname) local is_new_buf = bufnr == -1 + local needs_content = is_new_buf if is_new_buf then bufnr = vim.api.nvim_create_buf(true, false) vim.api.nvim_buf_set_name(bufnr, bufname) + elseif not vim.api.nvim_buf_is_loaded(bufnr) then + needs_content = true end local filetype = engines.filetype(exercise.engine) vim.bo[bufnr].buftype = "nofile" + vim.bo[bufnr].bufhidden = "hide" vim.bo[bufnr].filetype = filetype vim.bo[bufnr].swapfile = false - if is_new_buf then + if needs_content then vim.bo[bufnr].modifiable = true vim.bo[bufnr].readonly = false diff --git a/test/test_flow.lua b/test/test_flow.lua index b7788e7..8b7155b 100644 --- a/test/test_flow.lua +++ b/test/test_flow.lua @@ -777,6 +777,27 @@ test("Importer: only correct theory option has is_correct=1", function() end end) +-- 39. Reopening an unloaded exercise buffer repopulates content +test("Reopen unloaded exercise: buffer content is restored", function() + local cp = require("code-practice.init") + + local buf1 = cp.open_exercise(1) + assert_truthy(buf1, "open exercise 1") + + local lines_before = vim.api.nvim_buf_get_lines(buf1, 0, -1, false) + assert_truthy(table.concat(lines_before, "\n"):find("Exercise:"), "buffer should have content") + + cp.open_exercise(2) + vim.cmd("bunload " .. buf1) + assert_truthy(not vim.api.nvim_buf_is_loaded(buf1), "buffer should be unloaded after bunload") + + local buf1_again = cp.open_exercise(1) + assert_truthy(buf1_again, "reopen exercise 1") + local lines_after = vim.api.nvim_buf_get_lines(buf1_again, 0, -1, false) + local content = table.concat(lines_after, "\n") + assert_truthy(content:find("Exercise:"), "unloaded buffer should be repopulated with content") +end) + -- Summary io.write("\n" .. string.rep("=", 44) .. "\n") io.write(string.format(" Results: %d passed, %d failed, %d skipped\n", passed, failed, skipped)) From 48c8e52f4979f88ab8e37a919c0d0613bde9b387 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:47:51 +0100 Subject: [PATCH 5/8] Resolve command collisions and more --- doc/code-practice.txt | 8 +++--- lua/code-practice/browser.lua | 5 ++-- lua/code-practice/config.lua | 4 +-- lua/code-practice/init.lua | 52 +++++++++++++++++++---------------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/doc/code-practice.txt b/doc/code-practice.txt index 0b13399..6361831 100644 --- a/doc/code-practice.txt +++ b/doc/code-practice.txt @@ -169,8 +169,8 @@ via `keymaps.exercise` in config. `Ctrl-t` Run tests (|:CPRun|) `Ctrl-n` Next exercise (|:CPNext|) `Ctrl-p` Previous exercise (|:CPPrev|) - `Ctrl-s` Skip exercise (|:CPSkip|) - `Ctrl-h` Show hints (|:CPHint|) + `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|) @@ -230,12 +230,12 @@ Options are merged with |vim.tbl_deep_extend()|. }, exercise = { run_tests = "", - show_hint = "", + show_hint = "", view_solution = "", show_description = "", next_exercise = "", prev_exercise = "", - skip_exercise = "", + skip_exercise = "", open_browser = "", }, }, diff --git a/lua/code-practice/browser.lua b/lua/code-practice/browser.lua index 75fe1e3..8434685 100644 --- a/lua/code-practice/browser.lua +++ b/lua/code-practice/browser.lua @@ -298,6 +298,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 @@ -376,8 +379,6 @@ function browser.close() end function browser.open() - state.selected_index = 1 - state.current_filter = { difficulty = nil, engine = nil, search = "" } browser.create_popup() end diff --git a/lua/code-practice/config.lua b/lua/code-practice/config.lua index c1ef3de..b032af9 100644 --- a/lua/code-practice/config.lua +++ b/lua/code-practice/config.lua @@ -47,12 +47,12 @@ M.defaults = { }, exercise = { run_tests = "", - show_hint = "", + show_hint = "", view_solution = "", show_description = "", next_exercise = "", prev_exercise = "", - skip_exercise = "", + skip_exercise = "", open_browser = "", }, }, diff --git a/lua/code-practice/init.lua b/lua/code-practice/init.lua index 2dfea31..6c3d182 100644 --- a/lua/code-practice/init.lua +++ b/lua/code-practice/init.lua @@ -235,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() @@ -280,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() From d11ffbd5084adb415d88ebdc18656b96cade1411 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 00:49:52 +0100 Subject: [PATCH 6/8] Fix README --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fff2fb3..fa776d4 100644 --- a/README.md +++ b/README.md @@ -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) @@ -62,8 +62,7 @@ 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 ----------- @@ -128,8 +127,8 @@ single-chord access (configurable via `keymaps.exercise`): | `Ctrl-t` | Run tests | | `Ctrl-n` | Next exercise | | `Ctrl-p` | Previous exercise | -| `Ctrl-s` | Skip exercise | -| `Ctrl-h` | Show hints | +| `Ctrl-k` | Skip exercise | +| `Ctrl-i` | Show hints | | `Ctrl-l` | View solution (split) | | `Ctrl-d` | Show description | | `Ctrl-b` | Open browser | From 199ed2f66e59c767b4e59267726e504f9b19df52 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 01:05:17 +0100 Subject: [PATCH 7/8] Improve ergonomics --- lua/code-practice/browser.lua | 21 ++++++++++++++------- lua/code-practice/help.lua | 11 ++++------- lua/code-practice/manager.lua | 34 ++++++++++++++++------------------ lua/code-practice/results.lua | 11 ++++++++--- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/lua/code-practice/browser.lua b/lua/code-practice/browser.lua index 8434685..5266b76 100644 --- a/lua/code-practice/browser.lua +++ b/lua/code-practice/browser.lua @@ -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 @@ -244,10 +248,10 @@ function browser.setup_keymaps() map("", "lua require('code-practice.browser').open_selected()") end map("o", "lua require('code-practice.browser').open_selected()") - map("e", "lua require('code-practice.browser').filter_by_difficulty('easy')") - map("m", "lua require('code-practice.browser').filter_by_difficulty('medium')") - map("h", "lua require('code-practice.browser').filter_by_difficulty('hard')") - map("a", "lua require('code-practice.browser').clear_filters()") + map(keymaps.filter_easy or "e", "lua require('code-practice.browser').filter_by_difficulty('easy')") + map(keymaps.filter_medium or "m", "lua require('code-practice.browser').filter_by_difficulty('medium')") + map(keymaps.filter_hard or "h", "lua require('code-practice.browser').filter_by_difficulty('hard')") + map(keymaps.filter_all or "a", "lua require('code-practice.browser').clear_filters()") for _, name in ipairs(engines.list()) do local eng = engines.get(name) @@ -256,8 +260,11 @@ function browser.setup_keymaps() end end - map("q", "lua require('code-practice.browser').close()") - map("", "lua require('code-practice.browser').close()") + local close_key = keymaps.close or "q" + map(close_key, "lua require('code-practice.browser').close()") + if close_key ~= "" then + map("", "lua require('code-practice.browser').close()") + end map("?", "lua require('code-practice.help').show()") end diff --git a/lua/code-practice/help.lua b/lua/code-practice/help.lua index b35ffc4..58cdc64 100644 --- a/lua/code-practice/help.lua +++ b/lua/code-practice/help.lua @@ -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 = {} @@ -113,7 +114,7 @@ function help.show() "", " See :help code-practice for full documentation", "", - " Press q or to close", + " Press q, , or to close", "", } for _, el in ipairs(exercise_lines) do @@ -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" }, "", close, { buffer = popup.bufnr, silent = true, nowait = true }) - vim.keymap.set({ "n", "i" }, "", close, { buffer = popup.bufnr, silent = true, nowait = true }) + end) end return help diff --git a/lua/code-practice/manager.lua b/lua/code-practice/manager.lua index 72c26b5..6885b8e 100644 --- a/lua/code-practice/manager.lua +++ b/lua/code-practice/manager.lua @@ -1,4 +1,5 @@ -- Code Practice - Exercise Manager Module +local config = require("code-practice.config") local db = require("code-practice.db") local engines = require("code-practice.engines") local utils = require("code-practice.utils") @@ -94,7 +95,8 @@ function manager.open_exercise(id) add_meta(string.format("%d. %s", opt.option_number, opt.option_text)) end add_meta("") - add_meta("Press 1-" .. #theory_options .. " to select your answer, then Ctrl-t to run tests.") + local run_key = (config.get("keymaps.exercise") or {}).run_tests or "" + add_meta("Press 1-" .. #theory_options .. " to select your answer, then " .. run_key .. " to run tests.") end end @@ -142,24 +144,20 @@ function manager.open_exercise(id) end for num, text in pairs(opts_by_num) do - vim.api.nvim_buf_set_keymap(bufnr, "n", tostring(num), "", { - noremap = true, - nowait = true, - callback = function() - local line_count = vim.api.nvim_buf_line_count(bufnr) - for i = 0, line_count - 1 do - local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1] - if line and line:match("^Answer:") then - vim.bo[bufnr].modifiable = true - vim.api.nvim_buf_set_lines(bufnr, i, i + 1, false, { - string.format("Answer: %d [%s]", num, text), - }) - utils.notify(string.format("Selected option %d: %s", num, text), "info") - return - end + vim.keymap.set("n", tostring(num), function() + local line_count = vim.api.nvim_buf_line_count(bufnr) + for i = 0, line_count - 1 do + local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1] + if line and line:match("^Answer:") then + vim.bo[bufnr].modifiable = true + vim.api.nvim_buf_set_lines(bufnr, i, i + 1, false, { + string.format("Answer: %d [%s]", num, text), + }) + utils.notify(string.format("Selected option %d: %s", num, text), "info") + return end - end, - }) + end + end, { buffer = bufnr, noremap = true, nowait = true }) end end diff --git a/lua/code-practice/results.lua b/lua/code-practice/results.lua index 7848f7b..706a6ca 100644 --- a/lua/code-practice/results.lua +++ b/lua/code-practice/results.lua @@ -1,4 +1,5 @@ -- Code Practice - Results Display Module +local config = require("code-practice.config") local popup = require("code-practice.popup") local results = {} @@ -43,7 +44,9 @@ function results.show(result, on_next) end end - if result.passed then + if result.correct_option then + push(result.passed and "✓ Correct!" or "✗ Incorrect") + elseif result.passed then push("✓ All tests passed!") else push("✗ Some tests failed") @@ -82,9 +85,11 @@ function results.show(result, on_next) push("No detailed results available.") end + local next_key = (config.get("keymaps.exercise") or {}).next_exercise or "" + push("") if on_next then - push("Press n for next exercise | q, , or to close") + push("Press " .. next_key .. " for next exercise | q, , or to close") else push("Press q, , or to close") end @@ -96,7 +101,7 @@ function results.show(result, on_next) popup.map_close(bufnr, results.close) if on_next then - vim.keymap.set("n", "n", on_next, { buffer = bufnr, silent = true, nowait = true }) + vim.keymap.set("n", next_key, on_next, { buffer = bufnr, silent = true, nowait = true }) end if result.passed then From 2347dba40918fab891e0661c2ae7f6df6a09ee41 Mon Sep 17 00:00:00 2001 From: "davide.fiocco" Date: Sat, 28 Feb 2026 01:15:58 +0100 Subject: [PATCH 8/8] Add rationales to theory questions --- test/example_exercises.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/example_exercises.json b/test/example_exercises.json index 39ae18c..001c8e6 100644 --- a/test/example_exercises.json +++ b/test/example_exercises.json @@ -991,7 +991,7 @@ "Count total iterations of the innermost operation.", "Outer loop runs n times. Inner loop runs n/2 times per outer iteration." ], - "solution": "", + "solution": "The algorithm has an outer loop running n times and an inner loop running n/2 times per outer iteration, resulting in n × (n/2) = n²/2 total operations. Since constant factors are ignored in Big O notation, this simplifies to O(n²). Option 1 (O(n)) is too fast, as the nested loops cause quadratic growth. Option 2 (O(n log n)) would require the inner loop to scale logarithmically, which it does not. Option 4 (O(n³)) overestimates the complexity, as there are only two nested loops, not three.", "starter_code": "", "created_at": "2026-02-23 14:02:15", "updated_at": "2026-02-23 14:02:15", @@ -1034,7 +1034,7 @@ "Recall the hierarchy: constant < logarithmic < polynomial < exponential < factorial.", "Consider comparing logarithmic, linear, n log n, and exponential functions." ], - "solution": "", + "solution": "Option 4, O(2ⁿ), grows asymptotically the fastest because exponential functions like 2ⁿ dominate polynomial and logarithmic functions as n → ∞. O(log n) grows slower than any polynomial, O(n) is linear, and O(n log n) is slightly faster than linear but still polynomial—none of which can keep up with the rapid growth of an exponential function.", "starter_code": "", "created_at": "2026-02-23 14:02:15", "updated_at": "2026-02-23 14:02:15", @@ -1077,7 +1077,7 @@ "Consider the maximum depth of the recursion call stack.", "Each recursive call adds a frame to the stack until reaching the base cases." ], - "solution": "", + "solution": "The space complexity is O(n) because the recursion depth reaches n in the worst case (e.g., when computing fib(n), the call stack grows linearly as it goes down to fib(0)), and each recursive call adds a frame to the call stack. Although the time complexity is O(2ⁿ), space complexity depends on maximum stack depth, not total calls. O(1) is incorrect because space usage isn’t constant—it grows with input size. O(log n) would apply to some optimized or tail-recursive versions, but not the naive recursive version.", "starter_code": "", "created_at": "2026-02-23 14:02:15", "updated_at": "2026-02-23 14:02:15", @@ -1120,7 +1120,7 @@ "Although occasional pushes trigger resizing (O(n)), most are O(1).", "Use the accounting method or aggregate analysis to find average cost per operation." ], - "solution": "", + "solution": "The amortized time complexity of a single push operation in a dynamically resizing array-based stack is O(1) because, although occasional resizes take O(n) time when the array doubles, these expensive operations are rare and their cost is spread out over many cheap pushes using the accounting or aggregate method of amortized analysis. Option 2 (O(n)) is the worst-case time for a single push during a resize, not the amortized time. Option 3 (O(log n)) and Option 4 (O(n log n)) do not reflect the actual behavior of geometric resizing strategies like doubling.", "starter_code": "", "created_at": "2026-02-23 14:02:15", "updated_at": "2026-02-23 14:02:15", @@ -1163,7 +1163,7 @@ "Big-Theta requires both upper and lower bounds that match asymptotically.", "Ignore lower-order terms and constant coefficients." ], - "solution": "", + "solution": "The correct answer is Θ(n²) because in Big-Theta notation, we focus on the dominant term as n grows large, and 3n² dominates the lower-order terms 5n and 2. Option 1 (Θ(n)) is too small since n² grows faster than n; Option 3 (Θ(n³)) is too large because n² grows slower than n³; Option 4 (Θ(log n)) is even smaller and incorrect for a quadratic function.", "starter_code": "", "created_at": "2026-02-23 14:02:15", "updated_at": "2026-02-23 14:02:15", @@ -1205,7 +1205,7 @@ "Linear structures store elements in a sequential manner.", "Arrays and linked lists are examples of linear structures." ], - "solution": "", + "solution": "Stack is a linear data structure because its elements are arranged in a sequential order where insertion and deletion occur at only one end (top), following the LIFO principle. Binary Tree, Graph, and Heap are non-linear: Binary Trees and Graphs have hierarchical or network-like connections between nodes, while Heaps are a specialized tree-based structure (usually implemented as a complete binary tree).", "starter_code": "", "created_at": "2026-02-23 14:02:22", "updated_at": "2026-02-23 14:02:22", @@ -1247,7 +1247,7 @@ "Arrays store elements in contiguous memory locations.", "Index-based access does not require traversal." ], - "solution": "", + "solution": "Accessing an element by index in an array is O(1) because arrays store elements in contiguous memory locations, allowing direct calculation of the element's address using the base address and index (base + index × element_size), which takes constant time. Options O(n), O(log n), and O(n log n) describe complexities for operations like linear search, binary search, or sorting—not direct index-based access.", "starter_code": "", "created_at": "2026-02-23 14:02:22", "updated_at": "2026-02-23 14:02:22", @@ -1289,7 +1289,7 @@ "Think about the order in which elements are added and removed.", "It's the same principle as stacking plates." ], - "solution": "", + "solution": "Stacks follow the Last In, First Out (LIFO) principle, meaning the last element added is the first one removed. Option 1 (FIFO) describes queues, not stacks. Option 3 (PIPO) is not a standard data structure principle, and Option 4 (Random Access) applies to structures like arrays, where elements can be accessed directly by index.", "starter_code": "", "created_at": "2026-02-23 14:02:22", "updated_at": "2026-02-23 14:02:22", @@ -1331,7 +1331,7 @@ "One method stores multiple items in the same bucket.", "Another uses probing to find the next available slot." ], - "solution": "", + "solution": "Quadratic probing is a common open addressing technique for resolving hash collisions by probing positions in a quadratic sequence when a collision occurs. Merge sort, binary search, and depth-first search are unrelated algorithms—merge sort is a sorting algorithm, binary search is for searching sorted arrays, and depth-first search is a graph traversal method.", "starter_code": "", "created_at": "2026-02-23 14:02:22", "updated_at": "2026-02-23 14:02:22", @@ -1373,7 +1373,7 @@ "One part stores data.", "The other part points to the next element." ], - "solution": "", + "solution": "Option 3 is correct because a node in a singly linked list consists of two parts: the data (or value) it holds and a pointer to the next node in the sequence. Option 1 is incorrect because \"Parent Pointer\" is not used in singly linked lists (it appears in trees or doubly linked lists). Option 2 is misleading—while some implementations store key-value pairs, the fundamental structure is data and next pointer, and \"key\" isn't a required component. Option 4 is wrong because a previous pointer is characteristic of a doubly linked list, not a singly linked list.", "starter_code": "", "created_at": "2026-02-23 14:02:22", "updated_at": "2026-02-23 14:02:22",