diff --git a/runtime/autoload/tutor.vim b/runtime/autoload/tutor.vim index d8f0f65139..9412960195 100644 --- a/runtime/autoload/tutor.vim +++ b/runtime/autoload/tutor.vim @@ -77,46 +77,6 @@ function! tutor#TutorFolds() endif endfunction -" Marks: {{{1 - -function! tutor#ApplyMarks() - hi! link tutorExpect Special - if exists('b:tutor_metadata') && has_key(b:tutor_metadata, 'expect') - let b:tutor_sign_id = 1 - for expct in keys(b:tutor_metadata['expect']) - let lnum = eval(expct) - call matchaddpos('tutorExpect', [lnum]) - call tutor#CheckLine(lnum) - endfor - endif -endfunction - -function! tutor#ApplyMarksOnChanged() - if exists('b:tutor_metadata') && has_key(b:tutor_metadata, 'expect') - let lnum = line('.') - if index(keys(b:tutor_metadata['expect']), string(lnum)) > -1 - call tutor#CheckLine(lnum) - endif - endif -endfunction - -function! tutor#CheckLine(line) - if exists('b:tutor_metadata') && has_key(b:tutor_metadata, 'expect') - let bufn = bufnr('%') - let ctext = getline(a:line) - let signs = sign_getplaced(bufn, {'lnum': a:line})[0].signs - if !empty(signs) - call sign_unplace('', {'id': signs[0].id}) - endif - if b:tutor_metadata['expect'][string(a:line)] == -1 || ctext ==# b:tutor_metadata['expect'][string(a:line)] - exe "sign place ".b:tutor_sign_id." line=".a:line." name=tutorok buffer=".bufn - else - exe "sign place ".b:tutor_sign_id." line=".a:line." name=tutorbad buffer=".bufn - endif - let b:tutor_sign_id+=1 - endif -endfunction - " Tutor Cmd: {{{1 function! s:Locale() @@ -243,9 +203,9 @@ function! tutor#EnableInteractive(enable) setlocal buftype=nofile setlocal concealcursor+=inv setlocal conceallevel=2 - call tutor#ApplyMarks() + lua require('nvim.tutor').apply_marks() augroup tutor_interactive - autocmd! TextChanged,TextChangedI call tutor#ApplyMarksOnChanged() + autocmd! TextChanged,TextChangedI lua require('nvim.tutor').apply_marks_on_changed() augroup END else setlocal buftype< 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 diff --git a/runtime/tutor/en/vim-01-beginner.tutor b/runtime/tutor/en/vim-01-beginner.tutor index 300742ff51..5da3d68fb4 100644 --- a/runtime/tutor/en/vim-01-beginner.tutor +++ b/runtime/tutor/en/vim-01-beginner.tutor @@ -304,7 +304,7 @@ it would be easier to simply type two d's to delete a line. 3. Now move to the fourth line. - 4. Type `2dd`{normal} to delete two lines, then press `u`{normal} twice to undo all three lines. + 4. Type `2dd`{normal} to delete two lines. 1) Roses are red, 2) Mud is fun, diff --git a/runtime/tutor/en/vim-01-beginner.tutor.json b/runtime/tutor/en/vim-01-beginner.tutor.json index 18b3591842..d6cf66c799 100644 --- a/runtime/tutor/en/vim-01-beginner.tutor.json +++ b/runtime/tutor/en/vim-01-beginner.tutor.json @@ -12,11 +12,11 @@ "273": -1, "292": "This line of words is cleaned up.", "309": "1) Roses are red,", - "310": "3) Violets are blue,", - "311": "6) Sugar is sweet", - "312": "7) And so are you.", - "313": "7) And so are you.", - "314": "7) And so are you.", + "310": "", + "311": "3) Violets are blue,", + "312": "", + "313": "", + "314": "6) Sugar is sweet", "315": "7) And so are you.", "335": "Fix the errors on this line and replace them with undo.", "381": -1, diff --git a/runtime/tutor/ja/vim-01-beginner.tutor.json b/runtime/tutor/ja/vim-01-beginner.tutor.json index 6452a27db4..25edb36038 100644 --- a/runtime/tutor/ja/vim-01-beginner.tutor.json +++ b/runtime/tutor/ja/vim-01-beginner.tutor.json @@ -11,13 +11,13 @@ "233": "誰かがこの行の最後を2度タイプしました。", "272": -1, "291": "この行の単語は綺麗になった。", - "308": -1, - "309": -1, - "310": -1, - "311": -1, - "312": -1, - "313": -1, - "314": -1, + "308": "1) 薔薇は赤く", + "309": "", + "310": "3) 菫は青く", + "311": "", + "312": "", + "313": "6) 砂糖は甘く", + "314": "7) そして貴方も", "335": "この行の間違いを修正し、後でそれらの修正を取り消します。", "381": -1, "382": -1, diff --git a/runtime/tutor/zh/vim-01-beginner.tutor b/runtime/tutor/zh/vim-01-beginner.tutor index 1319e09ce0..b20a18c35d 100644 --- a/runtime/tutor/zh/vim-01-beginner.tutor +++ b/runtime/tutor/zh/vim-01-beginner.tutor @@ -287,7 +287,7 @@ This ABC DE line FGHI JK LMN OP of words is Q RS TUV cleaned up. 3. 现在移动到第 4 行。 - 4. 输入 `2dd`{normal} 来删除两行,然后按两次 `u`{normal} 来恢复这三行。 + 4. 输入 `2dd`{normal} 来删除两行。 1) Roses are red, 2) Mud is fun, diff --git a/runtime/tutor/zh/vim-01-beginner.tutor.json b/runtime/tutor/zh/vim-01-beginner.tutor.json index f85aa16288..52676db4e8 100644 --- a/runtime/tutor/zh/vim-01-beginner.tutor.json +++ b/runtime/tutor/zh/vim-01-beginner.tutor.json @@ -14,11 +14,11 @@ "259": -1, "276": "This line of words is cleaned up.", "292": "1) Roses are red,", - "293": "3) Violets are blue,", - "294": "6) Sugar is sweet", - "295": "7) And so are you.", - "296": "7) And so are you.", - "297": "7) And so are you.", + "293": "", + "294": "3) Violets are blue,", + "295": "", + "296": "", + "297": "6) Sugar is sweet", "298": "7) And so are you.", "318": "Fix the errors on this line and replace them with undo.", "319": "Fix the errors on this line and replace them with undo.", diff --git a/test/functional/plugin/tutor_spec.lua b/test/functional/plugin/tutor_spec.lua index 49feb825aa..59ba856c14 100644 --- a/test/functional/plugin/tutor_spec.lua +++ b/test/functional/plugin/tutor_spec.lua @@ -16,16 +16,21 @@ describe(':Tutor', function() command('Tutor') screen = Screen.new(81, 30) screen:set_default_attr_ids({ - [0] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.Gray }, + [0] = { foreground = Screen.colors.Blue4, background = Screen.colors.Grey }, [1] = { bold = true }, [2] = { underline = true, foreground = tonumber('0x0088ff') }, [3] = { foreground = Screen.colors.SlateBlue }, [4] = { bold = true, foreground = Screen.colors.Brown }, [5] = { bold = true, foreground = Screen.colors.Magenta1 }, [6] = { italic = true }, + [7] = { foreground = tonumber('0x00ff88'), bold = true, background = Screen.colors.Grey }, + [8] = { bold = true, foreground = Screen.colors.Blue }, + [9] = { foreground = Screen.colors.Magenta1 }, + [10] = { foreground = tonumber('0xff2000'), bold = true }, + [11] = { foreground = tonumber('0xff2000'), bold = true, background = Screen.colors.Grey }, + [12] = { foreground = tonumber('0x6a0dad') }, }) end) - it('applies {unix:…,win:…} transform', function() local expected = is_os('win') and [[ @@ -134,6 +139,152 @@ describe(':Tutor', function() feed(':983zt') screen:expect(expected) end) + + it("removing a line doesn't affect highlight/mark of other lines", function() + -- Do lesson 2.6 + feed(':294zt') + screen:expect([[ + {0: }{3:^#}{5: Lesson 2.6: OPERATING ON LINES} | + {0: } | + {0: }{1: Type }{4:dd}{1: to delete a whole line. } | + {0: } | + {0: }Due to the frequency of whole line deletion, the designers of Vi decided | + {0: }it would be easier to simply type two d's to delete a line. | + {0: } | + {0: } 1. Move the cursor to the second line in the phrase below. | + {0: } | + {0: } 2. Type {2:dd} to delete the line. | + {0: } | + {0: } 3. Now move to the fourth line. | + {0: } | + {0: } 4. Type {9:2}{4:dd} to delete two lines. | + {0: } | + {7:✓ }{3:1) Roses are red, }| + {11:✗ }{3:2) Mud is fun, }| + {7:✓ }{3:3) Violets are blue, }| + {11:✗ }{3:4) I have a car, }| + {11:✗ }{3:5) Clocks tell time, }| + {7:✓ }{3:6) Sugar is sweet }| + {7:✓ }{3:7) And so are you. }| + {0: } | + {0: }{3:#}{5: Lesson 2.7: THE UNDO COMMAND} | + {0: } | + {0: }{1: Press }{4:u}{1: to undo the last commands, }{4:U}{1: to fix a whole line. } | + {0: } | + {0: } 1. Move the cursor to the line below marked {10:✗} and place it on the first error.| + {0: } | + {0: } 2. Type {4:x} to delete the first unwanted character. | +]]) + + feed('310dd3112dd') + screen:expect([[ + {0: }{3:#}{5: Lesson 2.6: OPERATING ON LINES} | + {0: } | + {0: }{1: Type }{4:dd}{1: to delete a whole line. } | + {0: } | + {0: }Due to the frequency of whole line deletion, the designers of Vi decided | + {0: }it would be easier to simply type two d's to delete a line. | + {0: } | + {0: } 1. Move the cursor to the second line in the phrase below. | + {0: } | + {0: } 2. Type {2:dd} to delete the line. | + {0: } | + {0: } 3. Now move to the fourth line. | + {0: } | + {0: } 4. Type {9:2}{4:dd} to delete two lines. | + {0: } | + {7:✓ }{3:1) Roses are red, }| + {7:✓ }{3:3) Violets are blue, }| + {7:✓ }{3:^6) Sugar is sweet }| + {7:✓ }{3:7) And so are you. }| + {0: } | + {0: }{3:#}{5: Lesson 2.7: THE UNDO COMMAND} | + {0: } | + {0: }{1: Press }{4:u}{1: to undo the last commands, }{4:U}{1: to fix a whole line. } | + {0: } | + {0: } 1. Move the cursor to the line below marked {10:✗} and place it on the first error.| + {0: } | + {0: } 2. Type {4:x} to delete the first unwanted character. | + {0: } | + {0: } 3. Now type {4:u} to undo the last command executed. | + {0: } | + ]]) + end) + + it("inserting text at start of line doesn't affect highlight/sign", function() + -- Go to lesson 1.3 and make it top line in the window + feed('92zt') + screen:expect([[ + {0: }{3:^#}{5: Lesson 1.3: TEXT EDITING: DELETION} | + {0: } | + {0: }{1: Press }{4:x}{1: to delete the character under the cursor. } | + {0: } | + {0: } 1. Move the cursor to the line below marked {10:✗}. | + {0: } | + {0: } 2. To fix the errors, move the cursor until it is on top of the | + {0: } character to be deleted. | + {0: } | + {0: } 3. Press {2:the x key} to delete the unwanted character. | + {0: } | + {0: } 4. Repeat steps 2 through 4 until the sentence is correct. | + {0: } | + {11:✗ }{3:The ccow jumpedd ovverr thhe mooon. }| + {0: } | + {0: } 5. Now that the line is correct, go on to Lesson 1.4. | + {0: } | + {0: }{1:NOTE}: As you go through this tutorial, do not try to memorize everything, | + {0: } your Neovim vocabulary will expand with usage. Consider returning to | + {0: } this tutorial periodically for a refresher. | + {0: } | + {0: }{3:#}{5: Lesson 1.4: TEXT EDITING: INSERTION} | + {0: } | + {0: }{1: Press }{12:i}{1: to insert text. } | + {0: } | + {0: } 1. Move the cursor to the first line below marked {10:✗}. | + {0: } | + {0: } 2. To make the first line the same as the second, move the cursor on top | + {0: } of the first character AFTER where the text is to be inserted. | + {0: } | + ]]) + -- Go to the test line and insert text at the start of the line + feed('105iThe ') + -- Remove redundant characters + feed('fcxfdxfvxfrxfhxfox') + -- Remove the original "The " text (not the just-inserted one) + feed('^4ldw^') + screen:expect([[ + {0: }{3:#}{5: Lesson 1.3: TEXT EDITING: DELETION} | + {0: } | + {0: }{1: Press }{4:x}{1: to delete the character under the cursor. } | + {0: } | + {0: } 1. Move the cursor to the line below marked {10:✗}. | + {0: } | + {0: } 2. To fix the errors, move the cursor until it is on top of the | + {0: } character to be deleted. | + {0: } | + {0: } 3. Press {2:the x key} to delete the unwanted character. | + {0: } | + {0: } 4. Repeat steps 2 through 4 until the sentence is correct. | + {0: } | + {7:✓ }{3:^The cow jumped over the moon. }| + {0: } | + {0: } 5. Now that the line is correct, go on to Lesson 1.4. | + {0: } | + {0: }{1:NOTE}: As you go through this tutorial, do not try to memorize everything, | + {0: } your Neovim vocabulary will expand with usage. Consider returning to | + {0: } this tutorial periodically for a refresher. | + {0: } | + {0: }{3:#}{5: Lesson 1.4: TEXT EDITING: INSERTION} | + {0: } | + {0: }{1: Press }{12:i}{1: to insert text. } | + {0: } | + {0: } 1. Move the cursor to the first line below marked {10:✗}. | + {0: } | + {0: } 2. To make the first line the same as the second, move the cursor on top | + {0: } of the first character AFTER where the text is to be inserted. | + {0: } | + ]]) + end) end) describe(':Tutor tutor', function()