diff options
-rw-r--r-- | runtime/doc/eval.txt | 58 | ||||
-rw-r--r-- | runtime/doc/nvim_terminal_emulator.txt | 17 | ||||
-rw-r--r-- | runtime/doc/options.txt | 2 | ||||
-rw-r--r-- | runtime/pack/dist/opt/termdebug/plugin/termdebug.vim | 241 | ||||
-rw-r--r-- | src/nvim/buffer.c | 24 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 6 | ||||
-rw-r--r-- | src/nvim/diff.c | 4 | ||||
-rw-r--r-- | src/nvim/edit.c | 79 | ||||
-rw-r--r-- | src/nvim/eval.c | 56 | ||||
-rw-r--r-- | src/nvim/eval.lua | 4 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 98 | ||||
-rw-r--r-- | src/nvim/normal.c | 59 | ||||
-rw-r--r-- | src/nvim/ops.c | 72 | ||||
-rw-r--r-- | src/nvim/option.c | 12 | ||||
-rw-r--r-- | src/nvim/undo.c | 5 | ||||
-rw-r--r-- | test/functional/legacy/prompt_buffer_spec.lua | 153 |
16 files changed, 816 insertions, 74 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index e0ce83f8d2..b8739ff9b3 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2084,6 +2084,7 @@ ctxsize() Number return |context-stack| size cursor({lnum}, {col} [, {off}]) Number move cursor to {lnum}, {col}, {off} cursor({list}) Number move cursor to position in {list} +debugbreak({pid}) Number interrupt process being debugged deepcopy({expr} [, {noref}]) any make a full copy of {expr} delete({fname} [, {flags}]) Number delete the file or directory {fname} deletebufline({expr}, {first}[, {last}]) @@ -2280,6 +2281,10 @@ pathshorten({expr}) String shorten directory names in a path pow({x}, {y}) Float {x} to the power of {y} prevnonblank({lnum}) Number line nr of non-blank line <= {lnum} printf({fmt}, {expr1}...) String format text +prompt_addtext({buf}, {expr}) none add text to a prompt buffer +prompt_setcallback({buf}, {expr}) none set prompt callback function +prompt_setinterrupt({buf}, {text}) none set prompt interrupt function +prompt_setprompt({buf}, {text}) none set prompt text pum_getpos() Dict position and size of pum if visible pumvisible() Number whether popup menu is visible pyeval({expr}) any evaluate |Python| expression @@ -2290,7 +2295,7 @@ range({expr} [, {max} [, {stride}]]) readdir({dir} [, {expr}]) List file names in {dir} selected by {expr} readfile({fname} [, {binary} [, {max}]]) List get list of lines from file {fname} -reg_executing() Number get the executing register name +reg_executing() String get the executing register name reg_recording() String get the recording register name reltime([{start} [, {end}]]) List get time value reltimefloat({time}) Float turn the time value into a Float @@ -3637,6 +3642,11 @@ exp({expr}) *exp()* :echo exp(-1) < 0.367879 +debugbreak({pid}) *debugbreak()* + Specifically used to interrupt a program being debugged. It + will cause process {pid} to get a SIGTRAP. Behavior for other + processes is undefined. See |terminal-debugger|. + {Sends a SIGINT to a process {pid} other than MS-Windows} expand({expr} [, {nosuf} [, {list}]]) *expand()* Expand wildcards and the following special keywords in {expr}. @@ -4541,7 +4551,7 @@ getline({lnum} [, {end}]) from the current buffer. Example: > getline(1) < When {lnum} is a String that doesn't start with a - digit, line() is called to translate the String into a Number. + digit, |line()| is called to translate the String into a Number. To get the line under the cursor: > getline(".") < When {lnum} is smaller than 1 or bigger than the number of @@ -6541,6 +6551,50 @@ printf({fmt}, {expr1} ...) *printf()* of "%" items. If there are not sufficient or too many arguments an error is given. Up to 18 arguments can be used. +prompt_setcallback({buf}, {expr}) *prompt_setcallback()* + Set prompt callback for buffer {buf} to {expr}. When {expr} + is an empty string the callback is removed. This has only + effect if {buf} has 'buftype' set to "prompt". + + The callback is invoked when pressing Enter. The current + buffer will always be the prompt buffer. A new line for a + prompt is added before invoking the callback, thus the prompt + for which the callback was invoked will be in the last but one + line. + If the callback wants to add text to the buffer, it must + insert it above the last line, since that is where the current + prompt is. This can also be done asynchronously. + The callback is invoked with one argument, which is the text + that was entered at the prompt. This can be an empty string + if the user only typed Enter. + Example: > + call prompt_setcallback(bufnr(''), function('s:TextEntered')) + func s:TextEntered(text) + if a:text == 'exit' || a:text == 'quit' + stopinsert + close + else + call append(line('$') - 1, 'Entered: "' . a:text . '"') + " Reset 'modified' to allow the buffer to be closed. + set nomodified + endif + endfunc + +prompt_setinterrupt({buf}, {expr}) *prompt_setinterrupt()* + Set a callback for buffer {buf} to {expr}. When {expr} is an + empty string the callback is removed. This has only effect if + {buf} has 'buftype' set to "prompt". + + This callback will be invoked when pressing CTRL-C in Insert + mode. Without setting a callback Vim will exit Insert mode, + as in any buffer. + +prompt_setprompt({buf}, {text}) *prompt_setprompt()* + Set prompt for buffer {buf} to {text}. You most likely want + {text} to end in a space. + The result is only visible if {buf} has 'buftype' set to + "prompt". Example: > + call prompt_setprompt(bufnr(''), 'command: ') pum_getpos() *pum_getpos()* If the popup menu (see |ins-completion-menu|) is not visible, diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt index 1a5e6421e7..55c5335a60 100644 --- a/runtime/doc/nvim_terminal_emulator.txt +++ b/runtime/doc/nvim_terminal_emulator.txt @@ -307,6 +307,23 @@ Other commands ~ isn't one +Prompt mode ~ + *termdebug-prompt* +When on MS-Windows, gdb will run in a buffer with 'buftype' set to "prompt". +This works slightly differently: +- The gdb window will be in Insert mode while typing commands. Go to Normal + mode with <Esc>, then you can move around in the buffer, copy/paste, etc. + Go back to editing the gdb command with any command that starts Insert mode, + such as `a` or `i`. +- The program being debugged will run in a separate window. On MS-Windows + this is a new console window. On Unix, if the |+terminal| feature is + available a Terminal window will be opened to run the debugged program in. + + *termdebug_use_prompt* +Prompt mode can be used even when the |+terminal| feature is present with: > + let g:termdebug_use_prompt = 1 + + Communication ~ *termdebug-communication* There is another, hidden, buffer, which is used for Vim to communicate with diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 70af23ee29..7107a0135d 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1093,6 +1093,8 @@ A jump table for the options with a short description can be found at |Q_op|. nowrite buffer will not be written quickfix list of errors |:cwindow| or locations |:lwindow| terminal |terminal-emulator| buffer + prompt buffer where only the last line can be edited, meant + to be used by a plugin, see |prompt-buffer| This option is used together with 'bufhidden' and 'swapfile' to specify special kinds of buffers. See |special-buffers|. diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim index 7a757ef7d6..aa2b69ad97 100644 --- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim +++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim @@ -37,7 +37,7 @@ " For neovim compatibility, the vim specific calls were replaced with neovim " specific calls: " term_start -> term_open -" term_sendkeys -> jobsend +" term_sendkeys -> chansend " term_getline -> getbufline " job_info && term_getjob -> using linux command ps to get the tty " balloon -> nvim floating window @@ -47,8 +47,6 @@ " https://github.com/autozimu/LanguageClient-neovim/blob/0ed9b69dca49c415390a8317b19149f97ae093fa/autoload/LanguageClient.vim#L304 " " Neovim terminal also works seamlessly on windows, which is why the ability -" to use the prompt buffer was removed. -" " Author: Bram Moolenaar " Copyright: Vim license applies, see ":help license" @@ -57,6 +55,12 @@ if exists(':Termdebug') finish endif +" The terminal feature does not work with gdb on win32. +if !has('win32') + let s:way = 'terminal' +else + let s:way = 'prompt' +endif let s:keepcpo = &cpo set cpo&vim @@ -138,7 +142,19 @@ func s:StartDebug_internal(dict) let s:vertical = 0 endif - call s:StartDebug_term(a:dict) + " Override using a terminal window by setting g:termdebug_use_prompt to 1. + let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt + if !has('win32') && !use_prompt + let s:way = 'terminal' + else + let s:way = 'prompt' + endif + + if s:way == 'prompt' + call s:StartDebug_prompt(a:dict) + else + call s:StartDebug_term(a:dict) + endif endfunc " Use when debugger didn't start or ended. @@ -214,11 +230,11 @@ func s:StartDebug_term(dict) " Set arguments to be run if len(proc_args) - call jobsend(s:gdb_job_id, 'set args ' . join(proc_args) . "\r") + call chansend(s:gdb_job_id, 'set args ' . join(proc_args) . "\r") endif " Connect gdb to the communication pty, using the GDB/MI interface - call jobsend(s:gdb_job_id, 'new-ui mi ' . commpty . "\r") + call chansend(s:gdb_job_id, 'new-ui mi ' . commpty . "\r") " Wait for the response to show up, users may not notice the error and wonder " why the debugger doesn't work. @@ -275,6 +291,100 @@ func s:StartDebug_term(dict) call s:StartDebugCommon(a:dict) endfunc +func s:StartDebug_prompt(dict) + " Open a window with a prompt buffer to run gdb in. + if s:vertical + vertical new + else + new + endif + let s:gdbwin = win_getid(winnr()) + let s:promptbuf = bufnr('') + call prompt_setprompt(s:promptbuf, 'gdb> ') + set buftype=prompt + file gdb + call prompt_setcallback(s:promptbuf, function('s:PromptCallback')) + call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt')) + + if s:vertical + " Assuming the source code window will get a signcolumn, use two more + " columns for that, thus one less for the terminal window. + exe (&columns / 2 - 1) . "wincmd |" + endif + + " Add -quiet to avoid the intro message causing a hit-enter prompt. + let gdb_args = get(a:dict, 'gdb_args', []) + let proc_args = get(a:dict, 'proc_args', []) + + let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args + "call ch_log('executing "' . join(cmd) . '"') + + let s:gdbjob = jobstart(cmd, { + \ 'on_exit': function('s:EndPromptDebug'), + \ 'on_stdout': function('s:GdbOutCallback'), + \ }) + if s:gdbjob == 0 + echoerr 'invalid argument (or job table is full) while starting gdb job' + exe 'bwipe! ' . s:ptybuf + return + elseif s:gdbjob == -1 + echoerr 'Failed to start the gdb job' + call s:CloseBuffers() + return + endif + + " Interpret commands while the target is running. This should usualy only + " be exec-interrupt, since many commands don't work properly while the + " target is running. + call s:SendCommand('-gdb-set mi-async on') + " Older gdb uses a different command. + call s:SendCommand('-gdb-set target-async on') + + let s:ptybuf = 0 + if has('win32') + " MS-Windows: run in a new console window for maximum compatibility + call s:SendCommand('set new-console on') + else + " Unix: Run the debugged program in a terminal window. Open it below the + " gdb window. + execute 'new' + wincmd x | wincmd j + belowright let s:pty_job_id = termopen('tail -f /dev/null;#gdb program') + if s:pty_job_id == 0 + echoerr 'invalid argument (or job table is full) while opening terminal window' + return + elseif s:pty_job_id == -1 + echoerr 'Failed to open the program terminal window' + return + endif + let pty_job_info = nvim_get_chan_info(s:pty_job_id) + let s:ptybuf = pty_job_info['buffer'] + let pty = pty_job_info['pty'] + let s:ptywin = win_getid(winnr()) + call s:SendCommand('tty ' . pty) + + " Since GDB runs in a prompt window, the environment has not been set to + " match a terminal window, need to do that now. + call s:SendCommand('set env TERM = xterm-color') + call s:SendCommand('set env ROWS = ' . winheight(s:ptywin)) + call s:SendCommand('set env LINES = ' . winheight(s:ptywin)) + call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin)) + call s:SendCommand('set env COLORS = ' . &t_Co) + call s:SendCommand('set env VIM_TERMINAL = ' . v:version) + endif + call s:SendCommand('set print pretty on') + call s:SendCommand('set breakpoint pending on') + " Disable pagination, it causes everything to stop at the gdb + call s:SendCommand('set pagination off') + + " Set arguments to be run + if len(proc_args) + call s:SendCommand('set args ' . join(proc_args)) + endif + + call s:StartDebugCommon(a:dict) + startinsert +endfunc func s:StartDebugCommon(dict) " Sign used to highlight the line where the program has stopped. @@ -316,23 +426,99 @@ endfunc " Send a command to gdb. "cmd" is the string without line terminator. func s:SendCommand(cmd) "call ch_log('sending to gdb: ' . a:cmd) - call jobsend(s:comm_job_id, a:cmd . "\r") + if s:way == 'prompt' + call chansend(s:gdbjob, a:cmd . "\n") + else + call chansend(s:comm_job_id, a:cmd . "\r") + endif endfunc " This is global so that a user can create their mappings with this. func TermDebugSendCommand(cmd) - let do_continue = 0 - if !s:stopped - let do_continue = 1 - call s:SendCommand('-exec-interrupt') - sleep 10m + if s:way == 'prompt' + call chansend(s:gdbjob, a:cmd . "\n") + else + let do_continue = 0 + if !s:stopped + let do_continue = 1 + if s:way == 'prompt' + " Need to send a signal to get the UI to listen. Strangely this is only + " needed once. + call jobstop(s:gdbjob) + else + call s:SendCommand('-exec-interrupt') + endif + sleep 10m + endif + call chansend(s:gdb_job_id, a:cmd . "\r") + if do_continue + Continue + endif endif - call jobsend(s:gdb_job_id, a:cmd . "\r") - if do_continue - Continue +endfunc + +" Function called when entering a line in the prompt buffer. +func s:PromptCallback(text) + call s:SendCommand(a:text) +endfunc + +" Function called when pressing CTRL-C in the prompt buffer and when placing a +" breakpoint. +func s:PromptInterrupt() + if s:pid == 0 + echoerr 'Cannot interrupt gdb, did not find a process ID' + else + "call ch_log('Interrupting gdb') + " Using job_stop(s:gdbjob, 'int') does not work. + call debugbreak(s:pid) endif endfunc +" Function called when gdb outputs text. +func s:GdbOutCallback(job_id, msgs, event) + "call ch_log('received from gdb: ' . a:text) + + " Drop the gdb prompt, we have our own. + " Drop status and echo'd commands. + call filter(a:msgs, { index, val -> + \ val !=# '(gdb)' && val !=# '^done' && val[0] !=# '&'}) + + let lines = [] + let index = 0 + + for msg in a:msgs + if msg =~ '^^error,msg=' + if exists('s:evalexpr') + \ && s:DecodeMessage(msg[11:]) + \ =~ 'A syntax error in expression, near\|No symbol .* in current context' + " Silently drop evaluation errors. + call remove(a:msgs, index) + unlet s:evalexpr + continue + endif + elseif msg[0] == '~' + call add(lines, s:DecodeMessage(msg[1:])) + call remove(a:msgs, index) + continue + endif + let index += 1 + endfor + + let curwinid = win_getid(winnr()) + call win_gotoid(s:gdbwin) + + " Add the output above the current prompt. + for line in lines + call append(line('$') - 1, line) + endfor + if !empty(lines) + set modified + endif + + call win_gotoid(curwinid) + call s:CommOutput(a:job_id, a:msgs, a:event) +endfunc + " Decode a message from gdb. quotedText starts with a ", return the text up " to the next ", unescaping characters. func s:DecodeMessage(quotedText) @@ -396,6 +582,19 @@ func s:EndDebugCommon() au! TermDebug endfunc +func s:EndPromptDebug(job_id, exit_code, event) + let curwinid = win_getid(winnr()) + call win_gotoid(s:gdbwin) + close + if curwinid != s:gdbwin + call win_gotoid(curwinid) + endif + + call s:EndDebugCommon() + unlet s:gdbwin + "call ch_log("Returning from EndPromptDebug()") +endfunc + func s:CommOutput(job_id, msgs, event) for msg in a:msgs @@ -436,7 +635,11 @@ func s:InstallCommands() command Stop call s:SendCommand('-exec-interrupt') " using -exec-continue results in CTRL-C in gdb window not working - command Continue call jobsend(s:gdb_job_id, "continue\r") + if s:way == 'prompt' + command Continue call s:SendCommand('continue') + else + command Continue call chansend(s:gdb_job_id, "continue\r") + endif command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) command Gdb call win_gotoid(s:gdbwin) @@ -494,7 +697,11 @@ func s:SetBreakpoint() let do_continue = 0 if !s:stopped let do_continue = 1 - call s:SendCommand('-exec-interrupt') + if s:way == 'prompt' + call s:PromptInterrupt() + else + call s:SendCommand('-exec-interrupt') + endif sleep 10m endif " Use the fname:lnum format, older gdb can't handle --source. diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 19d0cac2d3..790609ab94 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -763,6 +763,9 @@ static void free_buffer(buf_T *buf) unref_var_dict(buf->b_vars); aubuflocal_remove(buf); tv_dict_unref(buf->additional_data); + xfree(buf->b_prompt_text); + callback_free(&buf->b_prompt_callback); + callback_free(&buf->b_prompt_interrupt); clear_fmark(&buf->b_last_cursor); clear_fmark(&buf->b_last_insert); clear_fmark(&buf->b_last_change); @@ -1876,6 +1879,10 @@ buf_T * buflist_new(char_u *ffname, char_u *sfname, linenr_T lnum, int flags) } } + buf->b_prompt_callback.type = kCallbackNone; + buf->b_prompt_interrupt.type = kCallbackNone; + buf->b_prompt_text = NULL; + return buf; } @@ -4824,6 +4831,12 @@ do_arg_all( xfree(opened); } +// Return TRUE if "buf" is a prompt buffer. +int bt_prompt(buf_T *buf) +{ + return buf != NULL && buf->b_p_bt[0] == 'p'; +} + /* * Open a window for a number of buffers. */ @@ -5218,14 +5231,18 @@ bool bt_nofile(const buf_T *const buf) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { return buf != NULL && ((buf->b_p_bt[0] == 'n' && buf->b_p_bt[2] == 'f') - || buf->b_p_bt[0] == 'a' || buf->terminal); + || buf->b_p_bt[0] == 'a' + || buf->terminal + || buf->b_p_bt[0] == 'p'); } // Return true if "buf" is a "nowrite", "nofile" or "terminal" buffer. bool bt_dontwrite(const buf_T *const buf) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - return buf != NULL && (buf->b_p_bt[0] == 'n' || buf->terminal); + return buf != NULL && (buf->b_p_bt[0] == 'n' + || buf->terminal + || buf->b_p_bt[0] == 'p'); } bool bt_dontwrite_msg(const buf_T *const buf) @@ -5278,6 +5295,9 @@ char_u *buf_spname(buf_T *buf) if (buf->b_fname != NULL) { return buf->b_fname; } + if (bt_prompt(buf)) { + return (char_u *)_("[Prompt]"); + } return (char_u *)_("[Scratch]"); } if (buf->b_fname == NULL) { diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index a0379740b6..9cdf36e4ed 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -791,6 +791,12 @@ struct file_buffer { // are not used! Use the B_SPELL macro to // access b_spell without #ifdef. + char_u *b_prompt_text; // set by prompt_setprompt() + Callback b_prompt_callback; // set by prompt_setcallback() + Callback b_prompt_interrupt; // set by prompt_setinterrupt() + int b_prompt_insert; // value for restart_edit when entering + // a prompt buffer window. + synblock_T b_s; // Info related to syntax highlighting. w_s // normally points to this, but some windows // may use a different synblock_T. diff --git a/src/nvim/diff.c b/src/nvim/diff.c index c31adc01fd..04309444d9 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -2432,6 +2432,10 @@ void nv_diffgetput(bool put, size_t count) exarg_T ea; char buf[30]; + if (bt_prompt(curbuf)) { + vim_beep(BO_OPER); + return; + } if (count == 0) { ea.arg = (char_u *)""; } else { diff --git a/src/nvim/edit.c b/src/nvim/edit.c index e253905057..cebd08af28 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -574,6 +574,12 @@ static int insert_check(VimState *state) foldCheckClose(); } + int cmdchar_todo = s->cmdchar; + if (bt_prompt(curbuf)) { + init_prompt(cmdchar_todo); + cmdchar_todo = NUL; + } + // If we inserted a character at the last position of the last line in the // window, scroll the window one line up. This avoids an extra redraw. This // is detected when the cursor column is smaller after inserting something. @@ -817,6 +823,16 @@ static int insert_handle_key(InsertState *s) s->nomove = true; return 0; // exit insert mode } + if (s->c == Ctrl_C && bt_prompt(curbuf)) { + if (invoke_prompt_interrupt()) { + if (!bt_prompt(curbuf)) { + // buffer changed to a non-prompt buffer, get out of + // Insert mode + return 0; + } + break; + } + } // when 'insertmode' set, and not halfway through a mapping, don't leave // Insert mode @@ -1143,6 +1159,15 @@ check_pum: cmdwin_result = CAR; return 0; } + if (bt_prompt(curbuf)) { + invoke_prompt_callback(); + if (!bt_prompt(curbuf)) { + // buffer changed to a non-prompt buffer, get out of + // Insert mode + return 0; + } + break; + } if (!ins_eol(s->c) && !p_im) { return 0; // out of memory } @@ -1569,6 +1594,52 @@ void edit_putchar(int c, int highlight) } } +// Return the effective prompt for the current buffer. +char_u *prompt_text(void) +{ + if (curbuf->b_prompt_text == NULL) { + return (char_u *)"% "; + } + return curbuf->b_prompt_text; +} + +// Prepare for prompt mode: Make sure the last line has the prompt text. +// Move the cursor to this line. +static void init_prompt(int cmdchar_todo) +{ + char_u *prompt = prompt_text(); + char_u *text; + + curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + text = get_cursor_line_ptr(); + if (STRNCMP(text, prompt, STRLEN(prompt)) != 0) { + // prompt is missing, insert it or append a line with it + if (*text == NUL) { + ml_replace(curbuf->b_ml.ml_line_count, prompt, true); + } else { + ml_append(curbuf->b_ml.ml_line_count, prompt, 0, false); + } + curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + coladvance((colnr_T)MAXCOL); + changed_bytes(curbuf->b_ml.ml_line_count, 0); + } + if (cmdchar_todo == 'A') { + coladvance((colnr_T)MAXCOL); + } + if (cmdchar_todo == 'I' || curwin->w_cursor.col <= (int)STRLEN(prompt)) { + curwin->w_cursor.col = STRLEN(prompt); + } + // Make sure the cursor is in a valid position. + check_cursor(); +} + +// Return TRUE if the cursor is in the editable position of the prompt line. +int prompt_curpos_editable(void) +{ + return curwin->w_cursor.lnum == curbuf->b_ml.ml_line_count + && curwin->w_cursor.col >= (int)STRLEN(prompt_text()); +} + /* * Undo the previous edit_putchar(). */ @@ -8161,10 +8232,14 @@ static void ins_mouse(int c) win_T *new_curwin = curwin; if (curwin != old_curwin && win_valid(old_curwin)) { - /* Mouse took us to another window. We need to go back to the - * previous one to stop insert there properly. */ + // Mouse took us to another window. We need to go back to the + // previous one to stop insert there properly. curwin = old_curwin; curbuf = curwin->w_buffer; + if (bt_prompt(curbuf)) { + // Restart Insert mode when re-entering the prompt buffer. + curbuf->b_prompt_insert = 'A'; + } } start_arrow(curwin == old_curwin ? &tpos : NULL); if (curwin != new_curwin && win_valid(new_curwin)) { diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 0dceca671b..ddafe1981f 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -5152,6 +5152,10 @@ bool garbage_collect(bool testing) } // buffer ShaDa additional data ABORTING(set_ref_dict)(buf->additional_data, copyID); + + // buffer callback functions + set_ref_in_callback(&buf->b_prompt_callback, copyID, NULL, NULL); + set_ref_in_callback(&buf->b_prompt_interrupt, copyID, NULL, NULL); } FOR_ALL_TAB_WINDOWS(tp, wp) { @@ -7315,9 +7319,7 @@ dict_T *get_win_info(win_T *wp, int16_t tpnr, int16_t winnr) return dict; } -/* - * Find window specified by "vp" in tabpage "tp". - */ +// Find window specified by "vp" in tabpage "tp". win_T * find_win_by_nr( typval_T *vp, @@ -13687,3 +13689,51 @@ void ex_checkhealth(exarg_T *eap) xfree(buf); } + +void invoke_prompt_callback(void) +{ + typval_T rettv; + typval_T argv[2]; + char_u *text; + char_u *prompt; + linenr_T lnum = curbuf->b_ml.ml_line_count; + + // Add a new line for the prompt before invoking the callback, so that + // text can always be inserted above the last line. + ml_append(lnum, (char_u *)"", 0, false); + curwin->w_cursor.lnum = lnum + 1; + curwin->w_cursor.col = 0; + + if (curbuf->b_prompt_callback.type == kCallbackNone) { + return; + } + text = ml_get(lnum); + prompt = prompt_text(); + if (STRLEN(text) >= STRLEN(prompt)) { + text += STRLEN(prompt); + } + argv[0].v_type = VAR_STRING; + argv[0].vval.v_string = vim_strsave(text); + argv[1].v_type = VAR_UNKNOWN; + + callback_call(&curbuf->b_prompt_callback, 1, argv, &rettv); + tv_clear(&argv[0]); + tv_clear(&rettv); +} + +// Return true When the interrupt callback was invoked. +bool invoke_prompt_interrupt(void) +{ + typval_T rettv; + typval_T argv[1]; + + if (curbuf->b_prompt_interrupt.type == kCallbackNone) { + return false; + } + argv[0].v_type = VAR_UNKNOWN; + + got_int = false; // don't skip executing commands + callback_call(&curbuf->b_prompt_interrupt, 0, argv, &rettv); + tv_clear(&rettv); + return true; +} diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index efeac70816..17d9cc56aa 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -82,6 +82,7 @@ return { ctxset={args={1, 2}}, ctxsize={}, cursor={args={1, 3}}, + debugbreak={args={1, 1}}, deepcopy={args={1, 2}}, delete={args={1,2}}, deletebufline={args={2,3}}, @@ -243,6 +244,9 @@ return { pow={args=2}, prevnonblank={args=1}, printf={args=varargs(1)}, + prompt_setcallback={args={2, 2}}, + prompt_setinterrupt={args={2, 2}}, + prompt_setprompt={args={2, 2}}, pum_getpos={}, pumvisible={}, py3eval={args=1}, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 8232136783..36782e4223 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -1408,9 +1408,31 @@ static void f_cursor(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = 0; } -/* - * "deepcopy()" function - */ +// "debugbreak()" function +static void f_debugbreak(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + int pid; + + rettv->vval.v_number = FAIL; + pid = (int)tv_get_number(&argvars[0]); + if (pid == 0) { + EMSG(_(e_invarg)); + } else { +#ifdef WIN32 + HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, pid); + + if (hProcess != NULL) { + DebugBreakProcess(hProcess); + CloseHandle(hProcess); + rettv->vval.v_number = OK; + } +#else + uv_kill(pid, SIGINT); +#endif + } +} + +// "deepcopy()" function static void f_deepcopy(typval_T *argvars, typval_T *rettv, FunPtr fptr) { int noref = 0; @@ -6076,6 +6098,76 @@ static void f_printf(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } +// "prompt_setcallback({buffer}, {callback})" function +static void f_prompt_setcallback(typval_T *argvars, + typval_T *rettv, FunPtr fptr) +{ + buf_T *buf; + Callback prompt_callback = { .type = kCallbackNone }; + + if (check_secure()) { + return; + } + buf = tv_get_buf(&argvars[0], false); + if (buf == NULL) { + return; + } + + if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) { + if (!callback_from_typval(&prompt_callback, &argvars[1])) { + return; + } + } + + callback_free(&buf->b_prompt_callback); + buf->b_prompt_callback = prompt_callback; +} + +// "prompt_setinterrupt({buffer}, {callback})" function +static void f_prompt_setinterrupt(typval_T *argvars, + typval_T *rettv, FunPtr fptr) +{ + buf_T *buf; + Callback interrupt_callback = { .type = kCallbackNone }; + + if (check_secure()) { + return; + } + buf = tv_get_buf(&argvars[0], false); + if (buf == NULL) { + return; + } + + if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) { + if (!callback_from_typval(&interrupt_callback, &argvars[1])) { + return; + } + } + + callback_free(&buf->b_prompt_interrupt); + buf->b_prompt_interrupt= interrupt_callback; +} + +// "prompt_setprompt({buffer}, {text})" function +static void f_prompt_setprompt(typval_T *argvars, + typval_T *rettv, FunPtr fptr) +{ + buf_T *buf; + const char_u *text; + + if (check_secure()) { + return; + } + buf = tv_get_buf(&argvars[0], false); + if (buf == NULL) { + return; + } + + text = (const char_u *)tv_get_string(&argvars[1]); + xfree(buf->b_prompt_text); + buf->b_prompt_text = vim_strsave(text); +} + // "pum_getpos()" function static void f_pum_getpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { diff --git a/src/nvim/normal.c b/src/nvim/normal.c index dac4c8f527..c69b10f99a 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -3642,7 +3642,9 @@ static void nv_help(cmdarg_T *cap) */ static void nv_addsub(cmdarg_T *cap) { - if (!VIsual_active && cap->oap->op_type == OP_NOP) { + if (bt_prompt(curbuf) && !prompt_curpos_editable()) { + clearopbeep(cap->oap); + } else if (!VIsual_active && cap->oap->op_type == OP_NOP) { prep_redo_cmd(cap); cap->oap->op_type = cap->cmdchar == Ctrl_A ? OP_NR_ADD : OP_NR_SUB; op_addsub(cap->oap, cap->count1, cap->arg); @@ -5239,6 +5241,13 @@ static void nv_down(cmdarg_T *cap) // In the cmdline window a <CR> executes the command. if (cmdwin_type != 0 && cap->cmdchar == CAR) { cmdwin_result = CAR; + } else if (bt_prompt(curbuf) && cap->cmdchar == CAR + && curwin->w_cursor.lnum == curbuf->b_ml.ml_line_count) { + // In a prompt buffer a <CR> in the last line invokes the callback. + invoke_prompt_callback(); + if (restart_edit == 0) { + restart_edit = 'a'; + } } else { cap->oap->motion_type = kMTLineWise; if (cursor_down(cap->count1, cap->oap->op_type == OP_NOP) == false) { @@ -5831,6 +5840,10 @@ static void nv_undo(cmdarg_T *cap) static void nv_kundo(cmdarg_T *cap) { if (!checkclearopq(cap->oap)) { + if (bt_prompt(curbuf)) { + clearopbeep(cap->oap); + return; + } u_undo((int)cap->count1); curwin->w_set_curswant = true; } @@ -5844,8 +5857,13 @@ static void nv_replace(cmdarg_T *cap) char_u *ptr; int had_ctrl_v; - if (checkclearop(cap->oap)) + if (checkclearop(cap->oap)) { + return; + } + if (bt_prompt(curbuf) && !prompt_curpos_editable()) { + clearopbeep(cap->oap); return; + } /* get another character */ if (cap->nchar == Ctrl_V) { @@ -6182,7 +6200,11 @@ static void v_visop(cmdarg_T *cap) */ static void nv_subst(cmdarg_T *cap) { - if (VIsual_active) { /* "vs" and "vS" are the same as "vc" */ + if (bt_prompt(curbuf) && !prompt_curpos_editable()) { + clearopbeep(cap->oap); + return; + } + if (VIsual_active) { // "vs" and "vS" are the same as "vc" if (cap->cmdchar == 'S') { VIsual_mode_orig = VIsual_mode; VIsual_mode = 'V'; @@ -7120,10 +7142,15 @@ static void nv_tilde(cmdarg_T *cap) { if (!p_to && !VIsual_active - && cap->oap->op_type != OP_TILDE) + && cap->oap->op_type != OP_TILDE) { + if (bt_prompt(curbuf) && !prompt_curpos_editable()) { + clearopbeep(cap->oap); + return; + } n_swapchar(cap); - else + } else { nv_operator(cap); + } } /* @@ -7136,6 +7163,12 @@ static void nv_operator(cmdarg_T *cap) op_type = get_op_type(cap->cmdchar, cap->nchar); + if (bt_prompt(curbuf) && op_is_change(op_type) + && !prompt_curpos_editable()) { + clearopbeep(cap->oap); + return; + } + if (op_type == cap->oap->op_type) /* double operator works on lines */ nv_lineop(cap); else if (!checkclearop(cap->oap)) { @@ -7796,8 +7829,11 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) clearop(cap->oap); assert(cap->opcount >= 0); nv_diffgetput(true, (size_t)cap->opcount); - } else + } else { clearopbeep(cap->oap); + } + } else if (bt_prompt(curbuf) && !prompt_curpos_editable()) { + clearopbeep(cap->oap); } else { if (fix_indent) { dir = (cap->cmdchar == ']' && cap->nchar == 'p') @@ -7809,8 +7845,9 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) ? BACKWARD : FORWARD; } prep_redo_cmd(cap); - if (cap->cmdchar == 'g') + if (cap->cmdchar == 'g') { flags |= PUT_CURSEND; + } if (VIsual_active) { /* Putting in Visual mode: The put text replaces the selected @@ -7916,10 +7953,14 @@ static void nv_open(cmdarg_T *cap) clearop(cap->oap); assert(cap->opcount >= 0); nv_diffgetput(false, (size_t)cap->opcount); - } else if (VIsual_active) /* switch start and end of visual */ + } else if (VIsual_active) { + // switch start and end of visual/ v_swap_corners(cap->cmdchar); - else + } else if (bt_prompt(curbuf)) { + clearopbeep(cap->oap); + } else { n_opencmd(cap); + } } // Calculate start/end virtual columns for operating in block mode. diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 6d327c0814..735a33ca97 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -89,6 +89,10 @@ struct block_def { # include "ops.c.generated.h" #endif +// Flags for third item in "opchars". +#define OPF_LINES 1 // operator always works on lines +#define OPF_CHANGE 2 // operator changes text + /* * The names of operators. * IMPORTANT: Index must correspond with defines in vim.h!!! @@ -96,36 +100,36 @@ struct block_def { */ static char opchars[][3] = { - { NUL, NUL, false }, // OP_NOP - { 'd', NUL, false }, // OP_DELETE - { 'y', NUL, false }, // OP_YANK - { 'c', NUL, false }, // OP_CHANGE - { '<', NUL, true }, // OP_LSHIFT - { '>', NUL, true }, // OP_RSHIFT - { '!', NUL, true }, // OP_FILTER - { 'g', '~', false }, // OP_TILDE - { '=', NUL, true }, // OP_INDENT - { 'g', 'q', true }, // OP_FORMAT - { ':', NUL, true }, // OP_COLON - { 'g', 'U', false }, // OP_UPPER - { 'g', 'u', false }, // OP_LOWER - { 'J', NUL, true }, // DO_JOIN - { 'g', 'J', true }, // DO_JOIN_NS - { 'g', '?', false }, // OP_ROT13 - { 'r', NUL, false }, // OP_REPLACE - { 'I', NUL, false }, // OP_INSERT - { 'A', NUL, false }, // OP_APPEND - { 'z', 'f', true }, // OP_FOLD - { 'z', 'o', true }, // OP_FOLDOPEN - { 'z', 'O', true }, // OP_FOLDOPENREC - { 'z', 'c', true }, // OP_FOLDCLOSE - { 'z', 'C', true }, // OP_FOLDCLOSEREC - { 'z', 'd', true }, // OP_FOLDDEL - { 'z', 'D', true }, // OP_FOLDDELREC - { 'g', 'w', true }, // OP_FORMAT2 - { 'g', '@', false }, // OP_FUNCTION - { Ctrl_A, NUL, false }, // OP_NR_ADD - { Ctrl_X, NUL, false }, // OP_NR_SUB + { NUL, NUL, 0 }, // OP_NOP + { 'd', NUL, OPF_CHANGE }, // OP_DELETE + { 'y', NUL, 0 }, // OP_YANK + { 'c', NUL, OPF_CHANGE }, // OP_CHANGE + { '<', NUL, OPF_LINES | OPF_CHANGE }, // OP_LSHIFT + { '>', NUL, OPF_LINES | OPF_CHANGE }, // OP_RSHIFT + { '!', NUL, OPF_LINES | OPF_CHANGE }, // OP_FILTER + { 'g', '~', OPF_CHANGE }, // OP_TILDE + { '=', NUL, OPF_LINES | OPF_CHANGE }, // OP_INDENT + { 'g', 'q', OPF_LINES | OPF_CHANGE }, // OP_FORMAT + { ':', NUL, OPF_LINES }, // OP_COLON + { 'g', 'U', OPF_CHANGE }, // OP_UPPER + { 'g', 'u', OPF_CHANGE }, // OP_LOWER + { 'J', NUL, OPF_LINES | OPF_CHANGE }, // DO_JOIN + { 'g', 'J', OPF_LINES | OPF_CHANGE }, // DO_JOIN_NS + { 'g', '?', OPF_CHANGE }, // OP_ROT13 + { 'r', NUL, OPF_CHANGE }, // OP_REPLACE + { 'I', NUL, OPF_CHANGE }, // OP_INSERT + { 'A', NUL, OPF_CHANGE }, // OP_APPEND + { 'z', 'f', OPF_LINES }, // OP_FOLD + { 'z', 'o', OPF_LINES }, // OP_FOLDOPEN + { 'z', 'O', OPF_LINES }, // OP_FOLDOPENREC + { 'z', 'c', OPF_LINES }, // OP_FOLDCLOSE + { 'z', 'C', OPF_LINES }, // OP_FOLDCLOSEREC + { 'z', 'd', OPF_LINES }, // OP_FOLDDEL + { 'z', 'D', OPF_LINES }, // OP_FOLDDELREC + { 'g', 'w', OPF_LINES | OPF_CHANGE }, // OP_FORMAT2 + { 'g', '@', OPF_CHANGE }, // OP_FUNCTION + { Ctrl_A, NUL, OPF_CHANGE }, // OP_NR_ADD + { Ctrl_X, NUL, OPF_CHANGE }, // OP_NR_SUB }; /* @@ -169,7 +173,13 @@ int get_op_type(int char1, int char2) */ int op_on_lines(int op) { - return opchars[op][2]; + return opchars[op][2] & OPF_LINES; +} + +// Return TRUE if operator "op" changes text. +int op_is_change(int op) +{ + return opchars[op][2] & OPF_CHANGE; } /* diff --git a/src/nvim/option.c b/src/nvim/option.c index 15ff8414ce..100902897a 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -299,7 +299,8 @@ static char *(p_scbopt_values[]) = { "ver", "hor", "jump", NULL }; static char *(p_debug_values[]) = { "msg", "throw", "beep", NULL }; static char *(p_ead_values[]) = { "both", "ver", "hor", NULL }; static char *(p_buftype_values[]) = { "nofile", "nowrite", "quickfix", - "help", "acwrite", "terminal", NULL }; + "help", "acwrite", "terminal", + "prompt", NULL }; static char *(p_bufhidden_values[]) = { "hide", "unload", "delete", "wipe", NULL }; @@ -7091,10 +7092,13 @@ static int check_opt_wim(void) */ bool can_bs(int what) { + if (what == BS_START && bt_prompt(curbuf)) { + return false; + } switch (*p_bs) { - case '2': return true; - case '1': return what != BS_START; - case '0': return false; + case '2': return true; + case '1': return what != BS_START; + case '0': return false; } return vim_strchr(p_bs, what) != NULL; } diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 1f74bada41..df92b2c036 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -2971,7 +2971,10 @@ static char_u *u_save_line(linenr_T lnum) bool bufIsChanged(buf_T *buf) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - return !bt_dontwrite(buf) && (buf->b_changed || file_ff_differs(buf, true)); + // 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)); } // Return true if any buffer has changes. Also buffers that are not written. diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua new file mode 100644 index 0000000000..513be807be --- /dev/null +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -0,0 +1,153 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local feed= helpers.feed +local source = helpers.source +local clear = helpers.clear +local feed_command = helpers.feed_command + +describe('prompt buffer', function() + local screen + + before_each(function() + clear() + screen = Screen.new(25, 10) + screen:attach() + source([[ + func TextEntered(text) + if a:text == "exit" + set nomodified + stopinsert + close + else + call append(line("$") - 1, 'Command: "' . a:text . '"') + set nomodfied + call timer_start(20, {id -> TimerFunc(a:text)}) + endif + endfunc + + func TimerFunc(text) + call append(line("$") - 1, 'Result: "' . a:text .'"') + endfunc + ]]) + feed_command("set noshowmode | set laststatus=0") + feed_command("call setline(1, 'other buffer')") + feed_command("new") + feed_command("set buftype=prompt") + feed_command("call prompt_setcallback(bufnr(''), function('TextEntered'))") + end) + + after_each(function() + screen:detach() + end) + + it('works', function() + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + [Prompt] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("i") + feed("hello\n") + screen:expect([[ + % hello | + Command: "hello" | + Result: "hello" | + % ^ | + [Prompt] [+] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("exit\n") + screen:expect([[ + ^other buffer | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + end) + + it('editing', function() + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + [Prompt] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("i") + feed("hello<BS><BS>") + screen:expect([[ + % hel^ | + ~ | + ~ | + ~ | + [Prompt] [+] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("<Left><Left><Left><BS>-") + screen:expect([[ + % -^hel | + ~ | + ~ | + ~ | + [Prompt] [+] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("<End>x") + screen:expect([[ + % -helx^ | + ~ | + ~ | + ~ | + [Prompt] [+] | + other buffer | + ~ | + ~ | + ~ | + | + ]]) + feed("<C-U>exit\n") + screen:expect([[ + ^other buffer | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + end) + +end) |