fix(prompt): don't implicitly set 'modified' #38118

Problem:
In aec3d7915c Vim changed prompt-buffers
to respect 'modified' so the termdebug plugin can "control closing the
window". But for most use-cases  (REPL, shell, AI "chat", …),
prompt-buffers are in practice always "modified", and no way to "save"
them, so *implicitly* setting 'modified' is noisy and annoying.

Solution:
Don't implicitly set 'modified' when a prompt-buffer is updated.
Plugins/users can still explicitly set 'modified', which will then
trigger the "E37: No write since last change" warning.
This commit is contained in:
Willaaaaaaa
2026-03-12 02:16:35 +08:00
committed by GitHub
parent f168d215cf
commit 689a149b08
6 changed files with 85 additions and 46 deletions

View File

@@ -186,6 +186,11 @@ normally only do that in a newly created buffer: >vim
:set buftype=prompt :set buftype=prompt
Prompt buffers ignore modified checks for changes made to the buffer, so
interactive input does not make them hard to close. But if a plugin or user
explicitly sets 'modified', a modified prompt buffer can't be closed without
saving.
The user can edit and input text at the end of the buffer. Pressing Enter in The user can edit and input text at the end of the buffer. Pressing Enter in
the input section invokes the |prompt_setcallback()| callback, which is the input section invokes the |prompt_setcallback()| callback, which is
typically expected to process the prompt and show results by appending to the typically expected to process the prompt and show results by appending to the

View File

@@ -4468,8 +4468,11 @@ A jump table for the options with a short description can be found at |Q_op|.
result of a BufNewFile, BufRead/BufReadPost, BufWritePost, result of a BufNewFile, BufRead/BufReadPost, BufWritePost,
FileAppendPost or VimLeave autocommand event. See |gzip-example| for FileAppendPost or VimLeave autocommand event. See |gzip-example| for
an explanation. an explanation.
When 'buftype' is "nowrite" or "nofile" this option may be set, but When 'buftype' is "nowrite" or "nofile", this option may be set, but
will be ignored. it is ignored and will not block closing the window. For "prompt"
buffers, changes made to the buffer do not make it count as modified,
but an explicit ":set modified" is respected and will block closing the
window.
Note that the text may actually be the same, e.g. 'modified' is set Note that the text may actually be the same, e.g. 'modified' is set
when using "rA" on an "A". when using "rA" on an "A".

View File

@@ -4565,8 +4565,11 @@ vim.bo.ma = vim.bo.modifiable
--- result of a BufNewFile, BufRead/BufReadPost, BufWritePost, --- result of a BufNewFile, BufRead/BufReadPost, BufWritePost,
--- FileAppendPost or VimLeave autocommand event. See `gzip-example` for --- FileAppendPost or VimLeave autocommand event. See `gzip-example` for
--- an explanation. --- an explanation.
--- When 'buftype' is "nowrite" or "nofile" this option may be set, but --- When 'buftype' is "nowrite" or "nofile", this option may be set, but
--- will be ignored. --- it is ignored and will not block closing the window. For "prompt"
--- buffers, changes made to the buffer do not make it count as modified,
--- but an explicit ":set modified" is respected and will block closing the
--- window.
--- Note that the text may actually be the same, e.g. 'modified' is set --- Note that the text may actually be the same, e.g. 'modified' is set
--- when using "rA" on an "A". --- when using "rA" on an "A".
--- ---

View File

@@ -5935,8 +5935,11 @@ local options = {
result of a BufNewFile, BufRead/BufReadPost, BufWritePost, result of a BufNewFile, BufRead/BufReadPost, BufWritePost,
FileAppendPost or VimLeave autocommand event. See |gzip-example| for FileAppendPost or VimLeave autocommand event. See |gzip-example| for
an explanation. an explanation.
When 'buftype' is "nowrite" or "nofile" this option may be set, but When 'buftype' is "nowrite" or "nofile", this option may be set, but
will be ignored. it is ignored and will not block closing the window. For "prompt"
buffers, changes made to the buffer do not make it count as modified,
but an explicit ":set modified" is respected and will block closing the
window.
Note that the text may actually be the same, e.g. 'modified' is set Note that the text may actually be the same, e.g. 'modified' is set
when using "rA" on an "A". when using "rA" on an "A".
]=], ]=],

View File

@@ -3090,6 +3090,8 @@ static char *u_save_line_buf(buf_T *buf, linenr_T lnum)
/// Check if the 'modified' flag is set, or 'ff' has changed (only need to /// Check if the 'modified' flag is set, or 'ff' has changed (only need to
/// check the first character, because it can only be "dos", "unix" or "mac"). /// check the first character, because it can only be "dos", "unix" or "mac").
/// "nofile" and "scratch" type buffers are considered to always be unchanged. /// "nofile" and "scratch" type buffers are considered to always be unchanged.
/// Prompt buffers ignore implicit modifications by default, but an explicit
/// ":set modified" still makes them count as changed.
/// ///
/// @param buf The buffer to check /// @param buf The buffer to check
/// ///
@@ -3097,10 +3099,10 @@ static char *u_save_line_buf(buf_T *buf, linenr_T lnum)
bool bufIsChanged(buf_T *buf) bool bufIsChanged(buf_T *buf)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{ {
// In a "prompt" buffer we do respect 'modified', so that we can control // In a "prompt" buffer we respect 'modified' if the user or a plugin explicitly set it.
// closing the window by setting or resetting that option. return bt_prompt(buf)
return (!bt_dontwrite(buf) || bt_prompt(buf)) ? buf->b_modified_was_set
&& (buf->b_changed || file_ff_differs(buf, true)); : (!bt_dontwrite(buf) && (buf->b_changed || file_ff_differs(buf, true)));
} }
// Return true if any buffer has changes. Also buffers that are not written. // Return true if any buffer has changes. Also buffers that are not written.

View File

@@ -27,15 +27,11 @@ describe('prompt buffer', function()
source([[ source([[
func TextEntered(text) func TextEntered(text)
if a:text == "exit" if a:text == "exit"
" Reset &modified to allow the buffer to be closed.
set nomodified
stopinsert stopinsert
close close
else else
" Add the output above the current prompt. " Add the output above the current prompt.
call append(line("$") - 1, split('Command: "' . a:text . '"', '\n')) call append(line("$") - 1, split('Command: "' . a:text . '"', '\n'))
" Reset &modified to allow the buffer to be closed.
set nomodified
call timer_start(20, {id -> TimerFunc(a:text)}) call timer_start(20, {id -> TimerFunc(a:text)})
endif endif
endfunc endfunc
@@ -43,8 +39,6 @@ describe('prompt buffer', function()
func TimerFunc(text) func TimerFunc(text)
" Add the output above the current prompt. " Add the output above the current prompt.
call append(line("$") - 1, split('Result: "' . a:text .'"', '\n')) call append(line("$") - 1, split('Result: "' . a:text .'"', '\n'))
" Reset &modified to allow the buffer to be closed.
set nomodified
endfunc endfunc
func SwitchWindows() func SwitchWindows()
@@ -62,7 +56,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: ^ | cmd: ^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -89,6 +83,21 @@ describe('prompt buffer', function()
{1:~ }|*8 {1:~ }|*8
| |
]]) ]])
command('new')
command('set buftype=prompt')
feed('iabc<BS><BS>')
eq('a', fn('prompt_getinput', fn('bufnr')))
command('quit')
eq(1, #api.nvim_list_wins())
command('new')
command('set buftype=prompt modified')
eq(
'Vim(quit):E37: No write since last change (add ! to override)',
t.pcall_err(command, 'quit')
)
eq(2, #api.nvim_list_wins())
end) end)
-- oldtest: Test_prompt_editing() -- oldtest: Test_prompt_editing()
@@ -98,7 +107,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: hel^ | cmd: hel^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -107,7 +116,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: -^hel | cmd: -^hel |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -116,7 +125,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: -hz^el | cmd: -hz^el |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -125,7 +134,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: -hzelx^ | cmd: -hzelx^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -149,7 +158,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: | cmd: |
{1:~ }|*3 {1:~ }|*3
{2:[Prompt] [+] }| {2:[Prompt] }|
^other buffer | ^other buffer |
{1:~ }|*3 {1:~ }|*3
| |
@@ -158,7 +167,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: ^ | cmd: ^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -167,7 +176,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd:^ | cmd:^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
| |
@@ -281,7 +290,7 @@ describe('prompt buffer', function()
line 2 | line 2 |
line 3^ | line 3^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -403,7 +412,7 @@ describe('prompt buffer', function()
line 2 | line 2 |
line 3 | line 3 |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
| |
@@ -433,7 +442,7 @@ describe('prompt buffer', function()
line 2 | line 2 |
line 3^ | line 3^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -450,7 +459,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: tests-middle^-initial| cmd: tests-middle^-initial|
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
| |
@@ -460,7 +469,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: tests-mid^le-initial | cmd: tests-mid^le-initial |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
| |
@@ -471,7 +480,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: tests-mid^dle-initial| cmd: tests-mid^dle-initial|
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
1 change; {MATCH:.*} | 1 change; {MATCH:.*} |
@@ -484,7 +493,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: tests-^initial | cmd: tests-^initial |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
1 change; {MATCH:.*} | 1 change; {MATCH:.*} |
@@ -509,7 +518,7 @@ describe('prompt buffer', function()
Command: "tests-initial" | Command: "tests-initial" |
cmd:^ | cmd:^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
1 line {MATCH:.*} | 1 line {MATCH:.*} |
@@ -522,7 +531,7 @@ describe('prompt buffer', function()
Command: "tests-initial" | Command: "tests-initial" |
cmd: ^ | cmd: ^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -533,7 +542,7 @@ describe('prompt buffer', function()
Command: "tests-initial" | Command: "tests-initial" |
^cmd: hello | ^cmd: hello |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
1 change; {MATCH:.*} | 1 change; {MATCH:.*} |
@@ -548,7 +557,7 @@ describe('prompt buffer', function()
Command: "tests-initial" | Command: "tests-initial" |
c^md > hello | c^md > hello |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
Already at oldest change | Already at oldest change |
@@ -563,7 +572,7 @@ describe('prompt buffer', function()
Command: "tests-initial" | Command: "tests-initial" |
cmd > hello there | cmd > hello there |
cmd >^ | cmd >^ |
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
Already at oldest change | Already at oldest change |
@@ -580,7 +589,7 @@ describe('prompt buffer', function()
line 2 | line 2 |
line 3^ | line 3^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -592,7 +601,7 @@ describe('prompt buffer', function()
line 2 | line 2 |
after^ | after^ |
line 3 | line 3 |
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -608,7 +617,7 @@ describe('prompt buffer', function()
before^ | before^ |
line 2 | line 2 |
after | after |
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -618,6 +627,8 @@ describe('prompt buffer', function()
eq('line 1\nbefore\nline 2\nafter\nline 3', fn('prompt_getinput', buf)) eq('line 1\nbefore\nline 2\nafter\nline 3', fn('prompt_getinput', buf))
feed('<cr>') feed('<cr>')
vim.uv.sleep(20)
eq('', fn('prompt_getinput', buf))
screen:expect([[ screen:expect([[
line 2 | line 2 |
after | after |
@@ -630,6 +641,16 @@ describe('prompt buffer', function()
]]) ]])
feed('line 4<s-cr>line 5') feed('line 4<s-cr>line 5')
screen:expect([[
after |
line 3" |
cmd: line 4 |
line 5^ |
{3:[Prompt] }|
other buffer |
{1:~ }|*3
{5:-- INSERT --} |
]])
feed('<esc>k0oafter prompt') feed('<esc>k0oafter prompt')
screen:expect([[ screen:expect([[
@@ -637,7 +658,7 @@ describe('prompt buffer', function()
line 3" | line 3" |
cmd: line 4 | cmd: line 4 |
after prompt^ | after prompt^ |
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -649,13 +670,15 @@ describe('prompt buffer', function()
line 3" | line 3" |
cmd: at prompt^ | cmd: at prompt^ |
line 4 | line 4 |
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
]]) ]])
feed('<cr>') feed('<cr>')
vim.uv.sleep(20)
eq('', fn('prompt_getinput', buf))
screen:expect([[ screen:expect([[
line 4 | line 4 |
after prompt | after prompt |
@@ -674,7 +697,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: asdf^ | cmd: asdf^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -684,7 +707,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: ^ | cmd: ^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -694,7 +717,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: asdf^ | cmd: asdf^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -704,7 +727,7 @@ describe('prompt buffer', function()
screen:expect([[ screen:expect([[
cmd: ^ | cmd: ^ |
{1:~ }|*3 {1:~ }|*3
{3:[Prompt] [+] }| {3:[Prompt] }|
other buffer | other buffer |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |
@@ -1021,7 +1044,7 @@ describe('prompt buffer', function()
ooooooooooooooooooooong >| ooooooooooooooooooooong >|
^ | ^ |
{1:~ }| {1:~ }|
{3:[Prompt] [+] }| {3:[Prompt] }|
foo > hello | foo > hello |
{1:~ }|*3 {1:~ }|*3
{5:-- INSERT --} | {5:-- INSERT --} |