-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Description
From :help undo_ftplugin:
b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
This gives an error:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
END
var ft = 'vim'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ftE94: No matching buffer for :undo_ftplugin =
That's because :let is necessary in legacy, and here it's missing:
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 71a4850ed..b8841b153 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -2502,7 +2502,7 @@ When the user does ":setfiletype xyz" the effect of the previous filetype
should be undone. Set the b:undo_ftplugin variable to the commands that will
undo the settings in your filetype plugin. Example: >
- b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
+ let b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
Using ":setlocal" with "<" after the option name resets the option to itsSecond, it does not work in a filetype plugin located in an after/ directory:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
setlocal shiftwidth=4
echomsg 'before sourcing after/ ftplugin: ' .. b:undo_ftplugin
let b:undo_ftplugin = 'setlocal shiftwidth<'
echomsg 'after sourcing after/ ftplugin: ' .. b:undo_ftplugin
END
var ft = 'vim'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ftbefore sourcing after/ ftplugin: call VimFtpluginUndo()
after sourcing after/ ftplugin: setlocal shiftwidth<
Notice that the call to VimFtpluginUndo() has been lost. That's because we used an assignment which completely resets the value, and prevents options set in other filetype plugins to be properly undone.
We need to append the string, not overwrite it:
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 71a4850ed..3731476f9 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -2502,7 +2502,7 @@ When the user does ":setfiletype xyz" the effect of the previous filetype
should be undone. Set the b:undo_ftplugin variable to the commands that will
undo the settings in your filetype plugin. Example: >
- b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
+ let b:undo_ftplugin ..= "| setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
Using ":setlocal" with "<" after the option name resets the option to itsBut it's still not correct. What if b:undo_ftplugin does not exist?
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
let b:undo_ftplugin ..= "| setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
END
var ft = 'test'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ftE121: Undefined variable: b:undo_ftplugin
An error is given. We need get() to suppress it:
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 71a4850ed..361692b76 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -2502,7 +2502,8 @@ When the user does ":setfiletype xyz" the effect of the previous filetype
should be undone. Set the b:undo_ftplugin variable to the commands that will
undo the settings in your filetype plugin. Example: >
- b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
+ let b:undo_ftplugin = get(b:, 'undo_ftplugin', '')
+ \ .. "| setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
Using ":setlocal" with "<" after the option name resets the option to itsIt's still not correct:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
let b:undo_ftplugin = get(b:, 'undo_ftplugin', '')
\ .. "| setlocal fo< com< tw< commentstring<"
END
var ft = 'test'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ft
&filetype = ''E749: empty buffer
This error is given because b:undo_ftplugin starts with a bar. And in legacy, at the start of a line, a bar prints the current line. This is an undesirable effect, which can give the previous unexpected error when the buffer is empty. When we call get() and pass it a default value, we can't just write an empty string. We need something which has no side-effect when executed by execute. The simplest no-op I can think of is execute itself:
:execute 'execute'
So instead of this:
let b:undo_ftplugin = get(b:, 'undo_ftplugin', '')
\ .. "| setlocal fo< com< tw< commentstring<"
We could write this:
let b:undo_ftplugin = get(b:, 'undo_ftplugin', 'execute')
\ .. "| setlocal fo< com< tw< commentstring<"
Test:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
let b:undo_ftplugin = get(b:, 'undo_ftplugin', 'execute')
\ .. "| setlocal fo< com< tw< commentstring<"
END
var ft = 'test'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ft
&filetype = ''no error
It's still not correct, because it doesn't handle the case where b:undo_ftplugin does exist, but is empty:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
let b:undo_ftplugin = ''
END
var ft = 'test_aaa'
lines->writefile(printf('%s/%s.vim', dir, ft))
lines =<< trim END
let b:undo_ftplugin = get(b:, 'undo_ftplugin', 'execute')
\ .. "| setlocal fo< com< tw< commentstring<"
END
ft = 'test_bbb'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ft->substitute('_.*', '', '')
&filetype = ''E749: empty buffer
One solution is to use the null coalescing operator to assert that b:undo_ftplugin should not be empty, and fall back on the string 'execute' otherwise:
let b:undo_ftplugin = (get(b:, 'undo_ftplugin') ?? 'execute')
\ .. ' | setlocal fo< com< tw< commentstring<'
Test:
vim9script
var dir = '/tmp/.vim'
dir->delete('rf')
dir ..= '/after'
&runtimepath ..= ',' .. dir
dir ..= '/ftplugin'
dir->mkdir('p')
var lines =<< trim END
let b:undo_ftplugin = ''
END
var ft = 'test_aaa'
lines->writefile(printf('%s/%s.vim', dir, ft))
lines =<< trim END
let b:undo_ftplugin = (get(b:, 'undo_ftplugin') ?? 'execute')
\ .. ' | setlocal fo< com< tw< commentstring<'
END
ft = 'test_bbb'
lines->writefile(printf('%s/%s.vim', dir, ft))
filetype plugin on
&filetype = ft->substitute('_.*', '', '')
&filetype = ''As a suggestion, here is the final patch which – hopefully – handles all corner cases:
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 71a4850ed..cff84c682 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -2502,7 +2502,8 @@ When the user does ":setfiletype xyz" the effect of the previous filetype
should be undone. Set the b:undo_ftplugin variable to the commands that will
undo the settings in your filetype plugin. Example: >
- b:undo_ftplugin = "setlocal fo< com< tw< commentstring<"
+ let b:undo_ftplugin = (get(b:, 'undo_ftplugin') ?? 'execute')
+ \ .. "| setlocal fo< com< tw< commentstring<"
\ .. "| unlet b:match_ignorecase b:match_words b:match_skip"
Using ":setlocal" with "<" after the option name resets the option to its