mirror of
https://github.com/neovim/neovim.git
synced 2025-09-05 19:08:15 +00:00
Merge #34921 tutor: reimplement interactive marks as extmarks
This commit is contained in:
@@ -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 <buffer> call tutor#ApplyMarksOnChanged()
|
||||
autocmd! TextChanged,TextChangedI <buffer> lua require('nvim.tutor').apply_marks_on_changed()
|
||||
augroup END
|
||||
else
|
||||
setlocal buftype<
|
||||
|
80
runtime/lua/nvim/tutor.lua
Normal file
80
runtime/lua/nvim/tutor.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
---@class nvim.TutorMetadata
|
||||
---@field expect table<string, string|-1>
|
||||
|
||||
---@alias nvim.TutorExtmarks table<string, string>
|
||||
|
||||
---@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
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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.",
|
||||
|
@@ -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,62 +139,150 @@ describe(':Tutor', function()
|
||||
feed(':983<CR>zt')
|
||||
screen:expect(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe(':Tutor tutor', function()
|
||||
local screen --- @type test.functional.ui.screen
|
||||
|
||||
before_each(function()
|
||||
clear({ args = { '--clean' } })
|
||||
command('set cmdheight=0')
|
||||
command('Tutor tutor')
|
||||
screen = Screen.new(81, 30)
|
||||
screen:set_default_attr_ids({
|
||||
[0] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.Gray },
|
||||
[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.Blue1 },
|
||||
})
|
||||
end)
|
||||
|
||||
it('applies interactive marks', function()
|
||||
feed(':216<CR>zt')
|
||||
it("removing a line doesn't affect highlight/mark of other lines", function()
|
||||
-- Do lesson 2.6
|
||||
feed(':294<CR>zt')
|
||||
screen:expect([[
|
||||
{0: }{3:^###}{5: expect } |
|
||||
{0: } |
|
||||
{0: }"expect" lines check that the contents of the line are identical to some preset|
|
||||
{0: } text |
|
||||
{0: }(like in the exercises above). |
|
||||
{0: } |
|
||||
{0: }These elements are specified in separate JSON files like this |
|
||||
{0: } |
|
||||
{0: }{3:~~~ json} |
|
||||
{0: }{ |
|
||||
{0: } "expect": { |
|
||||
{0: } "1": "This is how this line should look.", |
|
||||
{0: } "2": "This is how this line should look.", |
|
||||
{0: } "3": -1 |
|
||||
{0: } } |
|
||||
{0: }} |
|
||||
{0: }{3:~~~} |
|
||||
{0: } |
|
||||
{0: }These files contain an "expect" dictionary, for which the keys are line numbers|
|
||||
{0: } and |
|
||||
{0: }the values are the expected text. A value of -1 means that the condition for th|
|
||||
{0: }e line |
|
||||
{0: }will always be satisfied, no matter what (this is useful for letting the user p|
|
||||
{0: }lay a bit). |
|
||||
{0: } |
|
||||
{7:✓ }{3:This is an "expect" line that is always satisfied. Try changing it.} |
|
||||
{0: } |
|
||||
{0: }These files conventionally have the same name as the tutorial document with the|
|
||||
{0: } .json |
|
||||
{0: }extension appended (for a full example, see the file that corresponds to thi{8:@@@}|
|
||||
{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('<Cmd>310<CR>dd<Cmd>311<CR>2dd')
|
||||
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('<Cmd>92<CR>zt')
|
||||
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('<Cmd>105<CR>iThe <Esc>')
|
||||
-- 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)
|
||||
|
Reference in New Issue
Block a user