Problem: Job in terminal can't communicate with Vim.
Solution: Add the terminal API.
-*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
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|
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*
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\<CR>")
+
+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, "\<Esc>]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: >
+ <Esc>]51;["drop", "README.md"]<07>
+
+The body is always a list, making it easy to find the end: ]<07>.
+The <Esc>]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=\<Esc>]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
==============================================================================
-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
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
}
}
-#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.
*/
* 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.
}
}
+/*
+ * 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.
*/
{
VTerm *vterm;
VTermScreen *screen;
+ VTermState *state;
VTermValue value;
vterm = vterm_new(rows, cols);
#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);
}
/*
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
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=\<Esc>]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=\<Esc>]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=\<Esc>]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
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1641,
/**/
1640,
/**/