# HG changeset patch # User Christian Brabandt # Date 1521995407 -7200 # Node ID e9ffb5b352663a6e05296f70e08d0fd3c31eee6e # Parent e74cff16bab65be78afcb050e98d98c8304ef95b patch 8.0.1641: job in terminal can't communicate with Vim commit https://github.com/vim/vim/commit/8fbaeb195d9298c3a2a80300b5f96f1adddd2f59 Author: Bram Moolenaar Date: Sun Mar 25 18:20:17 2018 +0200 patch 8.0.1641: job in terminal can't communicate with Vim Problem: Job in terminal can't communicate with Vim. Solution: Add the terminal API. diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -1,4 +1,4 @@ -*terminal.txt* For Vim version 8.0. Last change: 2018 Mar 16 +*terminal.txt* For Vim version 8.0. Last change: 2018 Mar 25 VIM REFERENCE MANUAL by Bram Moolenaar @@ -23,12 +23,16 @@ 1. Basic use |terminal-use| Session |terminal-session| Unix |terminal-unix| MS-Windows |terminal-ms-windows| -2. Remote testing |terminal-testing| -3. Diffing screen dumps |terminal-diff| +2. Terminal communication |terminal-communication| + Vim to job: term_sendkeys() |terminal-to-job| + Job to Vim: JSON API |terminal-api| + Using the client-server feature |terminal-client-server| +3. Remote testing |terminal-testing| +4. Diffing screen dumps |terminal-diff| Writing a screen dump test for Vim |terminal-dumptest| Creating a screen dump |terminal-screendump| Comparing screen dumps |terminal-diffscreendump| -4. Debugging |terminal-debug| +5. Debugging |terminal-debug| Starting |termdebug-starting| Example session |termdebug-example| Stepping through code |termdebug-stepping| @@ -355,15 +359,6 @@ Environment variables are used to pass i COLORS number of colors, 't_Co' (256*256*256 in the GUI) VIM_SERVERNAME v:servername -The |client-server| feature can be used to communicate with the Vim instance -where the job was started. This only works when v:servername is not empty. -If needed you can set it with: > - call remote_startserver('vim-server') - -In the job you can then do something like: > - vim --servername $VIM_SERVERNAME --remote +123 some_file.c -This will open the file "some_file.c" and put the cursor on line 123. - MS-Windows ~ *terminal-ms-windows* @@ -389,7 +384,91 @@ Environment variables are used to pass i VIM_SERVERNAME v:servername ============================================================================== -2. Remote testing *terminal-testing* +2. Terminal communication *terminal-communication* + +There are several ways to communicate with the job running in a terminal: +- Use |term_sendkeys()| to send text and escape sequences from Vim to the job. +- Use the JSON API to send encoded commands from the job to Vim. +- Use the |client-server| mechanism. This works on machines with an X server + and on MS-Windows. + + +Vim to job: term_sendkeys() ~ + *terminal-to-job* +This allows for remote controlling the job running in the terminal. It is a +one-way mechanism. The job can update the display to signal back to Vim. +For example, if a shell is running in a terminal, you can do: > + call term_sendkeys(buf, "ls *.java\") + +This requires for the job to be in the right state where it will do the right +thing when receiving the keys. For the above example, the shell must be +waiting for a command to be typed. + +For a job that was written for the purpose, you can use the JSON API escape +sequence in the other direction. E.g.: > + call term_sendkeys(buf, "\]51;["response"]\x07") + + +Job to Vim: JSON API ~ + *terminal-api* +The job can send JSON to Vim, using a special escape sequence. The JSON +encodes a command that Vim understands. Example of such a message: > + ]51;["drop", "README.md"]<07> + +The body is always a list, making it easy to find the end: ]<07>. +The ]51;msg<07> sequence is reserved by xterm for "Emacs shell", which is +similar to what we are doing here. + +Currently supported commands: + + call {funcname} {argument} + + Call a user defined function with [argument]. The function is + called with the buffer number of the terminal and the decoded + argument. The user function must sanity check the argument. + The function can use |term_sendkeys()| to send back a reply. + Example in JSON: > + ["call", "Impression", ["play", 14]] +< Calls a function defined like this: > + function Impression(bufnum, arglist) + if len(a:arglist) == 2 + echo "impression " . a:arglist[0] + echo "count " . a:arglist[1] + endif + endfunc +< + drop {filename} + + Let Vim open a file, like the `:drop` command. If {filename} + is already open in a window, switch to that window. Otherwise + open a new window to edit {filename}. + Example in JSON: > + ["drop", "path/file.txt", {"ff": "dos"}] + +A trick to have Vim send this escape sequence: > + exe "set t_ts=\]51; t_fs=\x07" + let &titlestring = '["call","TryThis",["hello",123]]' + redraw + set t_ts& t_fs& + +Rationale: Why not allow for any command or expression? Because that might +create a security problem. + + +Using the client-server feature ~ + *terminal-client-server* +This only works when v:servername is not empty. If needed you can set it, +before opening the terminal, with: > + call remote_startserver('vim-server') + +$VIM_SERVERNAME is set in the terminal to pass on the server name. + +In the job you can then do something like: > + vim --servername $VIM_SERVERNAME --remote +123 some_file.c +This will open the file "some_file.c" and put the cursor on line 123. + +============================================================================== +3. Remote testing *terminal-testing* Most Vim tests execute a script inside Vim. For some tests this does not work, running the test interferes with the code being tested. To avoid this @@ -404,7 +483,7 @@ term_scrape() inspect terminal screen ============================================================================== -3. Diffing screen dumps *terminal-diff* +4. Diffing screen dumps *terminal-diff* In some cases it can be bothersome to test that Vim displays the right characters on the screen. E.g. with syntax highlighting. To make this @@ -494,7 +573,7 @@ Alternatively, press "s" to swap the fir times so that you can spot the difference in the context of the text. ============================================================================== -4. Debugging *terminal-debug* +5. Debugging *terminal-debug* The Terminal debugging plugin can be used to debug a program with gdb and view the source code in a Vim window. Since this is completely contained inside diff --git a/src/buffer.c b/src/buffer.c --- a/src/buffer.c +++ b/src/buffer.c @@ -948,7 +948,7 @@ clear_wininfo(buf_T *buf) } } -#if defined(FEAT_LISTCMDS) || defined(PROTO) +#if defined(FEAT_LISTCMDS) || defined(FEAT_TERMINAL) || defined(PROTO) /* * Go to another buffer. Handles the result of the ATTENTION dialog. */ diff --git a/src/terminal.c b/src/terminal.c --- a/src/terminal.c +++ b/src/terminal.c @@ -38,12 +38,11 @@ * in tl_scrollback are no longer used. * * TODO: - * - Win32: In the GUI use a terminal emulator for :!cmd. + * - For the "drop" command accept another argument for options. * - Add a way to set the 16 ANSI colors, to be used for 'termguicolors' and in * the GUI. - * - Some way for the job running in the terminal to send a :drop command back - * to the Vim running the terminal. Should be usable by a simple shell or - * python script. + * - Win32: Make terminal used for :!cmd in the GUI work better. Allow for + * redirection. * - implement term_setsize() * - Copy text in the vterm to the Vim buffer once in a while, so that * completion works. @@ -3146,6 +3145,140 @@ init_default_colors(term_T *term) } /* + * Handles a "drop" command from the job in the terminal. + * "item" is the file name, "item->li_next" may have options. + */ + static void +handle_drop_command(listitem_T *item) +{ + char_u *fname = get_tv_string(&item->li_tv); + int bufnr; + win_T *wp; + tabpage_T *tp; + exarg_T ea; + + bufnr = buflist_add(fname, BLN_LISTED | BLN_NOOPT); + FOR_ALL_TAB_WINDOWS(tp, wp) + { + if (wp->w_buffer->b_fnum == bufnr) + { + /* buffer is in a window already, go there */ + goto_tabpage_win(tp, wp); + return; + } + } + + /* open in new window, like ":sbuffer N" */ + vim_memset(&ea, 0, sizeof(ea)); + ea.cmd = (char_u *)"sbuffer"; + goto_buffer(&ea, DOBUF_FIRST, FORWARD, bufnr); +} + +/* + * Handles a function call from the job running in a terminal. + * "item" is the function name, "item->li_next" has the arguments. + */ + static void +handle_call_command(term_T *term, channel_T *channel, listitem_T *item) +{ + char_u *func; + typval_T argvars[2]; + typval_T rettv; + int doesrange; + + if (item->li_next == NULL) + { + ch_log(channel, "Missing function arguments for call"); + return; + } + func = get_tv_string(&item->li_tv); + + if (!ASCII_ISUPPER(*func)) + { + ch_log(channel, "Invalid function name: %s", func); + return; + } + + argvars[0].v_type = VAR_NUMBER; + argvars[0].vval.v_number = term->tl_buffer->b_fnum; + argvars[1] = item->li_next->li_tv; + if (call_func(func, STRLEN(func), &rettv, + 2, argvars, /* argv_func */ NULL, + /* firstline */ 1, /* lastline */ 1, + &doesrange, /* evaluate */ TRUE, + /* partial */ NULL, /* selfdict */ NULL) == OK) + { + clear_tv(&rettv); + ch_log(channel, "Function %s called", func); + } + else + ch_log(channel, "Calling function %s failed", func); +} + +/* + * Called by libvterm when it cannot recognize an OSC sequence. + * We recognize a terminal API command. + */ + static int +parse_osc(const char *command, size_t cmdlen, void *user) +{ + term_T *term = (term_T *)user; + js_read_T reader; + typval_T tv; + channel_T *channel = term->tl_job == NULL ? NULL + : term->tl_job->jv_channel; + + /* We recognize only OSC 5 1 ; {command} */ + if (cmdlen < 3 || STRNCMP(command, "51;", 3) != 0) + return 0; /* not handled */ + + reader.js_buf = vim_strnsave((char_u *)command + 3, cmdlen - 3); + if (reader.js_buf == NULL) + return 1; + reader.js_fill = NULL; + reader.js_used = 0; + if (json_decode(&reader, &tv, 0) == OK + && tv.v_type == VAR_LIST + && tv.vval.v_list != NULL) + { + listitem_T *item = tv.vval.v_list->lv_first; + + if (item == NULL) + ch_log(channel, "Missing command"); + else + { + char_u *cmd = get_tv_string(&item->li_tv); + + item = item->li_next; + if (item == NULL) + ch_log(channel, "Missing argument for %s", cmd); + else if (STRCMP(cmd, "drop") == 0) + handle_drop_command(item); + else if (STRCMP(cmd, "call") == 0) + handle_call_command(term, channel, item); + else + ch_log(channel, "Invalid command received: %s", cmd); + } + } + else + ch_log(channel, "Invalid JSON received"); + + vim_free(reader.js_buf); + clear_tv(&tv); + return 1; +} + +static VTermParserCallbacks parser_fallbacks = { + NULL, /* text */ + NULL, /* control */ + NULL, /* escape */ + NULL, /* csi */ + parse_osc, /* osc */ + NULL, /* dcs */ + NULL /* resize */ +}; + +/* * Create a new vterm and initialize it. */ static void @@ -3153,6 +3286,7 @@ create_vterm(term_T *term, int rows, int { VTerm *vterm; VTermScreen *screen; + VTermState *state; VTermValue value; vterm = vterm_new(rows, cols); @@ -3186,8 +3320,9 @@ create_vterm(term_T *term, int rows, int #else value.boolean = 0; #endif - vterm_state_set_termprop(vterm_obtain_state(vterm), - VTERM_PROP_CURSORBLINK, &value); + state = vterm_obtain_state(vterm); + vterm_state_set_termprop(state, VTERM_PROP_CURSORBLINK, &value); + vterm_state_set_unrecognised_fallbacks(state, &parser_fallbacks, term); } /* diff --git a/src/testdir/screendump.vim b/src/testdir/screendump.vim --- a/src/testdir/screendump.vim +++ b/src/testdir/screendump.vim @@ -38,8 +38,8 @@ func RunVimInTerminal(arguments, options endif " Make a horizontal and vertical split, so that we can get exactly the right - " size terminal window. Works only when we currently have one window. - call assert_equal(1, winnr('$')) + " size terminal window. Works only when the current window is full width. + call assert_equal(&columns, winwidth(0)) split vsplit diff --git a/src/testdir/test_terminal.vim b/src/testdir/test_terminal.vim --- a/src/testdir/test_terminal.vim +++ b/src/testdir/test_terminal.vim @@ -1023,3 +1023,79 @@ func Test_terminal_dumpdiff_options() set laststatus& endfunc + +func Test_terminal_api_drop_newwin() + if !CanRunVimInTerminal() + return + endif + call assert_equal(1, winnr('$')) + + " Use the title termcap entries to output the escape sequence. + call writefile([ + \ 'exe "set t_ts=\]51; t_fs=\x07"', + \ 'let &titlestring = ''["drop","Xtextfile"]''', + \ 'redraw', + \ "set t_ts=", + \ ], 'Xscript') + let buf = RunVimInTerminal('-S Xscript', {}) + call WaitFor({-> bufnr('Xtextfile') > 0}) + call assert_equal('Xtextfile', expand('%:t')) + call assert_true(winnr('$') >= 3) + + call StopVimInTerminal(buf) + call delete('Xscript') + bwipe Xtextfile +endfunc + +func Test_terminal_api_drop_oldwin() + if !CanRunVimInTerminal() + return + endif + let firstwinid = win_getid() + split Xtextfile + let textfile_winid = win_getid() + call assert_equal(2, winnr('$')) + call win_gotoid(firstwinid) + + " Use the title termcap entries to output the escape sequence. + call writefile([ + \ 'exe "set t_ts=\]51; t_fs=\x07"', + \ 'let &titlestring = ''["drop","Xtextfile"]''', + \ 'redraw', + \ "set t_ts=", + \ ], 'Xscript') + let buf = RunVimInTerminal('-S Xscript', {}) + call WaitFor({-> expand('%:t') =='Xtextfile'}) + call assert_equal(textfile_winid, win_getid()) + + call StopVimInTerminal(buf) + call delete('Xscript') + bwipe Xtextfile +endfunc + +func TryThis(bufnum, arg) + let g:called_bufnum = a:bufnum + let g:called_arg = a:arg +endfunc + +func Test_terminal_api_call() + if !CanRunVimInTerminal() + return + endif + " Use the title termcap entries to output the escape sequence. + call writefile([ + \ 'exe "set t_ts=\]51; t_fs=\x07"', + \ 'let &titlestring = ''["call","TryThis",["hello",123]]''', + \ 'redraw', + \ "set t_ts=", + \ ], 'Xscript') + let buf = RunVimInTerminal('-S Xscript', {}) + call WaitFor({-> exists('g:called_bufnum')}) + call assert_equal(buf, g:called_bufnum) + call assert_equal(['hello', 123], g:called_arg) + + call StopVimInTerminal(buf) + call delete('Xscript') + unlet g:called_bufnum + unlet g:called_arg +endfunc diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -767,6 +767,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1641, +/**/ 1640, /**/ 1639,