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
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 input section invokes the |prompt_setcallback()| callback, which is
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,
FileAppendPost or VimLeave autocommand event. See |gzip-example| for
an explanation.
When 'buftype' is "nowrite" or "nofile" this option may be set, but
will be ignored.
When 'buftype' is "nowrite" or "nofile", this option may be set, but
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
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,
--- FileAppendPost or VimLeave autocommand event. See `gzip-example` for
--- an explanation.
--- When 'buftype' is "nowrite" or "nofile" this option may be set, but
--- will be ignored.
--- When 'buftype' is "nowrite" or "nofile", this option may be set, but
--- 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
--- when using "rA" on an "A".
---

View File

@@ -5935,8 +5935,11 @@ local options = {
result of a BufNewFile, BufRead/BufReadPost, BufWritePost,
FileAppendPost or VimLeave autocommand event. See |gzip-example| for
an explanation.
When 'buftype' is "nowrite" or "nofile" this option may be set, but
will be ignored.
When 'buftype' is "nowrite" or "nofile", this option may be set, but
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
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 the first character, because it can only be "dos", "unix" or "mac").
/// "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
///
@@ -3097,10 +3099,10 @@ static char *u_save_line_buf(buf_T *buf, linenr_T lnum)
bool bufIsChanged(buf_T *buf)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
// In a "prompt" buffer we do respect 'modified', so that we can control
// closing the window by setting or resetting that option.
return (!bt_dontwrite(buf) || bt_prompt(buf))
&& (buf->b_changed || file_ff_differs(buf, true));
// In a "prompt" buffer we respect 'modified' if the user or a plugin explicitly set it.
return bt_prompt(buf)
? buf->b_modified_was_set
: (!bt_dontwrite(buf) && (buf->b_changed || file_ff_differs(buf, true)));
}
// Return true if any buffer has changes. Also buffers that are not written.

View File

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