Skip to content

wrong example at :help undo_ftplugin #9645

@lacygoill

Description

@lacygoill

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 = ft
E94: 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 its

Second, 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 = ft
before 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 its

But 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 = ft
E121: 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 its

It'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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions