Files
neovim/runtime/autoload/tutor.vim
Matthew Chen 410ba8374d fix(tutor): open a temporary copy instead of original file #38522
Problem:
`:Tutor` should open a copy of the tutor file instead of the original. This is
because edits modify the original file buffer, and crashes or other misuse could
potentially corrupt the original file even if it's WO.

Solution:
Copy the tutor file to a temp path before opening. Store the original path in
`b:tutor_file` so metadata json loading still works.

- `tutor#TutorCmd` will now copy the tutor file to a temp path via `tempname()`
  before opening with `drop`. Store the original path in `b:tutor_file` only
  after the buffer is created.
- `tutor#LoadMetadata` now uses `b:tutor_file` to resolve the JSON path instead
  of `expand('%')`, which now points to the temp copy buffer
- `ftplugin/tutor.vim` does not make the `tutor#LoadMetadata` call anymore. It
  was guarded by `filereadable(expand('%').'.json')` which fails for the new
  temp copy path logic . Instead, `tutor#LoadMetadata` is already called
  directly inside `tutor#TutorCmd` since we are already assumed to enable
  interactive.

Co-authored-by: Phạm Bình An <111893501+brianhuster@users.noreply.github.com>
2026-03-31 15:27:06 -04:00

235 lines
7.1 KiB
VimL

" vim: fdm=marker et ts=4 sw=4
" Setup: {{{1
function! tutor#SetupVim()
if !exists('g:did_load_ftplugin') || g:did_load_ftplugin != 1
filetype plugin on
endif
if has('syntax')
if !exists('g:syntax_on') || g:syntax_on == 0
syntax on
endif
endif
endfunction
" Loads metadata file, if available
function! tutor#LoadMetadata()
let l:metadata_file = exists('b:tutor_file') ? b:tutor_file : expand('%')
let b:tutor_metadata = json_decode(readfile(l:metadata_file .. '.json'))
endfunction
" Mappings: {{{1
function! tutor#SetNormalMappings()
nnoremap <silent> <buffer> <CR> :call tutor#FollowLink(0)<cr>
nnoremap <silent> <buffer> <2-LeftMouse> :call tutor#MouseDoubleClick()<cr>
nnoremap <buffer> >> :call tutor#InjectCommand()<cr>
endfunction
function! tutor#MouseDoubleClick()
if foldclosed(line('.')) > -1
normal! zo
else
if match(getline('.'), '^#\{1,} ') > -1 && foldlevel(line('.')) > 0
silent normal! zc
else
call tutor#FollowLink(0)
endif
endif
endfunction
function! tutor#InjectCommand()
let l:cmd = substitute(getline('.'), '^\s*', '', '')
exe l:cmd
redraw | echohl WarningMsg | echon "tutor: ran" | echohl None | echon " " | echohl Statement | echon l:cmd
endfunction
function! tutor#FollowLink(force)
let l:stack_s = join(map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")'), '')
if l:stack_s =~# 'tutorLink'
let l:link_start = searchpairpos('\[', '', ')', 'nbcW')
let l:link_end = searchpairpos('\[', '', ')', 'ncW')
if l:link_start[0] == l:link_end[0]
let l:linkData = getline(l:link_start[0])[l:link_start[1]-1:l:link_end[1]-1]
else
return
endif
let l:target = matchstr(l:linkData, '(\@<=.*)\@=')
if a:force != 1 && match(l:target, '\*.\+\*') > -1
call cursor(l:link_start[0], l:link_end[1])
call search(l:target, '')
normal! ^
elseif a:force != 1 && match(l:target, '^@tutor:') > -1
let l:tutor = matchstr(l:target, '@tutor:\zs.*')
exe "Tutor ".l:tutor
else
exe "help ".l:target
endif
endif
endfunction
" Folding And Info: {{{1
function! tutor#TutorFolds()
if getline(v:lnum) =~# '^#\{1,6}'
return ">". len(matchstr(getline(v:lnum), '^#\{1,6}'))
else
return "="
endif
endfunction
" Tutor Cmd: {{{1
function! s:Locale()
" Make sure l:lang exists before returning.
let l:lang = 'en_US'
if exists('v:lang') && v:lang =~ '\a\a'
let l:lang = v:lang
elseif $LC_ALL =~ '\a\a'
let l:lang = $LC_ALL
elseif $LC_MESSAGES =~ '\a\a' || $LC_MESSAGES ==# "C"
" LC_MESSAGES=C can be used to explicitly ask for English messages while
" keeping LANG non-English; don't set l:lang then.
if $LC_MESSAGES =~ '\a\a'
let l:lang = $LC_MESSAGES
endif
elseif $LANG =~ '\a\a'
let l:lang = $LANG
endif
return split(l:lang, '_')
endfunction
function! s:GlobPath(lp, pat)
if version >= 704 && has('patch279')
return globpath(a:lp, a:pat, 1, 1)
else
return split(globpath(a:lp, a:pat, 1), '\n')
endif
endfunction
function! s:Sort(a, b)
let mod_a = fnamemodify(a:a, ':t')
let mod_b = fnamemodify(a:b, ':t')
if mod_a == mod_b
let retval = 0
elseif mod_a > mod_b
if match(mod_a, '^vim-') > -1 && match(mod_b, '^vim-') == -1
let retval = -1
else
let retval = 1
endif
else
if match(mod_b, '^vim-') > -1 && match(mod_a, '^vim-') == -1
let retval = 1
else
let retval = -1
endif
endif
return retval
endfunction
" returns a list of all tutor files matching the given name
function! tutor#GlobTutorials(name, locale)
let locale = a:locale
" pack/*/start/* are not reported in &rtp
let rtp = nvim_list_runtime_paths()
\ ->map({_, v -> escape(v:lua.vim.fs.normalize(v), ',')})
\ ->join(',')
" search for tutorials:
" 1. non-localized
let l:tutors = s:GlobPath(rtp, 'tutor/'.a:name.'.tutor')
" 2. localized for current locale
let l:locale_tutors = s:GlobPath(rtp, 'tutor/'.locale.'/'.a:name.'.tutor')
" 3. fallback to 'en'
if len(l:locale_tutors) == 0
let l:locale_tutors = s:GlobPath(rtp, 'tutor/en/'.a:name.'.tutor')
endif
call extend(l:tutors, l:locale_tutors)
return uniq(sort(l:tutors, 's:Sort'), 's:Sort')
endfunction
function! tutor#TutorCmd(tutor_name)
if match(a:tutor_name, '[[:space:]]') > 0
echom "Only one argument accepted (check spaces)"
return
endif
if a:tutor_name == ''
let l:tutor_name = 'vim-01-beginner.tutor'
else
let l:tutor_name = a:tutor_name
endif
if match(l:tutor_name, '\.tutor$') > 0
let l:tutor_name = fnamemodify(l:tutor_name, ':r')
endif
let l:tutors = tutor#GlobTutorials(l:tutor_name, s:Locale()[0])
if len(l:tutors) == 0
echom "No tutorial with that name found"
return
endif
if len(l:tutors) == 1
let l:to_open = l:tutors[0]
else
let l:idx = 0
let l:candidates = ['Several tutorials with that name found. Select one:']
for candidate in map(copy(l:tutors),
\'fnamemodify(v:val, ":h:h:t")."/".s:Locale()[0]."/".fnamemodify(v:val, ":t")')
let l:idx += 1
call add(l:candidates, l:idx.'. '.candidate)
endfor
let l:tutor_to_open = inputlist(l:candidates)
let l:to_open = l:tutors[l:tutor_to_open-1]
endif
let l:tutor_file_og = l:to_open
let l:tutor_file_tmp = tempname() .. '.' .. fnamemodify(l:tutor_file_og, ':t')
call filecopy(l:tutor_file_og, l:tutor_file_tmp)
call tutor#SetupVim()
exe "drop" fnameescape(l:tutor_file_tmp)
let b:tutor_file = l:tutor_file_og
call tutor#LoadMetadata()
call tutor#EnableInteractive(v:true)
call tutor#ApplyTransform()
endfunction
function! tutor#TutorCmdComplete(lead,line,pos)
let l:tutors = tutor#GlobTutorials('*', s:Locale()[0])
let l:names = uniq(sort(map(l:tutors, 'fnamemodify(v:val, ":t:r")'), 's:Sort'))
return join(l:names, "\n")
endfunction
" Enables/disables interactive mode.
function! tutor#EnableInteractive(enable)
let enable = a:enable
if enable
setlocal buftype=nowrite
setlocal concealcursor+=inv
setlocal conceallevel=2
lua require('nvim.tutor').apply_marks()
augroup tutor_interactive
autocmd! TextChanged,TextChangedI <buffer> lua require('nvim.tutor').apply_marks_on_changed()
augroup END
else
setlocal buftype<
setlocal concealcursor<
setlocal conceallevel<
if exists('#tutor_interactive')
autocmd! tutor_interactive * <buffer>
endif
endif
endfunction
function! tutor#ApplyTransform()
if has('win32')
sil! %s/{unix:(\(.\{-}\)),win:(\(.\{-}\))}/\2/g
else
sil! %s/{unix:(\(.\{-}\)),win:(\(.\{-}\))}/\1/g
endif
normal! gg0
endfunction