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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ single-chord access (configurable via `keymaps.exercise`):
| `Ctrl-n` | Next exercise |
| `Ctrl-p` | Previous exercise |
| `Ctrl-k` | Skip exercise |
| `Ctrl-i` | Show hints |
| `Ctrl-g` | Show hints |
| `Ctrl-l` | View solution (split) |
| `Ctrl-d` | Show description |
| `Ctrl-b` | Open browser |
Expand Down Expand Up @@ -158,7 +158,7 @@ Or from Neovim: `:CP generate` (prompts for topic, count, difficulty, and engine

### AI Hints

When enabled, `Ctrl-i` generates a context-aware hint using a Hugging Face model
When enabled, `Ctrl-g` generates a context-aware hint using a Hugging Face model
instead of showing static hints. The hint is based on your current buffer and the
reference solution.

Expand Down
8 changes: 3 additions & 5 deletions doc/code-practice.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Tab completion is supported: type `:CP <Tab>` to explore.
schema matches `test/example_exercises.json` in the
plugin source.

:CP import! {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}.

:CP generate Interactively generate exercises via Hugging Face
Expand Down Expand Up @@ -146,7 +146,7 @@ via `keymaps.exercise` in config.
`Ctrl-n` Next exercise
`Ctrl-p` Previous exercise
`Ctrl-k` Skip exercise
`Ctrl-i` Show hints
`Ctrl-g` Show hints
`Ctrl-l` View solution
`Ctrl-d` Show description
`Ctrl-b` Open browser
Expand Down Expand Up @@ -181,8 +181,6 @@ Options are merged with |vim.tbl_deep_extend()|.

runner = {
timeout = 5, -- seconds per test case
show_time = true,
auto_save = true,
},

ai_hints = {
Expand All @@ -202,7 +200,7 @@ Options are merged with |vim.tbl_deep_extend()|.
},
exercise = {
run_tests = "<C-t>",
show_hint = "<C-i>",
show_hint = "<C-g>",
view_solution = "<C-l>",
show_description = "<C-d>",
next_exercise = "<C-n>",
Expand Down
10 changes: 10 additions & 0 deletions lua/code-practice/browser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ local function update_display()
vim.bo[preview_buf].modifiable = false
vim.bo[list_buf].readonly = true
vim.bo[preview_buf].readonly = true

local list_win = state.popup.list.winid
if
list_win
and vim.api.nvim_win_is_valid(list_win)
and state.selected_index > 0
and state.selected_index <= #list_lines
then
vim.api.nvim_win_set_cursor(list_win, { state.selected_index, 0 })
end
end

function browser.refresh()
Expand Down
5 changes: 2 additions & 3 deletions lua/code-practice/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ config.defaults = {

runner = {
timeout = 5,
show_time = true,
auto_save = true,
},

ai_hints = {
Expand All @@ -52,7 +50,7 @@ config.defaults = {
},
exercise = {
run_tests = "<C-t>",
show_hint = "<C-i>",
show_hint = "<C-g>",
view_solution = "<C-l>",
show_description = "<C-d>",
next_exercise = "<C-n>",
Expand All @@ -67,6 +65,7 @@ config.config = vim.deepcopy(config.defaults)

function config.setup(user_config)
user_config = user_config or {}
config.defaults.engines = build_engine_defaults()
config.config = vim.tbl_deep_extend("force", config.defaults, user_config)

vim.fn.mkdir(config.config.storage.home, "p")
Expand Down
4 changes: 2 additions & 2 deletions lua/code-practice/engines.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ engines.registry = {
enabled = true,
cmd = "python3",
},
run_cmd = function(cfg)
return cfg.cmd or "python3"
run_cmd = function(cfg, file)
return { cfg.cmd or "python3", file }
end,
wrap_test = function(code, input)
local call = (input or ""):match("^%s*$") and "solution()" or ("solution(" .. input .. ")")
Expand Down
77 changes: 22 additions & 55 deletions lua/code-practice/help.lua
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
-- Code Practice - Keymap Cheat-Sheet
local ok, Popup = pcall(require, "nui.popup")
if not ok then
vim.notify("[code-practice] nui.nvim not found. Install MunifTanjim/nui.nvim", vim.log.levels.ERROR)
return {}
end

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

local help = {}

Expand All @@ -28,42 +22,20 @@ end
function help.show()
local width = math.min(90, vim.o.columns - 4)
local height = math.min(30, vim.o.lines - 4)
local row = math.max(1, math.floor((vim.o.lines - height) / 2))
local col = math.max(1, math.floor((vim.o.columns - width) / 2))

local popup = Popup({
relative = "editor",
position = {
row = row,
col = col,
},
size = {
width = width,
height = height,
},
border = {
style = "rounded",
text = {
top = " Keymaps ",
top_align = "center",
},
},
buf_options = {
modifiable = true,
readonly = false,
},
win_options = {
winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
},
})

popup:mount()
if popup.winid then
vim.api.nvim_set_current_win(popup.winid)
end
vim.cmd("stopinsert")

local bufnr, _, close_fn = popup.open_float({ width = width, height = height, title = " Keymaps " })

local km = config.get("keymaps.exercise", {})
local bkm = config.get("keymaps.browser", {})

-- Two-column row with the right column aligned to a fixed offset.
local function row(lkey, ldesc, rkey, rdesc)
local left = " " .. pad(fmt_key(lkey), 19) .. ldesc
if rkey == nil and rdesc == nil then
return left
end
return pad(left, 54) .. pad(fmt_key(rkey), 17) .. (rdesc or "")
end

local filter_lines = {}
for _, name in ipairs(engines.list()) do
Expand All @@ -73,21 +45,22 @@ function help.show()
end
end

local open_key = bkm.open_item or bkm.open or "<CR>"

local lines = {
"",
" BROWSER",
" " .. string.rep("─", width - 4),
" j / k Move up / down Enter / o Open exercise",
" e Filter by Easy difficulty m Filter by Medium",
" h Filter by Hard difficulty a Clear all filters",
row("j / k", "Move up / down", open_key .. " / o", "Open exercise"),
row(bkm.filter_easy or "e", "Filter by Easy difficulty", bkm.filter_medium or "m", "Filter by Medium"),
row(bkm.filter_hard or "h", "Filter by Hard difficulty", bkm.filter_all or "a", "Clear all filters"),
}

for _, fl in ipairs(filter_lines) do
table.insert(lines, fl)
end

table.insert(lines, " q / Esc Close browser")
table.insert(lines, " ? Show this cheat-sheet")
table.insert(lines, row(bkm.close or "q", "Close browser", "?", "Show this cheat-sheet"))

local exercise_lines = {
"",
Expand Down Expand Up @@ -122,22 +95,16 @@ function help.show()
table.insert(lines, el)
end

vim.bo[popup.bufnr].modifiable = true
vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, lines)
vim.bo[popup.bufnr].modifiable = false
popup.set_lines(bufnr, lines)

local ns_help = vim.api.nvim_create_namespace("code_practice_help")
for i, line in ipairs(lines) do
if line:match("^ [A-Z]") and not line:match("^ See") and not line:match("^ Press") then
vim.api.nvim_buf_add_highlight(popup.bufnr, ns_help, "Underlined", i - 1, 0, -1)
vim.api.nvim_buf_add_highlight(bufnr, ns_help, "Underlined", i - 1, 0, -1)
end
end

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)
popup.map_close(bufnr, close_fn)
end

return help
29 changes: 21 additions & 8 deletions lua/code-practice/importer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ local utils = require("code-practice.utils")

local importer = {}

-- sqlite.lua's stmt:bind treats strings matching "^[%S]+%(.*%)$" as SQL
-- functions and skips binding them (see kkharji/sqlite.lua#87). Values like
-- "O(n)" or "O(n log n)" trigger this and silently become NULL. We work
-- around it by formatting values directly into the SQL string.
local function sql_escape(val)
if val == nil then
return "NULL"
elseif type(val) == "number" then
return tostring(val)
else
return "'" .. tostring(val):gsub("'", "''") .. "'"
end
end

function importer.import(json_path, opts)
opts = opts or {}

Expand Down Expand Up @@ -101,14 +115,13 @@ function importer.import(json_path, opts)
local opt_ok, opt_err = pcall(
conn.eval,
conn,
[[INSERT INTO theory_options (exercise_id, option_number, option_text, is_correct)
VALUES (:eid, :num, :text, :correct)]],
{
eid = ex.id,
num = opt.option_number,
text = opt.option_text,
correct = opt.is_correct == 1 and 1 or 0,
}
string.format(
"INSERT INTO theory_options (exercise_id, option_number, option_text, is_correct) VALUES (%s, %s, %s, %s)",
sql_escape(ex.id),
sql_escape(opt.option_number),
sql_escape(opt.option_text),
sql_escape((opt.is_correct == 1 or opt.is_correct == true) and 1 or 0)
)
)
if opt_ok then
counts.theory_options = counts.theory_options + 1
Expand Down
26 changes: 9 additions & 17 deletions lua/code-practice/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,9 @@ function code_practice.show_stats()
"",
}

local bufnr, winid = popup.open_float({ width = 0.3, height = 0.3, title = " Statistics " })
local bufnr, _, close_fn = popup.open_float({ width = 0.3, height = 0.3, title = " Statistics " })
popup.set_lines(bufnr, lines)
popup.map_close(bufnr, function()
utils.close_win(winid)
end)
popup.map_close(bufnr, close_fn)
end

function code_practice.get_current_exercise_id()
Expand All @@ -264,11 +262,9 @@ local function show_static_hints(exercise)
table.insert(lines, "")
end

local bufnr, winid = popup.open_float({ width = 0.5, height = 0.4, title = " Hints " })
local bufnr, _, close_fn = popup.open_float({ width = 0.5, height = 0.4, title = " Hints " })
popup.set_lines(bufnr, lines)
popup.map_close(bufnr, function()
utils.close_win(winid)
end)
popup.map_close(bufnr, close_fn)
end

function code_practice.show_hints()
Expand All @@ -289,11 +285,9 @@ function code_practice.show_hints()
end

local buffer_content = utils.get_buffer_content(vim.api.nvim_get_current_buf())
local hint_bufnr, hint_winid = popup.open_float({ width = 0.5, height = 0.4, title = " AI Hint " })
local hint_bufnr, _, hint_close = popup.open_float({ width = 0.5, height = 0.4, title = " AI Hint " })
popup.set_lines(hint_bufnr, { "", " Generating hint..." })
popup.map_close(hint_bufnr, function()
utils.close_win(hint_winid)
end)
popup.map_close(hint_bufnr, hint_close)

local ai_hints = require("code-practice.ai_hints")
ai_hints.generate(exercise, buffer_content, function(hint_text, err)
Expand All @@ -303,7 +297,7 @@ function code_practice.show_hints()

if err then
utils.notify("AI hint failed: " .. err, "error")
utils.close_win(hint_winid)
hint_close()
show_static_hints(exercise)
return
end
Expand Down Expand Up @@ -370,11 +364,9 @@ function code_practice.show_description()
footer = "Press q, <Esc>, or <Enter> to close",
})

local bufnr, winid = popup.open_float({ filetype = "markdown", title = " Description " })
local bufnr, _, close_fn = popup.open_float({ filetype = "markdown", title = " Description " })
popup.set_lines(bufnr, lines)
popup.map_close(bufnr, function()
utils.close_win(winid)
end)
popup.map_close(bufnr, close_fn)
end

function code_practice.show_help()
Expand Down
Loading
Loading