From 1255a8d88d508e6c11d0addcd67d3959a2fb76cf Mon Sep 17 00:00:00 2001 From: brianhuster Date: Sun, 13 Jul 2025 23:44:39 +0700 Subject: refactor(tutor): reimplement interactive marks as extmark in Lua MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: From https://matrix.to/#/!cylwlNXSwagQmZSkzs:matrix.org/$Ofj-TFIsEMbp0O9OhE8xuZSNi-nhRLtZTOgs6JRLNrs?via=matrix.org&via=gitter.im&via=mozilla.org In lesson 2.6, users are asked to remove the second, forth and fifth lines with `dd` command, then they are asked to undo twice to make the text go back to original state. But after that, the mark ✗ appears again, which confuses the user because they think they do something wrong. This is a limitation with the current implementation, which is based on line number only. Solution: Reimplement interactive marks as extmarks in Lua. This also make the feature less fragile, as users can remove, add some arbitrary lines without breaking the interactive marks. Co-authored-by: Justin M. Keyes --- runtime/lua/nvim/tutor.lua | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 runtime/lua/nvim/tutor.lua (limited to 'runtime/lua/nvim/tutor.lua') diff --git a/runtime/lua/nvim/tutor.lua b/runtime/lua/nvim/tutor.lua new file mode 100644 index 0000000000..2f7e571958 --- /dev/null +++ b/runtime/lua/nvim/tutor.lua @@ -0,0 +1,80 @@ +---@class nvim.TutorMetadata +---@field expect table + +---@alias nvim.TutorExtmarks table + +---@type nvim.TutorExtmarks? +vim.b.tutor_extmarks = vim.b.tutor_extmarks + +---@type nvim.TutorMetadata? +vim.b.tutor_metadata = vim.b.tutor_metadata + +local sign_text_correct = '✓' +local sign_text_incorrect = '✗' +local tutor_mark_ns = vim.api.nvim_create_namespace('nvim.tutor.mark') +local tutor_hl_ns = vim.api.nvim_create_namespace('nvim.tutor.hl') + +local M = {} + +---@param line integer 1-based +local function check_line(line) + if vim.b.tutor_metadata and vim.b.tutor_metadata.expect and vim.b.tutor_extmarks then + local ctext = vim.fn.getline(line) + + local extmarks = vim.api.nvim_buf_get_extmarks( + 0, + tutor_mark_ns, + { line - 1, 0 }, + { line - 1, -1 }, -- the extmark can move to col > 0 if users insert text there + {} + ) + for _, extmark in ipairs(extmarks) do + local mark_id = extmark[1] + local expct = vim.b.tutor_extmarks[tostring(mark_id)] + local expect = vim.b.tutor_metadata.expect[expct] + local is_correct = expect == -1 or ctext == expect + + vim.api.nvim_buf_set_extmark(0, tutor_mark_ns, line - 1, 0, { + id = mark_id, + sign_text = is_correct and sign_text_correct or sign_text_incorrect, + sign_hl_group = is_correct and 'tutorOK' or 'tutorX', + -- This may be a hack. By default, all extmarks only move forward, so a line cannot contain + -- any extmarks that were originally created for later lines. + priority = tonumber(expct), + }) + end + end +end + +function M.apply_marks() + vim.cmd [[hi! link tutorExpect Special]] + if vim.b.tutor_metadata and vim.b.tutor_metadata.expect then + vim.b.tutor_extmarks = {} + for expct, _ in pairs(vim.b.tutor_metadata.expect) do + ---@diagnostic disable-next-line: assign-type-mismatch + local lnum = tonumber(expct) ---@type integer + vim.api.nvim_buf_set_extmark(0, tutor_hl_ns, lnum - 1, 0, { + line_hl_group = 'tutorExpect', + }) + + local mark_id = vim.api.nvim_buf_set_extmark(0, tutor_mark_ns, lnum - 1, 0, {}) + + -- Cannot edit field of a Vimscript dictionary from Lua directly, see `:h lua-vim-variables` + ---@type nvim.TutorExtmarks + local tutor_extmarks = vim.b.tutor_extmarks + tutor_extmarks[tostring(mark_id)] = expct + vim.b.tutor_extmarks = tutor_extmarks + + check_line(lnum) + end + end +end + +function M.apply_marks_on_changed() + if vim.b.tutor_metadata and vim.b.tutor_metadata.expect and vim.b.tutor_extmarks then + local lnum = vim.fn.line('.') + check_line(lnum) + end +end + +return M -- cgit