feat(jobs): jobstart(…,{term=true}), deprecate termopen() #31343

Problem:
`termopen` has long been a superficial wrapper around `jobstart`, and
has no real purpose. Also, `vim.system` and `nvim_open_term` presumably
will replace all features of `jobstart` and `termopen`, so centralizing
the logic will help with that.

Solution:
- Introduce `eval/deprecated.c`, where all deprecated eval funcs will live.
- Introduce "term" flag of `jobstart`.
- Deprecate `termopen`.
This commit is contained in:
Justin M. Keyes
2024-12-19 07:07:04 -08:00
committed by GitHub
parent a5a4149e97
commit 8ef41f5902
41 changed files with 466 additions and 435 deletions

View File

@@ -1181,12 +1181,12 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Arena *arena,
/// This temporarily switches current buffer to "buffer".
/// If the current window already shows "buffer", the window is not switched.
/// If a window inside the current tabpage (including a float) already shows the
/// buffer, then one of these windows will be set as current window temporarily.
/// buffer, then one of those windows will be set as current window temporarily.
/// Otherwise a temporary scratch window (called the "autocmd window" for
/// historical reasons) will be used.
///
/// This is useful e.g. to call Vimscript functions that only work with the
/// current buffer/window currently, like |termopen()|.
/// current buffer/window currently, like `jobstart(…, {'term': v:true})`.
///
/// @param buffer Buffer handle, or 0 for current buffer
/// @param fun Function to call inside the buffer (currently Lua callable

View File

@@ -8493,7 +8493,7 @@ char *do_string_sub(char *str, size_t len, char *pat, char *sub, typval_T *expr,
return ret;
}
/// common code for getting job callbacks for jobstart, termopen and rpcstart
/// Common code for getting job callbacks for `jobstart`.
///
/// @return true/false on success/failure.
bool common_job_callbacks(dict_T *vopts, CallbackReader *on_stdout, CallbackReader *on_stderr,

View File

@@ -5945,7 +5945,7 @@ M.funcs = {
jobstart = {
args = { 1, 2 },
desc = [=[
Note: Prefer |vim.system()| in Lua (unless using the `pty` option).
Note: Prefer |vim.system()| in Lua (unless using `rpc`, `pty`, or `term`).
Spawns {cmd} as a job.
If {cmd} is a List it runs directly (no 'shell').
@@ -5953,8 +5953,11 @@ M.funcs = {
call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}'])
<(See |shell-unquoting| for details.)
Example: >vim
call jobstart('nvim -h', {'on_stdout':{j,d,e->append(line('.'),d)}})
Example: start a job and handle its output: >vim
call jobstart(['nvim', '-h'], {'on_stdout':{j,d,e->append(line('.'),d)}})
<
Example: start a job in a |terminal| connected to the current buffer: >vim
call jobstart(['nvim', '-h'], {'term':v:true})
<
Returns |job-id| on success, 0 on invalid arguments (or job
table is full), -1 if {cmd}[0] or 'shell' is not executable.
@@ -6019,6 +6022,10 @@ M.funcs = {
stdin: (string) Either "pipe" (default) to connect the
job's stdin to a channel or "null" to disconnect
stdin.
term: (boolean) Spawns {cmd} in a new pseudo-terminal session
connected to the current (unmodified) buffer. Implies "pty".
Default "height" and "width" are set to the current window
dimensions. |jobstart()|. Defaults $TERM to "xterm-256color".
width: (number) Width of the `pty` terminal.
{opts} is passed as |self| dictionary to the callback; the
@@ -12271,21 +12278,10 @@ M.funcs = {
signature = 'tempname()',
},
termopen = {
deprecated = true,
args = { 1, 2 },
desc = [=[
Spawns {cmd} in a new pseudo-terminal session connected
to the current (unmodified) buffer. Parameters and behavior
are the same as |jobstart()| except "pty", "width", "height",
and "TERM" are ignored: "height" and "width" are taken from
the current window. Note that termopen() implies a "pty" arg
to jobstart(), and thus has the implications documented at
|jobstart()|.
Returns the same values as jobstart().
Terminal environment is initialized as in |jobstart-env|,
except $TERM is set to "xterm-256color". Full behavior is
described in |terminal|.
Use |jobstart()| with `{term: v:true}` instead.
]=],
name = 'termopen',
params = { { 'cmd', 'string|string[]' }, { 'opts', 'table' } },

View File

@@ -0,0 +1,44 @@
#include <stdbool.h> // for true
#include "nvim/errors.h"
#include "nvim/eval/deprecated.h"
#include "nvim/eval/funcs.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/ex_cmds.h"
#include "nvim/gettext_defs.h" // for _
#include "nvim/macros_defs.h" // for S_LEN
#include "nvim/message.h" // for semsg
#include "nvim/types_defs.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval/deprecated.c.generated.h"
#endif
/// "termopen(cmd[, cwd])" function
void f_termopen(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (check_secure()) {
return;
}
bool must_free = false;
if (argvars[1].v_type == VAR_UNKNOWN) {
must_free = true;
argvars[1].v_type = VAR_DICT;
argvars[1].vval.v_dict = tv_dict_alloc();
}
if (argvars[1].v_type != VAR_DICT) {
// Wrong argument types
semsg(_(e_invarg2), "expected dictionary");
return;
}
tv_dict_add_bool(argvars[1].vval.v_dict, S_LEN("term"), true);
f_jobstart(argvars, rettv, fptr);
if (must_free) {
tv_dict_free(argvars[1].vval.v_dict);
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <stdbool.h> // for true
#include "nvim/eval/typval_defs.h" // IWYU pragma: keep
#include "nvim/types_defs.h" // IWYU pragma: keep
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval/deprecated.h.generated.h"
#endif

View File

@@ -38,6 +38,7 @@
#include "nvim/eval.h"
#include "nvim/eval/buffer.h"
#include "nvim/eval/decode.h"
#include "nvim/eval/deprecated.h"
#include "nvim/eval/encode.h"
#include "nvim/eval/executor.h"
#include "nvim/eval/funcs.h"
@@ -3840,8 +3841,8 @@ static const char *required_env_vars[] = {
NULL
};
static dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, const bool pty,
const char * const pty_term_name)
dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, const bool pty,
const char * const pty_term_name)
{
dict_T *env = tv_dict_alloc();
@@ -3933,7 +3934,7 @@ static dict_T *create_environment(const dictitem_T *job_env, const bool clear_en
}
/// "jobstart()" function
static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
rettv->v_type = VAR_NUMBER;
rettv->vval.v_number = 0;
@@ -3942,9 +3943,9 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
return;
}
const char *cmd;
bool executable = true;
char **argv = tv_to_argv(&argvars[0], NULL, &executable);
dict_T *env = NULL;
char **argv = tv_to_argv(&argvars[0], &cmd, &executable);
if (!argv) {
rettv->vval.v_number = executable ? 0 : -1;
return; // Did error message in tv_to_argv.
@@ -3961,6 +3962,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
bool detach = false;
bool rpc = false;
bool pty = false;
bool term = false;
bool clear_env = false;
bool overlapped = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
@@ -3974,7 +3976,8 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
detach = tv_dict_get_number(job_opts, "detach") != 0;
rpc = tv_dict_get_number(job_opts, "rpc") != 0;
pty = tv_dict_get_number(job_opts, "pty") != 0;
term = tv_dict_get_number(job_opts, "term") != 0;
pty = term || tv_dict_get_number(job_opts, "pty") != 0;
clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
overlapped = tv_dict_get_number(job_opts, "overlapped") != 0;
@@ -3989,6 +3992,14 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
}
}
dictitem_T *const job_term = tv_dict_find(job_opts, S_LEN("term"));
if (job_term && VAR_BOOL != job_term->di_tv.v_type) {
// Restrict "term" field to boolean, in case we want to allow buffer numbers in the future.
semsg(_(e_invarg2), "'term' must be Boolean");
shell_free_argv(argv);
return;
}
if (pty && rpc) {
semsg(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set");
shell_free_argv(argv);
@@ -4032,24 +4043,87 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
uint16_t height = 0;
char *term_name = NULL;
if (pty) {
width = (uint16_t)tv_dict_get_number(job_opts, "width");
height = (uint16_t)tv_dict_get_number(job_opts, "height");
// Legacy method, before env option existed, to specify $TERM. No longer
// documented, but still usable to avoid breaking scripts.
term_name = tv_dict_get_string(job_opts, "TERM", false);
if (!term_name) {
term_name = "ansi";
if (term) {
if (text_locked()) {
text_locked_msg();
shell_free_argv(argv);
return;
}
if (curbuf->b_changed) {
emsg(_("jobstart(...,{term=true}) requires unmodified buffer"));
shell_free_argv(argv);
return;
}
assert(!rpc);
term_name = "xterm-256color";
cwd = cwd ? cwd : ".";
overlapped = false;
detach = false;
stdin_mode = kChannelStdinPipe;
width = (uint16_t)MAX(0, curwin->w_width_inner - win_col_off(curwin));
height = (uint16_t)curwin->w_height_inner;
}
env = create_environment(job_env, clear_env, pty, term_name);
if (pty) {
width = width ? width : (uint16_t)tv_dict_get_number(job_opts, "width");
height = height ? height : (uint16_t)tv_dict_get_number(job_opts, "height");
// Deprecated TERM field is from before `env` option existed.
term_name = term_name ? term_name : tv_dict_get_string(job_opts, "TERM", false);
term_name = term_name ? term_name : "ansi";
}
dict_T *env = create_environment(job_env, clear_env, pty, term_name);
Channel *chan = channel_job_start(argv, NULL, on_stdout, on_stderr, on_exit, pty,
rpc, overlapped, detach, stdin_mode, cwd,
width, height, env, &rettv->vval.v_number);
if (chan) {
if (!chan) {
return;
} else if (!term) {
channel_create_event(chan, NULL);
} else {
if (rettv->vval.v_number <= 0) {
return;
}
int pid = chan->stream.pty.proc.pid;
// "./…" => "/home/foo/…"
vim_FullName(cwd, NameBuff, sizeof(NameBuff), false);
// "/home/foo/…" => "~/…"
size_t len = home_replace(NULL, NameBuff, IObuff, sizeof(IObuff), true);
// Trim slash.
if (len != 1 && (IObuff[len - 1] == '\\' || IObuff[len - 1] == '/')) {
IObuff[len - 1] = NUL;
}
if (len == 1 && IObuff[0] == '/') {
// Avoid ambiguity in the URI when CWD is root directory.
IObuff[1] = '.';
IObuff[2] = NUL;
}
// Terminal URI: "term://$CWD//$PID:$CMD"
snprintf(NameBuff, sizeof(NameBuff), "term://%s//%d:%s", IObuff, pid, cmd);
// Buffer has no terminal associated yet; unset 'swapfile' to ensure no swapfile is created.
curbuf->b_p_swf = false;
apply_autocmds(EVENT_BUFFILEPRE, NULL, NULL, false, curbuf);
setfname(curbuf, NameBuff, NULL, true);
apply_autocmds(EVENT_BUFFILEPOST, NULL, NULL, false, curbuf);
Error err = ERROR_INIT;
// Set (deprecated) buffer-local vars (prefer 'channel' buffer-local option).
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_id"),
INTEGER_OBJ((Integer)chan->id), false, false, NULL, &err);
api_clear_error(&err);
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_pid"),
INTEGER_OBJ(pid), false, false, NULL, &err);
api_clear_error(&err);
channel_incref(chan);
channel_terminal_open(curbuf, chan);
channel_create_event(chan, NULL);
channel_decref(chan);
}
}
@@ -8133,134 +8207,6 @@ static void f_taglist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
(char *)tag_pattern, (char *)fname);
}
/// "termopen(cmd[, cwd])" function
static void f_termopen(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (check_secure()) {
return;
}
if (text_locked()) {
text_locked_msg();
return;
}
if (curbuf->b_changed) {
emsg(_("Can only call this function in an unmodified buffer"));
return;
}
const char *cmd;
bool executable = true;
char **argv = tv_to_argv(&argvars[0], &cmd, &executable);
if (!argv) {
rettv->vval.v_number = executable ? 0 : -1;
return; // Did error message in tv_to_argv.
}
if (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN) {
// Wrong argument type
semsg(_(e_invarg2), "expected dictionary");
shell_free_argv(argv);
return;
}
CallbackReader on_stdout = CALLBACK_READER_INIT;
CallbackReader on_stderr = CALLBACK_READER_INIT;
Callback on_exit = CALLBACK_NONE;
dict_T *job_opts = NULL;
const char *cwd = ".";
dict_T *env = NULL;
const bool pty = true;
bool clear_env = false;
dictitem_T *job_env = NULL;
if (argvars[1].v_type == VAR_DICT) {
job_opts = argvars[1].vval.v_dict;
const char *const new_cwd = tv_dict_get_string(job_opts, "cwd", false);
if (new_cwd && *new_cwd != NUL) {
cwd = new_cwd;
// The new cwd must be a directory.
if (!os_isdir(cwd)) {
semsg(_(e_invarg2), "expected valid directory");
shell_free_argv(argv);
return;
}
}
job_env = tv_dict_find(job_opts, S_LEN("env"));
if (job_env && job_env->di_tv.v_type != VAR_DICT) {
semsg(_(e_invarg2), "env");
shell_free_argv(argv);
return;
}
clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
if (!common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit)) {
shell_free_argv(argv);
return;
}
}
env = create_environment(job_env, clear_env, pty, "xterm-256color");
const bool rpc = false;
const bool overlapped = false;
const bool detach = false;
ChannelStdinMode stdin_mode = kChannelStdinPipe;
uint16_t term_width = (uint16_t)MAX(0, curwin->w_width_inner - win_col_off(curwin));
Channel *chan = channel_job_start(argv, NULL, on_stdout, on_stderr, on_exit,
pty, rpc, overlapped, detach, stdin_mode,
cwd, term_width, (uint16_t)curwin->w_height_inner,
env, &rettv->vval.v_number);
if (rettv->vval.v_number <= 0) {
return;
}
int pid = chan->stream.pty.proc.pid;
// "./…" => "/home/foo/…"
vim_FullName(cwd, NameBuff, sizeof(NameBuff), false);
// "/home/foo/…" => "~/…"
size_t len = home_replace(NULL, NameBuff, IObuff, sizeof(IObuff), true);
// Trim slash.
if (len != 1 && (IObuff[len - 1] == '\\' || IObuff[len - 1] == '/')) {
IObuff[len - 1] = NUL;
}
if (len == 1 && IObuff[0] == '/') {
// Avoid ambiguity in the URI when CWD is root directory.
IObuff[1] = '.';
IObuff[2] = NUL;
}
// Terminal URI: "term://$CWD//$PID:$CMD"
snprintf(NameBuff, sizeof(NameBuff), "term://%s//%d:%s",
IObuff, pid, cmd);
// at this point the buffer has no terminal instance associated yet, so unset
// the 'swapfile' option to ensure no swap file will be created
curbuf->b_p_swf = false;
apply_autocmds(EVENT_BUFFILEPRE, NULL, NULL, false, curbuf);
setfname(curbuf, NameBuff, NULL, true);
apply_autocmds(EVENT_BUFFILEPOST, NULL, NULL, false, curbuf);
// Save the job id and pid in b:terminal_job_{id,pid}
Error err = ERROR_INIT;
// deprecated: use 'channel' buffer option
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_id"),
INTEGER_OBJ((Integer)chan->id), false, false, NULL, &err);
api_clear_error(&err);
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_pid"),
INTEGER_OBJ(pid), false, false, NULL, &err);
api_clear_error(&err);
channel_incref(chan);
channel_terminal_open(curbuf, chan);
channel_create_event(chan, NULL);
channel_decref(chan);
}
/// "timer_info([timer])" function
static void f_timer_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{

View File

@@ -7775,7 +7775,7 @@ static void ex_terminal(exarg_T *eap)
if (*eap->arg != NUL) { // Run {cmd} in 'shell'.
char *name = vim_strsave_escaped(eap->arg, "\"\\");
snprintf(ex_cmd + len, sizeof(ex_cmd) - len,
" | call termopen(\"%s\")", name);
" | call jobstart(\"%s\",{'term':v:true})", name);
xfree(name);
} else { // No {cmd}: run the job with tokenized 'shell'.
if (*p_sh == NUL) {
@@ -7798,7 +7798,7 @@ static void ex_terminal(exarg_T *eap)
shell_free_argv(argv);
snprintf(ex_cmd + len, sizeof(ex_cmd) - len,
" | call termopen([%s])", shell_argv + 1);
" | call jobstart([%s], {'term':v:true})", shell_argv + 1);
}
do_cmdline_cmd(ex_cmd);

View File

@@ -19,6 +19,7 @@ hashpipe:write([[
#include "nvim/digraph.h"
#include "nvim/eval.h"
#include "nvim/eval/buffer.h"
#include "nvim/eval/deprecated.h"
#include "nvim/eval/fs.h"
#include "nvim/eval/funcs.h"
#include "nvim/eval/typval.h"

View File

@@ -357,7 +357,7 @@ static void term_output_callback(const char *s, size_t len, void *user_data)
/// Initializes terminal properties, and triggers TermOpen.
///
/// The PTY process (TerminalOptions.data) was already started by termopen(),
/// The PTY process (TerminalOptions.data) was already started by jobstart(),
/// via ex_terminal() or the term:// BufReadCmd.
///
/// @param buf Buffer used for presentation of the terminal.