]> granicus.if.org Git - vim/commitdiff
patch 8.0.1641: job in terminal can't communicate with Vim v8.0.1641
authorBram Moolenaar <Bram@vim.org>
Sun, 25 Mar 2018 16:20:17 +0000 (18:20 +0200)
committerBram Moolenaar <Bram@vim.org>
Sun, 25 Mar 2018 16:20:17 +0000 (18:20 +0200)
Problem:    Job in terminal can't communicate with Vim.
Solution:   Add the terminal API.

runtime/doc/terminal.txt
src/buffer.c
src/terminal.c
src/testdir/screendump.vim
src/testdir/test_terminal.vim
src/version.c

index 36b0e8e74a3043178cf268539b646f826bcfa75b..0c47fb637239aec3945a07e0736364ec15135f27 100644 (file)
@@ -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 @@ If the result is "1" you have it.
       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 information to the running job:
     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 information to the running job:
     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
@@ -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 first and second dump. Do this several
 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
index c7f361af0a5f75eb98929bb3104c5715fcd880c7..7bd3cdf0163b03b2d006e4989ea412f9b7183a17 100644 (file)
@@ -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.
  */
index 4f29b56699c54b09be9883dd68c1acca98a4c012..50e87c9e450e74ad70dcdeed4b8a31cc0d1f3b0d 100644 (file)
  * 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.
@@ -3145,6 +3144,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.
  */
@@ -3153,6 +3286,7 @@ create_vterm(term_T *term, int rows, int cols)
 {
     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 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);
 }
 
 /*
index c9b8085dead984f5e8367ebdfbd68914c118f647..093b483f10dcf44cabb1a1db99f7223d2b6b7bc2 100644 (file)
@@ -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
 
index f2fbaf4676fa738163a059f59635e923327ce956..a7e5f616831b7fe96e6d52f908652d3109f276a5 100644 (file)
@@ -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=\<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
index d4631ced10ca98df379eebe03976aedb74de4710..3295eb0892ced756443c59a1ba56ce4397f471de 100644 (file)
@@ -766,6 +766,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1641,
 /**/
     1640,
 /**/