diff options
author | Thiago de Arruda <tpadilha84@gmail.com> | 2015-03-08 08:58:31 -0300 |
---|---|---|
committer | Thiago de Arruda <tpadilha84@gmail.com> | 2015-03-25 18:57:35 -0300 |
commit | cdedd89d228a016a4e433968702e9e3ce5165e7d (patch) | |
tree | 2a98eb4008a6033621c293f84db29cb3127f9740 | |
parent | 6f471fa4fc24e72e1ac75c1579970414b168381e (diff) | |
download | rneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.tar.gz rneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.tar.bz2 rneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.zip |
terminal: New module that implements a terminal emulator
This commit integrates libvterm with Neovim and implements a terminal emulator
with nvim buffers as the display mechanism. Terminal buffers can be created
using any of the following methods:
- Opening a file with name following the "term://[${cwd}//[${pid}:]]${cmd}"
URI pattern where:
- cwd is the working directory of the process
- pid is the process id. This is just for use in session files where a pid
would have been assigned to the saved buffer title.
- cmd is the command to run
- Invoking the `:terminal` ex command
- Invoking the `termopen` function which returns a job id for automating the
terminal window.
Some extra changes were also implemented to adapt with terminal buffers. Here's
an overview:
- The `main` function now sets a BufReadCmd autocmd to intercept the term:// URI
and spawn the terminal buffer instead of reading the file.
- terminal buffers behave as if the following local buffer options were set:
- `nomodifiable`
- `swapfile`
- `undolevels=-1`
- `bufhidden=hide`
- All commands that delete buffers(`:bun`, `:bd` and `:bw`) behave the same for
terminal buffers, but only work when bang is passed(eg: `:bwipeout!`)
- A new "terminal" mode was added. A consequence is that a new set of mapping
commands were implemented with the "t" prefix(tmap, tunmap, tnoremap...)
- The `edit` function(which enters insert mode) will actually enter terminal
mode if the current buffer is a terminal
- The `put` operator was adapted to send data to the terminal instead of
modifying the buffer directly.
- A window being resized will also trigger a terminal resize if the window
displays the terminal.
-rw-r--r-- | src/nvim/buffer.c | 50 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 7 | ||||
-rw-r--r-- | src/nvim/edit.c | 6 | ||||
-rw-r--r-- | src/nvim/eval.c | 211 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 2 | ||||
-rw-r--r-- | src/nvim/ex_cmds.lua | 25 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 28 | ||||
-rw-r--r-- | src/nvim/fileio.c | 10 | ||||
-rw-r--r-- | src/nvim/fileio.h | 1 | ||||
-rw-r--r-- | src/nvim/fold.c | 5 | ||||
-rw-r--r-- | src/nvim/getchar.c | 6 | ||||
-rw-r--r-- | src/nvim/macros.h | 2 | ||||
-rw-r--r-- | src/nvim/main.c | 11 | ||||
-rw-r--r-- | src/nvim/memline.c | 8 | ||||
-rw-r--r-- | src/nvim/ops.c | 27 | ||||
-rw-r--r-- | src/nvim/option.c | 8 | ||||
-rw-r--r-- | src/nvim/os/event.c | 8 | ||||
-rw-r--r-- | src/nvim/quickfix.c | 15 | ||||
-rw-r--r-- | src/nvim/screen.c | 36 | ||||
-rw-r--r-- | src/nvim/syntax.c | 5 | ||||
-rw-r--r-- | src/nvim/terminal.c | 1129 | ||||
-rw-r--r-- | src/nvim/terminal.h | 33 | ||||
-rw-r--r-- | src/nvim/undo.c | 3 | ||||
-rw-r--r-- | src/nvim/vim.h | 5 | ||||
-rw-r--r-- | src/nvim/window.c | 28 |
25 files changed, 1577 insertions, 92 deletions
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 03adba97cd..dd20d61f75 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -68,6 +68,7 @@ #include "nvim/spell.h" #include "nvim/strings.h" #include "nvim/syntax.h" +#include "nvim/terminal.h" #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/version.h" @@ -307,20 +308,28 @@ close_buffer ( bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE); bool wipe_buf = (action == DOBUF_WIPE); - /* - * Force unloading or deleting when 'bufhidden' says so. - * The caller must take care of NOT deleting/freeing when 'bufhidden' is - * "hide" (otherwise we could never free or delete a buffer). - */ - if (buf->b_p_bh[0] == 'd') { /* 'bufhidden' == "delete" */ - del_buf = true; + // Force unloading or deleting when 'bufhidden' says so, but not for terminal + // buffers. + // The caller must take care of NOT deleting/freeing when 'bufhidden' is + // "hide" (otherwise we could never free or delete a buffer). + if (!buf->terminal) { + if (buf->b_p_bh[0] == 'd') { // 'bufhidden' == "delete" + del_buf = true; + unload_buf = true; + } else if (buf->b_p_bh[0] == 'w') { // 'bufhidden' == "wipe" + del_buf = true; + unload_buf = true; + wipe_buf = true; + } else if (buf->b_p_bh[0] == 'u') // 'bufhidden' == "unload" + unload_buf = true; + } + + if (buf->terminal && (unload_buf || del_buf || wipe_buf)) { + // terminal buffers can only be wiped unload_buf = true; - } else if (buf->b_p_bh[0] == 'w') { /* 'bufhidden' == "wipe" */ del_buf = true; - unload_buf = true; wipe_buf = true; - } else if (buf->b_p_bh[0] == 'u') /* 'bufhidden' == "unload" */ - unload_buf = true; + } if (win_valid(win)) { /* Set b_last_cursor when closing the last window for the buffer. @@ -383,6 +392,10 @@ close_buffer ( if (buf->b_nwindows > 0 || !unload_buf) return; + if (buf->terminal) { + terminal_close(buf->terminal, NULL); + } + /* Always remove the buffer when there is no file name. */ if (buf->b_ffname == NULL) del_buf = TRUE; @@ -925,8 +938,8 @@ do_buffer ( if (action != DOBUF_WIPE && buf->b_ml.ml_mfp == NULL && !buf->b_p_bl) return FAIL; - if (!forceit && bufIsChanged(buf)) { - if ((p_confirm || cmdmod.confirm) && p_write) { + if (!forceit && (buf->terminal || bufIsChanged(buf))) { + if ((p_confirm || cmdmod.confirm) && p_write && !buf->terminal) { dialog_changed(buf, FALSE); if (!buf_valid(buf)) /* Autocommand deleted buffer, oops! It's not changed @@ -937,9 +950,14 @@ do_buffer ( if (bufIsChanged(buf)) return FAIL; } else { - EMSGN(_("E89: No write since last change for buffer %" PRId64 - " (add ! to override)"), - buf->b_fnum); + if (buf->terminal) { + EMSG2(_("E89: %s will be killed(add ! to override)"), + (char *)buf->b_fname); + } else { + EMSGN(_("E89: No write since last change for buffer %" PRId64 + " (add ! to override)"), + buf->b_fnum); + } return FAIL; } } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 3b8b171bd4..35fa3978b6 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -27,7 +27,7 @@ // for String #include "nvim/api/private/defs.h" -#define MODIFIABLE(buf) (buf->b_p_ma) +#define MODIFIABLE(buf) (!buf->terminal && buf->b_p_ma) /* * Flags for w_valid. @@ -103,6 +103,9 @@ typedef struct file_buffer buf_T; /* forward declaration */ // for FileID #include "nvim/os/fs_defs.h" +// for Terminal +#include "nvim/terminal.h" + /* * The taggy struct is used to store the information about a :tag command. */ @@ -751,6 +754,8 @@ struct file_buffer { * may use a different synblock_T. */ signlist_T *b_signlist; /* list of signs to draw */ + + Terminal *terminal; // Terminal instance associated with the buffer }; /* diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 516278f34d..8b2ac1943f 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -56,6 +56,7 @@ #include "nvim/tag.h" #include "nvim/ui.h" #include "nvim/mouse.h" +#include "nvim/terminal.h" #include "nvim/undo.h" #include "nvim/window.h" #include "nvim/os/event.h" @@ -244,6 +245,11 @@ edit ( long count ) { + if (curbuf->terminal) { + terminal_enter(curbuf->terminal, true); + return false; + } + int c = 0; char_u *ptr; int lastc; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 08ad33e60e..6d8f47ee9c 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -1,4 +1,5 @@ /* + * * VIM - Vi IMproved by Bram Moolenaar * * Do ":help uganda" in Vim to read copying and usage conditions. @@ -78,6 +79,7 @@ #include "nvim/tempfile.h" #include "nvim/ui.h" #include "nvim/mouse.h" +#include "nvim/terminal.h" #include "nvim/undo.h" #include "nvim/version.h" #include "nvim/window.h" @@ -440,6 +442,15 @@ static struct vimvar { static dictitem_T vimvars_var; /* variable used for v: */ #define vimvarht vimvardict.dv_hashtab +typedef struct { + Job *job; + Terminal *term; + bool exited; + int refcount; + char *autocmd_file; +} TerminalJobData; + + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "eval.c.generated.h" #endif @@ -447,7 +458,6 @@ static dictitem_T vimvars_var; /* variable used for v: */ #define FNE_INCL_BR 1 /* find_name_end(): include [] in name */ #define FNE_CHECK_START 2 /* find_name_end(): check name starts with valid character */ - // Memory pool for reusing JobEvent structures typedef struct { int id; @@ -6600,6 +6610,7 @@ static struct fst { {"tan", 1, 1, f_tan}, {"tanh", 1, 1, f_tanh}, {"tempname", 0, 0, f_tempname}, + {"termopen", 1, 2, f_termopen}, {"test", 1, 1, f_test}, {"tolower", 1, 1, f_tolower}, {"toupper", 1, 1, f_toupper}, @@ -10750,12 +10761,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv) // The last item of argv must be NULL argv[i] = NULL; - JobOptions opts = JOB_OPTIONS_INIT; - opts.argv = argv; - opts.data = xstrdup((char *)argvars[0].vval.v_string); - opts.stdout_cb = on_job_stdout; - opts.stderr_cb = on_job_stderr; - opts.exit_cb = on_job_exit; + JobOptions opts = common_job_options(argv, (char *)argvars[0].vval.v_string); if (args && argvars[3].v_type == VAR_DICT) { dict_T *job_opts = argvars[3].vval.v_dict; @@ -10774,15 +10780,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv) } } - job_start(opts, &rettv->vval.v_number); - - if (rettv->vval.v_number <= 0) { - if (rettv->vval.v_number == 0) { - EMSG(_(e_jobtblfull)); - } else { - EMSG(_(e_jobexe)); - } - } + common_job_start(opts, rettv); } // "jobstop()" function @@ -14870,6 +14868,72 @@ static void f_tempname(typval_T *argvars, typval_T *rettv) rettv->vval.v_string = vim_tempname(); } +// "termopen(cmd[, cwd])" function +static void f_termopen(typval_T *argvars, typval_T *rettv) +{ + if (check_restricted() || check_secure()) { + return; + } + + if (curbuf->b_changed) { + EMSG(_("Can only call this function in an unmodified buffer")); + return; + } + + if (argvars[0].v_type != VAR_STRING + || (argvars[1].v_type != VAR_STRING + && argvars[1].v_type != VAR_UNKNOWN)) { + // Wrong argument types + EMSG(_(e_invarg)); + return; + } + + char **argv = shell_build_argv((char *)argvars[0].vval.v_string, NULL); + JobOptions opts = common_job_options(argv, NULL); + opts.pty = true; + opts.width = curwin->w_width; + opts.height = curwin->w_height; + opts.term_name = xstrdup("xterm-256color"); + Job *job = common_job_start(opts, rettv); + if (!job) { + return; + } + TerminalJobData *data = opts.data; + TerminalOptions topts = TERMINAL_OPTIONS_INIT; + topts.data = data; + topts.width = curwin->w_width; + topts.height = curwin->w_height; + topts.write_cb = term_write; + topts.resize_cb = term_resize; + topts.close_cb = term_close; + + char *cwd = "."; + if (argvars[1].v_type == VAR_STRING + && os_isdir(argvars[1].vval.v_string)) { + cwd = (char *)argvars[1].vval.v_string; + } + int pid = job_pid(job); + char buf[1024]; + // format the title with the pid to conform with the term:// URI + snprintf(buf, sizeof(buf), "term://%s//%d:%s", cwd, pid, + (char *)argvars[0].vval.v_string); + // 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; + (void)setfname(curbuf, (uint8_t *)buf, NULL, true); + data->autocmd_file = xstrdup(buf); + // Save the job id and pid in b:terminal_job_{id,pid} + Error err; + dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_id"), + INTEGER_OBJ(rettv->vval.v_number), &err); + dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_pid"), + INTEGER_OBJ(pid), &err); + + Terminal *term = terminal_open(topts); + data->term = term; + data->refcount++; +} + /* * "test(list)" function: Just checking the walls... */ @@ -19750,16 +19814,51 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags) return ret; } +static inline JobOptions common_job_options(char **argv, char *autocmd_file) +{ + TerminalJobData *data = xcalloc(1, sizeof(TerminalJobData)); + if (autocmd_file) { + data->autocmd_file = xstrdup(autocmd_file); + } + JobOptions opts = JOB_OPTIONS_INIT; + opts.argv = argv; + opts.data = data; + opts.stdout_cb = on_job_stdout; + opts.stderr_cb = on_job_stderr; + opts.exit_cb = on_job_exit; + return opts; +} + +static inline Job *common_job_start(JobOptions opts, typval_T *rettv) +{ + Job *job = job_start(opts, &rettv->vval.v_number); + TerminalJobData *data = opts.data; + data->refcount++; + + if (rettv->vval.v_number <= 0) { + if (rettv->vval.v_number == 0) { + EMSG(_(e_jobtblfull)); + free(data->autocmd_file); + } else { + EMSG(_(e_jobexe)); + free(opts.data); + } + return NULL; + } + data->job = job; + return job; +} + // JobActivity autocommands will execute vimscript code, so it must be executed // on Nvim main loop -static inline void push_job_event(Job *job, RStream *rstream, char *type) +static inline void push_job_event(Job *job, char *type, char *data, + size_t count) { JobEvent *event_data = kmp_alloc(JobEventPool, job_event_pool); event_data->received = NULL; - if (rstream) { + if (data) { event_data->received = list_alloc(); - char *ptr = rstream_read_ptr(rstream); - size_t count = rstream_pending(rstream); + char *ptr = data; size_t remaining = count; size_t off = 0; @@ -19780,10 +19879,10 @@ static inline void push_job_event(Job *job, RStream *rstream, char *type) off++; } list_append_string(event_data->received, (uint8_t *)ptr, off); - rbuffer_consumed(rstream_buffer(rstream), count); } + TerminalJobData *d = job_data(job); event_data->id = job_id(job); - event_data->name = job_data(job); + event_data->name = d->autocmd_file; event_data->type = type; event_push((Event) { .handler = on_job_event, @@ -19793,21 +19892,75 @@ static inline void push_job_event(Job *job, RStream *rstream, char *type) static void on_job_stdout(RStream *rstream, void *data, bool eof) { - if (!eof) { - push_job_event(data, rstream, "stdout"); - } + on_job_output(rstream, data, eof, "stdout"); } static void on_job_stderr(RStream *rstream, void *data, bool eof) { - if (!eof) { - push_job_event(data, rstream, "stderr"); + on_job_output(rstream, data, eof, "stderr"); +} + +static void on_job_output(RStream *rstream, Job *job, bool eof, char *type) +{ + if (eof) { + return; } + + TerminalJobData *data = job_data(job); + char *ptr = rstream_read_ptr(rstream); + size_t len = rstream_pending(rstream); + + // The order here matters, the terminal must receive the data first because + // push_job_event will modify the read buffer(convert NULs into NLs) + if (data->term) { + terminal_receive(data->term, ptr, len); + } + + push_job_event(job, type, ptr, len); + rbuffer_consumed(rstream_buffer(rstream), len); +} + +static void on_job_exit(Job *job, void *d) +{ + TerminalJobData *data = d; + push_job_event(job, "exit", NULL, 0); + + if (data->term && !data->exited) { + data->exited = true; + terminal_close(data->term, + _("\r\n[Program exited, press any key to close]")); + } + term_job_data_decref(data); +} + +static void term_write(char *buf, size_t size, void *data) +{ + Job *job = ((TerminalJobData *)data)->job; + WBuffer *wbuf = wstream_new_buffer(xmemdup(buf, size), size, 1, free); + job_write(job, wbuf); +} + +static void term_resize(uint16_t width, uint16_t height, void *data) +{ + job_resize(((TerminalJobData *)data)->job, width, height); } -static void on_job_exit(Job *job, void *data) +static void term_close(void *d) { - push_job_event(job, NULL, "exit"); + TerminalJobData *data = d; + if (!data->exited) { + data->exited = true; + job_stop(data->job); + } + terminal_destroy(data->term); + term_job_data_decref(d); +} + +static void term_job_data_decref(TerminalJobData *data) +{ + if (!(--data->refcount)) { + free(data); + } } static void on_job_event(Event event) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index d85664d1ba..81c7ecab5f 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2724,7 +2724,7 @@ do_ecmd ( /* close the link to the current buffer */ u_sync(FALSE); close_buffer(oldwin, curbuf, - (flags & ECMD_HIDE) ? 0 : DOBUF_UNLOAD, FALSE); + (flags & ECMD_HIDE) || curbuf->terminal ? 0 : DOBUF_UNLOAD, FALSE); /* Autocommands may open a new window and leave oldwin open * which leads to crashes since the above call sets diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 0897006555..e1951e88f8 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2236,6 +2236,11 @@ return { func='ex_tearoff', }, { + command='terminal', + flags=bit.bor(BANG, FILES, CMDWIN), + func='ex_terminal', + }, + { command='tfirst', flags=bit.bor(RANGE, NOTADR, BANG, TRLBAR, ZEROR), func='ex_tag', @@ -2256,6 +2261,16 @@ return { func='ex_tag', }, { + command='tmap', + flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN), + func='ex_map', + }, + { + command='tmapclear', + flags=bit.bor(EXTRA, TRLBAR, CMDWIN), + func='ex_mapclear', + }, + { command='tmenu', flags=bit.bor(RANGE, NOTADR, ZEROR, EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN), func='ex_menu', @@ -2266,6 +2281,11 @@ return { func='ex_tag', }, { + command='tnoremap', + flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN), + func='ex_map', + }, + { command='topleft', flags=bit.bor(NEEDARG, EXTRA, NOTRLCOM), func='ex_wrongmodifier', @@ -2291,6 +2311,11 @@ return { func='ex_tag', }, { + command='tunmap', + flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN), + func='ex_unmap', + }, + { command='tunmenu', flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN), func='ex_menu', diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index de04a3ea32..776ed844e9 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -62,6 +62,7 @@ #include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/tag.h" +#include "nvim/terminal.h" #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/version.h" @@ -71,6 +72,8 @@ #include "nvim/os/time.h" #include "nvim/ex_cmds_defs.h" #include "nvim/mouse.h" +#include "nvim/os/rstream.h" +#include "nvim/os/wstream.h" static int quitmore = 0; static int ex_pressedreturn = FALSE; @@ -1510,7 +1513,9 @@ static char_u * do_one_cmd(char_u **cmdlinep, errormsg = (char_u *)_(e_sandbox); goto doend; } - if (!MODIFIABLE(curbuf) && (ea.argt & MODIFY)) { + if (!MODIFIABLE(curbuf) && (ea.argt & MODIFY) + // allow :put in terminals + && (!curbuf->terminal || ea.cmdidx != CMD_put)) { /* Command not allowed in non-'modifiable' buffer */ errormsg = (char_u *)_(e_modifiable); goto doend; @@ -2610,7 +2615,7 @@ set_one_cmd_context ( xp->xp_context = EXPAND_FILES; /* For a shell command more chars need to be escaped. */ - if (usefilter || ea.cmdidx == CMD_bang) { + if (usefilter || ea.cmdidx == CMD_bang || ea.cmdidx == CMD_terminal) { #ifndef BACKSLASH_IN_FILENAME xp->xp_shell = TRUE; #endif @@ -5126,8 +5131,10 @@ static void ex_quit(exarg_T *eap) || (only_one_window() && check_changed_any(eap->forceit))) { not_exiting(); } else { - if (only_one_window()) /* quit last window */ + if (only_one_window()) { + // quit last window getout(0); + } /* close window; may free buffer */ win_close(curwin, !P_HID(curwin->w_buffer) || eap->forceit); } @@ -8060,7 +8067,9 @@ makeopens ( /* * Wipe out an empty unnamed buffer we started in. */ - if (put_line(fd, "if exists('s:wipebuf')") == FAIL) + if (put_line(fd, "if exists('s:wipebuf') " + "&& getbufvar(s:wipebuf, '&buftype') isnot# 'terminal'") + == FAIL) return FAIL; if (put_line(fd, " silent exe 'bwipe ' . s:wipebuf") == FAIL) return FAIL; @@ -8269,7 +8278,7 @@ put_view ( * Load the file. */ if (wp->w_buffer->b_ffname != NULL - && !bt_nofile(wp->w_buffer) + && (!bt_nofile(wp->w_buffer) || wp->w_buffer->terminal) ) { /* * Editing a file in this buffer: use ":edit file". @@ -8857,3 +8866,12 @@ static void ex_folddo(exarg_T *eap) global_exe(eap->arg); ml_clearmarked(); /* clear rest of the marks */ } + +static void ex_terminal(exarg_T *eap) +{ + char cmd[512]; + snprintf(cmd, sizeof(cmd), ":enew%s | call termopen('%s') | startinsert", + eap->forceit==TRUE ? "!" : "", + strcmp((char *)eap->arg, "") ? (char *)eap->arg : (char *)p_sh); + do_cmdline_cmd((uint8_t *)cmd); +} diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 799a6a2a50..29bcaec84a 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -4756,10 +4756,11 @@ buf_check_timestamp ( char_u *s; char *reason; - /* If there is no file name, the buffer is not loaded, 'buftype' is - * set, we are in the middle of a save or being called recursively: ignore - * this buffer. */ - if (buf->b_ffname == NULL + // If its a terminal, there is no file name, the buffer is not loaded, + // 'buftype' is set, we are in the middle of a save or being called + // recursively: ignore this buffer. + if (buf->terminal + || buf->b_ffname == NULL || buf->b_ml.ml_mfp == NULL || *buf->b_p_bt != NUL || buf->b_saving @@ -5208,6 +5209,7 @@ static struct event_name { {"TabNew", EVENT_TABNEW}, {"TabNewEntered", EVENT_TABNEWENTERED}, {"TermChanged", EVENT_TERMCHANGED}, + {"TermOpen", EVENT_TERMOPEN}, {"TermResponse", EVENT_TERMRESPONSE}, {"TextChanged", EVENT_TEXTCHANGED}, {"TextChangedI", EVENT_TEXTCHANGEDI}, diff --git a/src/nvim/fileio.h b/src/nvim/fileio.h index 7a9e2adae1..833a4fade6 100644 --- a/src/nvim/fileio.h +++ b/src/nvim/fileio.h @@ -100,6 +100,7 @@ typedef enum auto_event { EVENT_TABNEWENTERED, /* after entering a new tab */ EVENT_SHELLCMDPOST, /* after ":!cmd" */ EVENT_SHELLFILTERPOST, /* after ":1,2!cmd", ":w !cmd", ":r !cmd". */ + EVENT_TERMOPEN, // after opening a terminal buffer EVENT_TEXTCHANGED, /* text was modified */ EVENT_TEXTCHANGEDI, /* text was modified in Insert mode*/ EVENT_CMDUNDEFINED, ///< command undefined diff --git a/src/nvim/fold.c b/src/nvim/fold.c index 74444d4ab7..267c586543 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -137,7 +137,7 @@ void copyFoldingState(win_T *wp_from, win_T *wp_to) int hasAnyFolding(win_T *win) { /* very simple now, but can become more complex later */ - return win->w_p_fen + return !win->w_buffer->terminal && win->w_p_fen && (!foldmethodIsManual(win) || !GA_EMPTY(&win->w_folds)); } @@ -768,6 +768,9 @@ void clearFolding(win_T *win) void foldUpdate(win_T *wp, linenr_T top, linenr_T bot) { fold_T *fp; + if (wp->w_buffer->terminal) { + return; + } /* Mark all folds from top to bot as maybe-small. */ (void)foldFind(&curwin->w_folds, top, &fp); diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index bb251c102e..d901e99a2d 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -2538,6 +2538,7 @@ fix_input_buffer ( * for :xmap mode is VISUAL * for :smap mode is SELECTMODE * for :omap mode is OP_PENDING + * for :tmap mode is TERM_FOCUS * * for :abbr mode is INSERT + CMDLINE * for :iabbr mode is INSERT @@ -3056,6 +3057,8 @@ int get_map_mode(char_u **cmdp, int forceit) mode = SELECTMODE; /* :smap */ else if (modec == 'o') mode = OP_PENDING; /* :omap */ + else if (modec == 't') + mode = TERM_FOCUS; // :tmap else { --p; if (forceit) @@ -3923,6 +3926,9 @@ makemap ( case LANGMAP: c1 = 'l'; break; + case TERM_FOCUS: + c1 = 't'; + break; default: EMSG(_("E228: makemap: Illegal mode")); return FAIL; diff --git a/src/nvim/macros.h b/src/nvim/macros.h index 653f46fb44..60f6cd454f 100644 --- a/src/nvim/macros.h +++ b/src/nvim/macros.h @@ -154,4 +154,6 @@ /// zero in those cases (-Wdiv-by-zero in GCC). #define ARRAY_SIZE(arr) ((sizeof(arr)/sizeof((arr)[0])) / ((size_t)(!(sizeof(arr) % sizeof((arr)[0]))))) +#define RGB(r, g, b) ((r << 16) | (g << 8) | b) + #endif // NVIM_MACROS_H diff --git a/src/nvim/main.c b/src/nvim/main.c index 9f4bc22ae0..47bb2bc515 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -285,6 +285,17 @@ int main(int argc, char **argv) input_start_stdin(fd); } + // open terminals when opening files that start with term:// + do_cmdline_cmd((uint8_t *) + "autocmd BufReadCmd term://* " + ":call termopen( " + // Capture the command string + "matchstr(expand(\"<amatch>\"), " + "'\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " + // capture the working directory + "get(matchlist(expand(\"<amatch>\"), " + "'\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))"); + /* Execute --cmd arguments. */ exe_pre_commands(¶ms); diff --git a/src/nvim/memline.c b/src/nvim/memline.c index d6d7d3db1a..8b2ebfe554 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -278,10 +278,11 @@ int ml_open(buf_T *buf) /* * When 'updatecount' is non-zero swap file may be opened later. */ - if (p_uc && buf->b_p_swf) + if (!buf->terminal && p_uc && buf->b_p_swf) { buf->b_may_swap = true; - else + } else { buf->b_may_swap = false; + } /* * Open the memfile. No swap file is created yet. @@ -488,7 +489,8 @@ void ml_open_file(buf_T *buf) char_u *dirp; mfp = buf->b_ml.ml_mfp; - if (mfp == NULL || mfp->mf_fd >= 0 || !buf->b_p_swf || cmdmod.noswapfile) { + if (mfp == NULL || mfp->mf_fd >= 0 || !buf->b_p_swf || cmdmod.noswapfile + || buf->terminal) { return; /* nothing to do */ } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 50710cd78a..2714798368 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -45,6 +45,7 @@ #include "nvim/screen.h" #include "nvim/search.h" #include "nvim/strings.h" +#include "nvim/terminal.h" #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/window.h" @@ -2643,11 +2644,13 @@ do_put ( return; } - /* Autocommands may be executed when saving lines for undo, which may make - * y_array invalid. Start undo now to avoid that. */ - if (u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1) == FAIL) { - ELOG(_("Failed to save undo information")); - return; + if (!curbuf->terminal) { + // Autocommands may be executed when saving lines for undo, which may make + // y_array invalid. Start undo now to avoid that. + if (u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1) == FAIL) { + ELOG(_("Failed to save undo information")); + return; + } } if (insert_string != NULL) { @@ -2692,6 +2695,20 @@ do_put ( y_array = y_current->y_array; } + if (curbuf->terminal) { + for (int i = 0; i < count; i++) { + // feed the lines to the terminal + for (int j = 0; j < y_size; j++) { + if (j) { + // terminate the previous line + terminal_send(curbuf->terminal, "\n", 1); + } + terminal_send(curbuf->terminal, (char *)y_array[j], STRLEN(y_array[j])); + } + } + return; + } + if (y_type == MLINE) { if (flags & PUT_LINE_SPLIT) { /* "p" or "P" in Visual mode: split the lines to put the text in diff --git a/src/nvim/option.c b/src/nvim/option.c index 19202030c6..3f12709521 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -1746,7 +1746,7 @@ 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", NULL}; +{"nofile", "nowrite", "quickfix", "help", "acwrite", "terminal", NULL}; static char *(p_bufhidden_values[]) = {"hide", "unload", "delete", "wipe", NULL}; static char *(p_bs_values[]) = {"indent", "eol", "start", NULL}; static char *(p_fdm_values[]) = {"manual", "expr", "marker", "indent", "syntax", @@ -4097,9 +4097,11 @@ did_set_string_option ( } /* When 'buftype' is set, check for valid value. */ else if (gvarp == &p_bt) { - if (check_opt_strings(curbuf->b_p_bt, p_buftype_values, FALSE) != OK) + if ((curbuf->terminal && curbuf->b_p_bt[0] != 't') + || (!curbuf->terminal && curbuf->b_p_bt[0] == 't') + || check_opt_strings(curbuf->b_p_bt, p_buftype_values, FALSE) != OK) { errmsg = e_invarg; - else { + } else { if (curwin->w_status_height) { curwin->w_redr_status = TRUE; redraw_later(VALID); diff --git a/src/nvim/os/event.c b/src/nvim/os/event.c index 92a5205eaf..fd9ff5b230 100644 --- a/src/nvim/os/event.c +++ b/src/nvim/os/event.c @@ -20,6 +20,7 @@ #include "nvim/misc2.h" #include "nvim/ui.h" #include "nvim/screen.h" +#include "nvim/terminal.h" #include "nvim/lib/klist.h" @@ -63,6 +64,7 @@ void event_init(void) // finish mspgack-rpc initialization channel_init(); server_init(); + terminal_init(); } void event_teardown(void) @@ -83,6 +85,7 @@ void event_teardown(void) job_teardown(); server_teardown(); signal_teardown(); + terminal_teardown(); // this last `uv_run` will return after all handles are stopped, it will // also take care of finishing any uv_close calls made by other *_teardown // functions. @@ -169,11 +172,6 @@ void event_push(Event event, bool deferred) void event_process(void) { process_events_from(deferred_events); - - if (must_redraw) { - update_screen(0); - ui_flush(); - } } static void process_events_from(klist_t(Event) *queue) diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 39117b262a..8e55cced78 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -2377,7 +2377,6 @@ static void qf_fill_buffer(qf_info_T *qi) KeyTyped = old_KeyTyped; } - /* * Return TRUE if "buf" is the quickfix buffer. */ @@ -2386,22 +2385,18 @@ int bt_quickfix(buf_T *buf) return buf != NULL && buf->b_p_bt[0] == 'q'; } -/* - * Return TRUE if "buf" is a "nofile" or "acwrite" buffer. - * This means the buffer name is not a file name. - */ +// Return TRUE if "buf" is a "nofile", "acwrite" or "terminal" buffer. +// This means the buffer name is not a file name. int bt_nofile(buf_T *buf) { return buf != NULL && ((buf->b_p_bt[0] == 'n' && buf->b_p_bt[2] == 'f') - || buf->b_p_bt[0] == 'a'); + || buf->b_p_bt[0] == 'a' || buf->terminal); } -/* - * Return TRUE if "buf" is a "nowrite" or "nofile" buffer. - */ +// Return TRUE if "buf" is a "nowrite", "nofile" or "terminal" buffer. int bt_dontwrite(buf_T *buf) { - return buf != NULL && buf->b_p_bt[0] == 'n'; + return buf != NULL && (buf->b_p_bt[0] == 'n' || buf->terminal); } int bt_dontwrite_msg(buf_T *buf) diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 04a092c4f7..c32603afb0 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -131,6 +131,7 @@ #include "nvim/spell.h" #include "nvim/strings.h" #include "nvim/syntax.h" +#include "nvim/terminal.h" #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/version.h" @@ -2210,7 +2211,7 @@ win_line ( } /* Check for columns to display for 'colorcolumn'. */ - color_cols = wp->w_p_cc_cols; + color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols; if (color_cols != NULL) draw_color_col = advance_color_col(VCOL_HLC, &color_cols); @@ -2592,6 +2593,13 @@ win_line ( off += col; } + // wont highlight after 1024 columns + int term_attrs[1024] = {0}; + if (wp->w_buffer->terminal) { + terminal_get_line_attributes(wp->w_buffer->terminal, wp, lnum, term_attrs); + extra_check = true; + } + /* * Repeat for the whole displayed line. */ @@ -3213,6 +3221,8 @@ win_line ( syntax_flags = 0; else syntax_flags = get_syntax_info(&syntax_seqnr); + } else if (!attr_pri) { + char_attr = 0; } /* Check spelling (unless at the end of the line). @@ -3290,6 +3300,11 @@ win_line ( else char_attr = hl_combine_attr(spell_attr, char_attr); } + + if (wp->w_buffer->terminal) { + char_attr = hl_combine_attr(char_attr, term_attrs[vcol]); + } + /* * Found last space before word: check for line break. */ @@ -3787,6 +3802,18 @@ win_line ( } } + if (wp->w_buffer->terminal) { + // terminal buffers may need to highlight beyond the end of the + // logical line + while (col < wp->w_width) { + ScreenLines[off] = ' '; + if (enc_utf8) { + ScreenLinesUC[off] = 0; + } + ScreenAttrs[off++] = term_attrs[vcol++]; + col++; + } + } SCREEN_LINE(screen_row, wp->w_wincol, col, wp->w_width, wp->w_p_rl); row++; @@ -6536,7 +6563,8 @@ int showmode(void) int sub_attr; do_mode = ((p_smd && msg_silent == 0) - && ((State & INSERT) + && ((State & TERM_FOCUS) + || (State & INSERT) || restart_edit || VIsual_active )); @@ -6591,7 +6619,9 @@ int showmode(void) } } } else { - if (State & VREPLACE_FLAG) + if (State & TERM_FOCUS) { + MSG_PUTS_ATTR(_(" TERMINAL"), attr); + } else if (State & VREPLACE_FLAG) MSG_PUTS_ATTR(_(" VREPLACE"), attr); else if (State & REPLACE_FLAG) MSG_PUTS_ATTR(_(" REPLACE"), attr); diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index c339468233..20bfbc8db4 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -40,10 +40,12 @@ #include "nvim/option.h" #include "nvim/os_unix.h" #include "nvim/path.h" +#include "nvim/macros.h" #include "nvim/regexp.h" #include "nvim/screen.h" #include "nvim/strings.h" #include "nvim/syntax_defs.h" +#include "nvim/terminal.h" #include "nvim/ui.h" #include "nvim/os/os.h" #include "nvim/os/time.h" @@ -6636,7 +6638,7 @@ static garray_T attr_table = GA_EMPTY_INIT_VALUE; * if the combination is new. * Return 0 for error. */ -static int get_attr_entry(attrentry_T *aep) +int get_attr_entry(attrentry_T *aep) { garray_T *table = &attr_table; attrentry_T *taep; @@ -7424,7 +7426,6 @@ char_u *get_highlight_name(expand_T *xp, int idx) return HL_TABLE()[idx].sg_name; } -#define RGB(r, g, b) ((r << 16) | (g << 8) | b) color_name_table_T color_name_table[] = { // Color names taken from // http://www.rapidtables.com/web/color/RGB_Color.htm diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c new file mode 100644 index 0000000000..87b2d8ff99 --- /dev/null +++ b/src/nvim/terminal.c @@ -0,0 +1,1129 @@ +// VT220/xterm-like terminal emulator implementation for Neovim. Powered by +// libvterm(http://www.leonerd.org.uk/code/libvterm/). +// +// libvterm is a pure C99 terminal emulation library with abstract input and +// display. This means that the library needs to read data from the master fd +// and feed VTerm instances, which will invoke user callbacks with screen +// update instructions that must be mirrored to the real display. +// +// Keys are pressed in VTerm instances by calling +// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that +// must be fed back to the master fd. +// +// This implementation uses Neovim buffers as the display mechanism for both +// the visible screen and the scrollback buffer. When focused, the window +// "pins" to the bottom of the buffer and mirrors libvterm screen state. +// +// When a line becomes invisible due to a decrease in screen height or because +// a line was pushed up during normal terminal output, we store the line +// information in the scrollback buffer, which is mirrored in the Neovim buffer +// by appending lines just above the visible part of the buffer. +// +// When the screen height increases, libvterm will ask for a row in the +// scrollback buffer, which is mirrored in the Neovim buffer displaying lines +// that were previously invisible. +// +// The vterm->Neovim synchronization is performed in intervals of 10 +// milliseconds. This is done to minimize screen updates when receiving when +// receiving large bursts of data. +// +// This module is decoupled from the processes that normally feed it data, so +// it's possible to use it as a general purpose console buffer(possibly as a +// log/display mechanism for Neovim in the future) +// +// Inspired by vimshell(http://www.wana.at/vimshell/) and +// Conque(https://code.google.com/p/conque/). Libvterm usage instructions (plus +// some extra code) were taken from +// pangoterm(http://www.leonerd.org.uk/code/pangoterm/) +#include <assert.h> +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> + +#include <vterm.h> + +#include "nvim/vim.h" +#include "nvim/terminal.h" +#include "nvim/message.h" +#include "nvim/memory.h" +#include "nvim/option.h" +#include "nvim/macros.h" +#include "nvim/mbyte.h" +#include "nvim/buffer.h" +#include "nvim/ascii.h" +#include "nvim/getchar.h" +#include "nvim/ui.h" +#include "nvim/syntax.h" +#include "nvim/screen.h" +#include "nvim/keymap.h" +#include "nvim/edit.h" +#include "nvim/mouse.h" +#include "nvim/memline.h" +#include "nvim/mark.h" +#include "nvim/map.h" +#include "nvim/misc1.h" +#include "nvim/move.h" +#include "nvim/ex_docmd.h" +#include "nvim/ex_cmds.h" +#include "nvim/window.h" +#include "nvim/fileio.h" +#include "nvim/os/event.h" +#include "nvim/api/private/helpers.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "terminal.c.generated.h" +#endif + +#define SCROLLBACK_BUFFER_DEFAULT_SIZE 1000 +// Delay for refreshing the terminal buffer after receiving updates from +// libvterm. This is greatly improves performance when receiving large bursts +// of data. +#define REFRESH_DELAY 10 + +static uv_timer_t refresh_timer; +static bool refresh_pending = false; + +typedef struct { + size_t cols; + VTermScreenCell cells[]; +} ScrollbackLine; + +struct terminal { + // options passed to terminal_open + TerminalOptions opts; + // libvterm structures + VTerm *vt; + VTermScreen *vts; + // buffer used to: + // - convert VTermScreen cell arrays into utf8 strings + // - receive data from libvterm as a result of key presses. + char textbuf[0x1fff]; + // Scrollback buffer storage for libvterm. + // TODO(tarruda): Use a doubly-linked list + ScrollbackLine **sb_buffer; + // number of rows pushed to sb_buffer + size_t sb_current; + // sb_buffer size; + size_t sb_size; + // "virtual index" that points to the first sb_buffer row that we need to + // push to the terminal buffer when refreshing the scrollback. When negative, + // it actually points to entries that are no longer in sb_buffer (because the + // window height has increased) and must be deleted from the terminal buffer + int sb_pending; + // buf_T instance that acts as a "drawing surface" for libvterm + buf_T *buf; + // program exited + bool closed, destroy; + // some vterm properties + bool forward_mouse; + // invalid rows libvterm screen + int invalid_start, invalid_end; + struct { + int row, col; + bool visible; + } cursor; + // which mouse button is pressed + int pressed_button; + // pending width/height + bool pending_resize; + // color palette. this isn't set directly in the vterm instance because + // the default values are used to obtain the color numbers passed to cterm + // colors + RgbValue colors[256]; + // attributes for focused/unfocused cursor cells + int focused_cursor_attr_id, unfocused_cursor_attr_id; +}; + +static VTermScreenCallbacks vterm_screen_callbacks = { + .damage = term_damage, + .moverect = term_moverect, + .movecursor = term_movecursor, + .settermprop = term_settermprop, + .bell = term_bell, + .sb_pushline = term_sb_push, + .sb_popline = term_sb_pop, +}; + +static PMap(ptr_t) *invalidated_terminals; +static Map(int, int) *color_indexes; +static int default_vt_fg, default_vt_bg; +static VTermColor default_vt_bg_rgb; + +void terminal_init(void) +{ + invalidated_terminals = pmap_new(ptr_t)(); + uv_timer_init(uv_default_loop(), &refresh_timer); + + // initialize a rgb->color index map for cterm attributes(VTermScreenCell + // only has RGB information and we need color indexes for terminal UIs) + color_indexes = map_new(int, int)(); + VTerm *vt = vterm_new(24, 80); + VTermState *state = vterm_obtain_state(vt); + + for (int color_index = 0; color_index < 256; color_index++) { + VTermColor color; + vterm_state_get_palette_color(state, color_index, &color); + map_put(int, int)(color_indexes, + RGB(color.red, color.green, color.blue), color_index + 1); + } + + VTermColor fg, bg; + vterm_state_get_default_colors(state, &fg, &bg); + default_vt_fg = RGB(fg.red, fg.green, fg.blue); + default_vt_bg = RGB(bg.red, bg.green, bg.blue); + default_vt_bg_rgb = bg; + vterm_free(vt); +} + +void terminal_teardown(void) +{ + uv_timer_stop(&refresh_timer); + uv_close((uv_handle_t *)&refresh_timer, NULL); + pmap_free(ptr_t)(invalidated_terminals); + map_free(int, int)(color_indexes); +} + +// public API {{{ + +Terminal *terminal_open(TerminalOptions opts) +{ + // Create a new terminal instance and configure it + Terminal *rv = xcalloc(1, sizeof(Terminal)); + rv->opts = opts; + rv->cursor.visible = true; + // Associate the terminal instance with the new buffer + rv->buf = curbuf; + curbuf->terminal = rv; + // Create VTerm + rv->vt = vterm_new(opts.height, opts.width); + vterm_set_utf8(rv->vt, 1); + // Setup state + VTermState *state = vterm_obtain_state(rv->vt); + vterm_state_set_bold_highbright(state, true); + // Set up screen + rv->vts = vterm_obtain_screen(rv->vt); + vterm_screen_enable_altscreen(rv->vts, true); + // delete empty lines at the end of the buffer + vterm_screen_set_callbacks(rv->vts, &vterm_screen_callbacks, rv); + vterm_screen_set_damage_merge(rv->vts, VTERM_DAMAGE_SCROLL); + vterm_screen_reset(rv->vts, 1); + // force a initial refresh of the screen to ensure the buffer will always + // have as many lines as screen rows when refresh_scrollback is called + rv->invalid_start = 0; + rv->invalid_end = opts.height; + refresh_screen(rv); + set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL); + // some sane settings for terminal buffers + set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL); + set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL); + set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL); + RESET_BINDING(curwin); + // Apply TermOpen autocmds so the user can configure the terminal + apply_autocmds(EVENT_TERMOPEN, NULL, NULL, true, curbuf); + + // Configure the scrollback buffer. Try to get the size from: + // + // - b:terminal_scrollback_buffer_size + // - g:terminal_scrollback_buffer_size + // - SCROLLBACK_BUFFER_DEFAULT_SIZE + // + // but limit to 100k. + int size = get_config_int(rv, "terminal_scrollback_buffer_size"); + rv->sb_size = size > 0 ? (size_t)size : SCROLLBACK_BUFFER_DEFAULT_SIZE; + rv->sb_size = MIN(rv->sb_size, 100000); + rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size); + + // Configure the color palette. Try to get the color from: + // + // - b:terminal_color_{NUM} + // - g:terminal_color_{NUM} + // - the VTerm instance + for (int i = 0; i < (int)ARRAY_SIZE(rv->colors); i++) { + RgbValue color_val = -1; + char var[64]; + snprintf(var, sizeof(var), "terminal_color_%d", i); + char *name = get_config_string(rv, var); + if (name) { + color_val = name_to_color((uint8_t *)name); + free(name); + + if (color_val != -1) { + rv->colors[i] = color_val; + } + } + + if (color_val == -1) { + // the default is taken from vterm + VTermColor color; + vterm_state_get_palette_color(state, i, &color); + rv->colors[i] = RGB(color.red, color.green, color.blue); + } + } + + // Configure cursor highlighting when focused/unfocused + char *group = get_config_string(rv, "terminal_focused_cursor_highlight"); + if (group) { + int group_id = syn_name2id((uint8_t *)group); + free(group); + + if (group_id) { + rv->focused_cursor_attr_id = syn_id2attr(group_id); + } + } + if (!rv->focused_cursor_attr_id) { + rv->focused_cursor_attr_id = get_attr_entry(&(attrentry_T) { + .rgb_ae_attr = HL_INVERSE, .rgb_fg_color = -1, .rgb_bg_color = -1, + .cterm_ae_attr = HL_INVERSE, .cterm_fg_color = 0, .cterm_bg_color = 0 + }); + } + + group = get_config_string(rv, "terminal_unfocused_cursor_highlight"); + if (group) { + int group_id = syn_name2id((uint8_t *)group); + free(group); + + if (group_id) { + rv->unfocused_cursor_attr_id = syn_id2attr(group_id); + } + } + if (!rv->unfocused_cursor_attr_id) { + int yellow_rgb = RGB(0xfc, 0xe9, 0x4f); + int yellow_term = 12; + rv->unfocused_cursor_attr_id = get_attr_entry(&(attrentry_T) { + .rgb_ae_attr = 0, .rgb_fg_color = -1, .rgb_bg_color = yellow_rgb, + .cterm_ae_attr = 0, .cterm_fg_color = 0, .cterm_bg_color = yellow_term, + }); + } + + return rv; +} + +void terminal_close(Terminal *term, char *msg) +{ + if (term->closed) { + return; + } + + term->forward_mouse = false; + term->closed = true; + if (!msg || exiting) { + // If no msg was given, this was called by close_buffer(buffer.c) so we + // should not wait for the user to press a key. Also cannot wait if + // `exiting == true` + term->opts.close_cb(term->opts.data); + } else { + terminal_receive(term, msg, strlen(msg)); + } +} + +void terminal_resize(Terminal *term, uint16_t width, uint16_t height) +{ + if (term->closed) { + // will be called after exited if two windows display the same terminal and + // one of the is closed as a consequence of pressing a key. + return; + } + int curwidth, curheight; + vterm_get_size(term->vt, &curheight, &curwidth); + + if (!width) { + width = (uint16_t)curwidth; + } + + if (!height) { + height = (uint16_t)curheight; + } + + // The new width/height are the minimum for all windows that display the + // terminal in the current tab. + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (!wp->w_closing && wp->w_buffer == term->buf) { + width = (uint16_t)MIN(width, (uint16_t)wp->w_width); + height = (uint16_t)MIN(height, (uint16_t)wp->w_height); + } + } + + if (curheight == height && curwidth == width) { + return; + } + + vterm_set_size(term->vt, height, width); + vterm_screen_flush_damage(term->vts); + term->pending_resize = true; + invalidate_terminal(term, -1, -1); +} + +void terminal_enter(Terminal *term, bool process_deferred) +{ + checkpcmark(); + setpcmark(); + int save_state = State; + int save_rd = RedrawingDisabled; + State = TERM_FOCUS; + RedrawingDisabled = false; + bool save_mapped_ctrl_c = mapped_ctrl_c; + mapped_ctrl_c = true; + // go to the bottom when the terminal is focused + adjust_topline(term, false); + // erase the unfocused cursor + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + showmode(); + ui_busy_start(); + redraw(false); + int c; + bool close = false; + + for (;;) { + if (process_deferred) { + event_enable_deferred(); + } + + c = safe_vgetc(); + + if (process_deferred) { + event_disable_deferred(); + } + + switch (c) { + case Ctrl_BSL: + c = safe_vgetc(); + if (c == Ctrl_N) { + goto end; + } + terminal_send_key(term, c); + break; + + case K_LEFTMOUSE: + case K_LEFTDRAG: + case K_LEFTRELEASE: + case K_MIDDLEMOUSE: + case K_MIDDLEDRAG: + case K_MIDDLERELEASE: + case K_RIGHTMOUSE: + case K_RIGHTDRAG: + case K_RIGHTRELEASE: + case K_MOUSEDOWN: + case K_MOUSEUP: + if (send_mouse_event(term, c)) { + goto end; + } + break; + + case K_EVENT: + event_process(); + break; + + default: + if (term->closed) { + close = true; + goto end; + } + + terminal_send_key(term, c); + } + } + +end: + restart_edit = 0; + State = save_state; + RedrawingDisabled = save_rd; + // draw the unfocused cursor + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + mapped_ctrl_c = save_mapped_ctrl_c; + unshowmode(true); + redraw(false); + ui_busy_stop(); + if (close) { + term->opts.close_cb(term->opts.data); + do_cmdline_cmd((uint8_t *)"bwipeout!"); + } +} + +void terminal_destroy(Terminal *term) +{ + term->buf->terminal = NULL; + term->buf = NULL; + pmap_del(ptr_t)(invalidated_terminals, term); + for (size_t i = 0 ; i < term->sb_current; i++) { + free(term->sb_buffer[i]); + } + free(term->sb_buffer); + vterm_free(term->vt); + free(term); +} + +void terminal_send(Terminal *term, char *data, size_t size) +{ + if (term->closed) { + return; + } + term->opts.write_cb(data, size, term->opts.data); +} + +void terminal_send_key(Terminal *term, int c) +{ + VTermModifier mod = VTERM_MOD_NONE; + VTermKey key = convert_key(c, &mod); + + if (key) { + vterm_keyboard_key(term->vt, key, mod); + } else { + vterm_keyboard_unichar(term->vt, (uint32_t)c, mod); + } + + size_t len = vterm_output_read(term->vt, term->textbuf, + sizeof(term->textbuf)); + terminal_send(term, term->textbuf, (size_t)len); +} + +void terminal_receive(Terminal *term, char *data, size_t len) +{ + if (!data) { + return; + } + + vterm_input_write(term->vt, data, len); + vterm_screen_flush_damage(term->vts); +} + +void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, + int *term_attrs) +{ + int height, width; + vterm_get_size(term->vt, &height, &width); + assert(linenr); + int row = linenr_to_row(term, linenr); + if (row >= height) { + // Terminal height was decreased but the change wasn't reflected into the + // buffer yet + return; + } + + for (int col = 0; col < width; col++) { + VTermScreenCell cell; + fetch_cell(term, row, col, &cell); + // Get the rgb value set by libvterm. + int vt_fg = RGB(cell.fg.red, cell.fg.green, cell.fg.blue); + int vt_bg = RGB(cell.bg.red, cell.bg.green, cell.bg.blue); + vt_fg = vt_fg != default_vt_fg ? vt_fg : - 1; + vt_bg = vt_bg != default_vt_bg ? vt_bg : - 1; + // Since libvterm does not expose the color index used by the program, we + // use the rgb value to find the appropriate index in the cache computed by + // `terminal_init`. + int vt_fg_idx = vt_fg != default_vt_fg ? + map_get(int, int)(color_indexes, vt_fg) : 0; + int vt_bg_idx = vt_bg != default_vt_bg ? + map_get(int, int)(color_indexes, vt_bg) : 0; + // The index is now used to get the final rgb value from the + // user-customizable palette. + int vt_fg_rgb = vt_fg_idx != 0 ? term->colors[vt_fg_idx - 1] : -1; + int vt_bg_rgb = vt_bg_idx != 0 ? term->colors[vt_bg_idx - 1] : -1; + + int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0) + | (cell.attrs.italic ? HL_ITALIC : 0) + | (cell.attrs.reverse ? HL_INVERSE : 0) + | (cell.attrs.underline ? HL_UNDERLINE : 0); + + int attr_id = 0; + + if (hl_attrs || vt_fg != -1 || vt_bg != -1) { + attr_id = get_attr_entry(&(attrentry_T) { + .cterm_ae_attr = (int16_t)hl_attrs, + .cterm_fg_color = vt_fg_idx, + .cterm_bg_color = vt_bg_idx, + .rgb_ae_attr = (int16_t)hl_attrs, + .rgb_fg_color = vt_fg_rgb, + .rgb_bg_color = vt_bg_rgb, + }); + } + + if (term->cursor.visible && term->cursor.row == row + && term->cursor.col == col) { + attr_id = hl_combine_attr(attr_id, is_focused(term) && wp == curwin ? + term->focused_cursor_attr_id : term->unfocused_cursor_attr_id); + } + + term_attrs[col] = attr_id; + } +} + +// }}} +// libvterm callbacks {{{ + +static int term_damage(VTermRect rect, void *data) +{ + invalidate_terminal(data, rect.start_row, rect.end_row); + return 1; +} + +static int term_moverect(VTermRect dest, VTermRect src, void *data) +{ + invalidate_terminal(data, MIN(dest.start_row, src.start_row), + MAX(dest.end_row, src.end_row)); + return 1; +} + +static int term_movecursor(VTermPos new, VTermPos old, int visible, + void *data) +{ + Terminal *term = data; + term->cursor.row = new.row; + term->cursor.col = new.col; + invalidate_terminal(term, old.row, old.row + 1); + invalidate_terminal(term, new.row, new.row + 1); + return 1; +} + +static int term_settermprop(VTermProp prop, VTermValue *val, void *data) +{ + Terminal *term = data; + + switch (prop) { + case VTERM_PROP_ALTSCREEN: + break; + + case VTERM_PROP_CURSORVISIBLE: + term->cursor.visible = val->boolean; + invalidate_terminal(term, term->cursor.row, term->cursor.row + 1); + break; + + case VTERM_PROP_TITLE: { + Error err; + dict_set_value(term->buf->b_vars, + cstr_as_string("term_title"), + STRING_OBJ(cstr_as_string(val->string)), &err); + break; + } + + case VTERM_PROP_MOUSE: + term->forward_mouse = (bool)val->number; + break; + + default: + return 0; + } + + return 1; +} + +static int term_bell(void *data) +{ + ui_putc('\x07'); + return 1; +} + +// the scrollback push/pop handlers were copied almost verbatim from pangoterm +static int term_sb_push(int cols, const VTermScreenCell *cells, void *data) +{ + Terminal *term = data; + // copy vterm cells into sb_buffer + size_t c = (size_t)cols; + ScrollbackLine *sbrow = NULL; + if (term->sb_current == term->sb_size) { + if (term->sb_buffer[term->sb_current - 1]->cols == c) { + // Recycle old row if it's the right size + sbrow = term->sb_buffer[term->sb_current - 1]; + } else { + free(term->sb_buffer[term->sb_current - 1]); + } + + memmove(term->sb_buffer + 1, term->sb_buffer, + sizeof(term->sb_buffer[0]) * (term->sb_current - 1)); + + } else if (term->sb_current > 0) { + memmove(term->sb_buffer + 1, term->sb_buffer, + sizeof(term->sb_buffer[0]) * term->sb_current); + } + + if (!sbrow) { + sbrow = xmalloc(sizeof(ScrollbackLine) + c * sizeof(sbrow->cells[0])); + sbrow->cols = c; + } + + term->sb_buffer[0] = sbrow; + if (term->sb_current < term->sb_size) { + term->sb_current++; + } + + if (term->sb_pending < (int)term->sb_size) { + term->sb_pending++; + } + + memcpy(sbrow->cells, cells, sizeof(cells[0]) * c); + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + + return 1; +} + +static int term_sb_pop(int cols, VTermScreenCell *cells, void *data) +{ + Terminal *term = data; + + if (!term->sb_current) { + return 0; + } + + if (term->sb_pending) { + term->sb_pending--; + } + + // restore vterm state + size_t c = (size_t)cols; + ScrollbackLine *sbrow = term->sb_buffer[0]; + term->sb_current--; + memmove(term->sb_buffer, term->sb_buffer + 1, + sizeof(term->sb_buffer[0]) * (term->sb_current)); + + size_t cols_to_copy = c; + if (cols_to_copy > sbrow->cols) { + cols_to_copy = sbrow->cols; + } + + // copy to vterm state + memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy); + for (size_t col = cols_to_copy; col < c; col++) { + cells[col].chars[0] = 0; + cells[col].width = 1; + } + free(sbrow); + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + + return 1; +} + +// }}} +// input handling {{{ + +static void convert_modifiers(VTermModifier *statep) +{ + if (mod_mask & MOD_MASK_SHIFT) { *statep |= VTERM_MOD_SHIFT; } + if (mod_mask & MOD_MASK_CTRL) { *statep |= VTERM_MOD_CTRL; } + if (mod_mask & MOD_MASK_ALT) { *statep |= VTERM_MOD_ALT; } +} + +static VTermKey convert_key(int key, VTermModifier *statep) +{ + convert_modifiers(statep); + + switch (key) { + case K_BS: return VTERM_KEY_BACKSPACE; + case TAB: return VTERM_KEY_TAB; + case Ctrl_M: return VTERM_KEY_ENTER; + case ESC: return VTERM_KEY_ESCAPE; + + case K_UP: return VTERM_KEY_UP; + case K_DOWN: return VTERM_KEY_DOWN; + case K_LEFT: return VTERM_KEY_LEFT; + case K_RIGHT: return VTERM_KEY_RIGHT; + + case K_INS: return VTERM_KEY_INS; + case K_DEL: return VTERM_KEY_DEL; + case K_HOME: return VTERM_KEY_HOME; + case K_END: return VTERM_KEY_END; + case K_PAGEUP: return VTERM_KEY_PAGEUP; + case K_PAGEDOWN: return VTERM_KEY_PAGEDOWN; + + case K_K0: + case K_KINS: return VTERM_KEY_KP_0; + case K_K1: + case K_KEND: return VTERM_KEY_KP_1; + case K_K2: return VTERM_KEY_KP_2; + case K_K3: + case K_KPAGEDOWN: return VTERM_KEY_KP_3; + case K_K4: return VTERM_KEY_KP_4; + case K_K5: return VTERM_KEY_KP_5; + case K_K6: return VTERM_KEY_KP_6; + case K_K7: + case K_KHOME: return VTERM_KEY_KP_7; + case K_K8: return VTERM_KEY_KP_8; + case K_K9: + case K_KPAGEUP: return VTERM_KEY_KP_9; + case K_KDEL: + case K_KPOINT: return VTERM_KEY_KP_PERIOD; + case K_KENTER: return VTERM_KEY_KP_ENTER; + case K_KPLUS: return VTERM_KEY_KP_PLUS; + case K_KMINUS: return VTERM_KEY_KP_MINUS; + case K_KMULTIPLY: return VTERM_KEY_KP_MULT; + case K_KDIVIDE: return VTERM_KEY_KP_DIVIDE; + + default: return VTERM_KEY_NONE; + } +} + +static void mouse_action(Terminal *term, int button, int row, int col, + bool drag, VTermModifier mod) +{ + if (term->pressed_button && (term->pressed_button != button || !drag)) { + // release the previous button + vterm_mouse_button(term->vt, term->pressed_button, 0, mod); + term->pressed_button = 0; + } + + // move the mouse + vterm_mouse_move(term->vt, row, col, mod); + + if (!term->pressed_button) { + // press the button if not already pressed + vterm_mouse_button(term->vt, button, 1, mod); + term->pressed_button = button; + } +} + +// process a mouse event while the terminal is focused. return true if the +// terminal should lose focus +static bool send_mouse_event(Terminal *term, int c) +{ + int row = mouse_row, col = mouse_col; + win_T *mouse_win = mouse_find_win(&row, &col); + + if (term->forward_mouse && mouse_win->w_buffer == term->buf) { + // event in the terminal window and mouse events was enabled by the + // program. translate and forward the event + int button; + bool drag = false; + + switch (c) { + case K_LEFTDRAG: drag = true; + case K_LEFTMOUSE: button = 1; break; + case K_MIDDLEDRAG: drag = true; + case K_MIDDLEMOUSE: button = 2; break; + case K_RIGHTDRAG: drag = true; + case K_RIGHTMOUSE: button = 3; break; + case K_MOUSEDOWN: button = 4; break; + case K_MOUSEUP: button = 5; break; + default: return false; + } + + mouse_action(term, button, row, col, drag, 0); + size_t len = vterm_output_read(term->vt, term->textbuf, + sizeof(term->textbuf)); + terminal_send(term, term->textbuf, (size_t)len); + return false; + } + + if (c == K_MOUSEDOWN || c == K_MOUSEUP) { + win_T *save_curwin = curwin; + // switch window/buffer to perform the scroll + curwin = mouse_win; + curbuf = curwin->w_buffer; + int direction = c == K_MOUSEDOWN ? MSCR_DOWN : MSCR_UP; + if (mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL)) { + scroll_redraw(direction, curwin->w_botline - curwin->w_topline); + } else { + scroll_redraw(direction, 3L); + } + + curwin->w_redr_status = true; + curwin = save_curwin; + curbuf = curwin->w_buffer; + redraw_win_later(mouse_win, NOT_VALID); + invalidate_terminal(term, -1, -1); + // Only need to exit focus if the scrolled window is the terminal window + return mouse_win == curwin; + } + + ins_char_typebuf(c); + return true; +} + +// }}} +// terminal buffer refresh & misc {{{ + + +void fetch_row(Terminal *term, int row, int end_col) +{ + int col = 0; + size_t line_len = 0; + char *ptr = term->textbuf; + + while (col < end_col) { + VTermScreenCell cell; + fetch_cell(term, row, col, &cell); + int cell_len = 0; + if (cell.chars[0]) { + for (int i = 0; cell.chars[i]; i++) { + cell_len += utf_char2bytes((int)cell.chars[i], + (uint8_t *)ptr + cell_len); + } + } else { + *ptr = ' '; + cell_len = 1; + } + char c = *ptr; + ptr += cell_len; + if (c != ' ') { + // only increase the line length if the last character is not whitespace + line_len = (size_t)(ptr - term->textbuf); + } + col += cell.width; + } + + // trim trailing whitespace + term->textbuf[line_len] = 0; +} + +static void fetch_cell(Terminal *term, int row, int col, + VTermScreenCell *cell) +{ + if (row < 0) { + ScrollbackLine *sbrow = term->sb_buffer[-row - 1]; + if ((size_t)col < sbrow->cols) { + *cell = sbrow->cells[col]; + } else { + // fill the pointer with an empty cell + *cell = (VTermScreenCell) { + .chars = { 0 }, + .width = 1, + .bg = default_vt_bg_rgb + }; + } + } else { + vterm_screen_get_cell(term->vts, (VTermPos){.row = row, .col = col}, + cell); + } +} + +// queue a terminal instance for refresh +static void invalidate_terminal(Terminal *term, int start_row, int end_row) +{ + if (start_row != -1 && end_row != -1) { + term->invalid_start = MIN(term->invalid_start, start_row); + term->invalid_end = MAX(term->invalid_end, end_row); + } + + pmap_put(ptr_t)(invalidated_terminals, term, NULL); + if (!refresh_pending) { + uv_timer_start(&refresh_timer, refresh_timer_cb, REFRESH_DELAY, 0); + refresh_pending = true; + } +} + +// libuv timer callback. This will enqueue on_refresh to be processed as an +// event. +static void refresh_timer_cb(uv_timer_t *handle) +{ + event_push((Event) {.handler = on_refresh}, false); + refresh_pending = false; +} + +// Refresh all invalidated terminals +static void on_refresh(Event event) +{ + if (exiting) { + // bad things can happen if we redraw when exiting, and there's no need to + // update the buffer. + return; + } + Terminal *term; + void *stub; (void)(stub); + // dont process autocommands while updating terminal buffers. JobActivity can + // be used act on terminal output. + block_autocmds(); + map_foreach(invalidated_terminals, term, stub, { + if (!term->buf) { + // destroyed by `close_buffer`. Dont do anything else + continue; + } + bool pending_resize = term->pending_resize; + WITH_BUFFER(term->buf, { + refresh_size(term); + refresh_scrollback(term); + refresh_screen(term); + redraw_buf_later(term->buf, NOT_VALID); + }); + adjust_topline(term, pending_resize); + }); + pmap_clear(ptr_t)(invalidated_terminals); + unblock_autocmds(); + redraw(true); +} + +static void refresh_size(Terminal *term) +{ + if (!term->pending_resize || term->closed) { + return; + } + + term->pending_resize = false; + int width, height; + vterm_get_size(term->vt, &height, &width); + term->invalid_start = 0; + term->invalid_end = height; + term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data); +} + +// Refresh the scrollback of a invalidated terminal +static void refresh_scrollback(Terminal *term) +{ + int width, height; + vterm_get_size(term->vt, &height, &width); + + while (term->sb_pending > 0) { + // This means that either the window height has decreased or the screen + // became full and libvterm had to push all rows up. Convert the first + // pending scrollback row into a string and append it just above the visible + // section of the buffer + if (((int)term->buf->b_ml.ml_line_count - height) >= (int)term->sb_size) { + // scrollback full, delete lines at the top + ml_delete(1, false); + deleted_lines(1, 1); + } + fetch_row(term, -term->sb_pending, width); + int buf_index = (int)term->buf->b_ml.ml_line_count - height; + ml_append(buf_index, (uint8_t *)term->textbuf, 0, false); + appended_lines(buf_index, 1); + term->sb_pending--; + } + + // Remove extra lines at the bottom + int max_line_count = (int)term->sb_current + height; + while (term->buf->b_ml.ml_line_count > max_line_count) { + ml_delete(term->buf->b_ml.ml_line_count, false); + deleted_lines(term->buf->b_ml.ml_line_count, 1); + } +} + +// Refresh the screen(visible part of the buffer when the terminal is +// focused) of a invalidated terminal +static void refresh_screen(Terminal *term) +{ + int changed = 0; + int added = 0; + int height; + int width; + vterm_get_size(term->vt, &height, &width); + // It's possible that the terminal height decreased and `term->invalid_end` + // doesn't reflect it yet + term->invalid_end = MIN(term->invalid_end, height); + + for (int r = term->invalid_start, linenr = row_to_linenr(term, r); + r < term->invalid_end; r++, linenr++) { + fetch_row(term, r, width); + + if (linenr <= term->buf->b_ml.ml_line_count) { + ml_replace(linenr, (uint8_t *)term->textbuf, true); + changed++; + } else { + ml_append(linenr - 1, (uint8_t *)term->textbuf, 0, false); + added++; + } + } + + int change_start = row_to_linenr(term, term->invalid_start); + int change_end = change_start + changed; + changed_lines(change_start, 0, change_end, added); + term->invalid_start = INT_MAX; + term->invalid_end = -1; +} + +static void redraw(bool restore_cursor) +{ + int save_row, save_col; + if (restore_cursor) { + // save the current row/col to restore after updating screen when not + // focused + save_row = ui_current_row(); + save_col = ui_current_col(); + } + block_autocmds(); + validate_cursor(); + + if (must_redraw) { + update_screen(0); + } + + redraw_statuslines(); + + if (need_maketitle) { + maketitle(); + } + + showruler(false); + + Terminal *term = curbuf->terminal; + if (term && is_focused(term)) { + curwin->w_wrow = term->cursor.row; + curwin->w_wcol = term->cursor.col + win_col_off(curwin); + setcursor(); + } else if (restore_cursor) { + ui_cursor_goto(save_row, save_col); + } else { + // exiting terminal focus, put the window cursor in a valid position + int height, width; + vterm_get_size(term->vt, &height, &width); + curwin->w_wrow = height - 1; + curwin->w_wcol = 0; + setcursor(); + } + + unblock_autocmds(); + ui_flush(); +} + +static void adjust_topline(Terminal *term, bool force) +{ + int height, width; + vterm_get_size(term->vt, &height, &width); + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == term->buf) { + // for every window that displays a terminal, ensure the cursor is in a + // valid line + wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, term->buf->b_ml.ml_line_count); + if (force || curbuf != term->buf || is_focused(term)) { + // if the terminal is not in the current window or if it's focused, + // adjust topline/cursor so the window will "follow" the terminal + // output + wp->w_cursor.lnum = term->buf->b_ml.ml_line_count; + set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1)); + } + } + } +} + +static int row_to_linenr(Terminal *term, int row) +{ + return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX; +} + +static int linenr_to_row(Terminal *term, int linenr) +{ + return linenr - (int)term->sb_current - 1; +} + +static bool is_focused(Terminal *term) +{ + return State & TERM_FOCUS && curbuf == term->buf; +} + +#define GET_CONFIG_VALUE(t, k, o) \ + do { \ + Error err; \ + o = dict_get_value(t->buf->b_vars, cstr_as_string(k), &err); \ + if (obj.type == kObjectTypeNil) { \ + o = dict_get_value(&globvardict, cstr_as_string(k), &err); \ + } \ + } while (0) + +static char *get_config_string(Terminal *term, char *key) +{ + Object obj = OBJECT_INIT; + GET_CONFIG_VALUE(term, key, obj); + if (obj.type == kObjectTypeString) { + return obj.data.string.data; + } + return NULL; +} + +static int get_config_int(Terminal *term, char *key) +{ + Object obj = OBJECT_INIT; + GET_CONFIG_VALUE(term, key, obj); + if (obj.type == kObjectTypeInteger) { + return (int)obj.data.integer; + } + return 0; +} + +// }}} + +// vim: foldmethod=marker foldenable diff --git a/src/nvim/terminal.h b/src/nvim/terminal.h new file mode 100644 index 0000000000..6e0b062fbd --- /dev/null +++ b/src/nvim/terminal.h @@ -0,0 +1,33 @@ +#ifndef NVIM_TERMINAL_H +#define NVIM_TERMINAL_H + +#include <stddef.h> +#include <stdbool.h> +#include <stdint.h> + +typedef struct terminal Terminal; +typedef void (*terminal_write_cb)(char *buffer, size_t size, void *data); +typedef void (*terminal_resize_cb)(uint16_t width, uint16_t height, void *data); +typedef void (*terminal_close_cb)(void *data); + +typedef struct { + void *data; + uint16_t width, height; + terminal_write_cb write_cb; + terminal_resize_cb resize_cb; + terminal_close_cb close_cb; +} TerminalOptions; + +#define TERMINAL_OPTIONS_INIT ((TerminalOptions) { \ + .data = NULL, \ + .width = 80, \ + .height = 24, \ + .write_cb = NULL, \ + .resize_cb = NULL, \ + .close_cb = NULL \ + }) + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "terminal.h.generated.h" +#endif +#endif // NVIM_TERMINAL_H diff --git a/src/nvim/undo.c b/src/nvim/undo.c index e67c5f2aba..67195235fe 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -315,6 +315,9 @@ int undo_allowed(void) */ static long get_undolevel(void) { + if (curbuf->terminal) { + return -1; + } if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL) return p_ul; return curbuf->b_p_ul; diff --git a/src/nvim/vim.h b/src/nvim/vim.h index a4ce9084c2..12b73843f8 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -112,9 +112,10 @@ Error: configure did not run properly.Check auto/config.log. #define SHOWMATCH (0x700 + INSERT) /* show matching paren */ #define CONFIRM 0x800 /* ":confirm" prompt */ #define SELECTMODE 0x1000 /* Select mode, only for mappings */ +#define TERM_FOCUS 0x2000 // Terminal focus mode -#define MAP_ALL_MODES (0x3f | SELECTMODE) /* all mode bits used for - * mapping */ +// all mode bits used for mapping +#define MAP_ALL_MODES (0x3f | SELECTMODE | TERM_FOCUS) /* directions */ #define FORWARD 1 diff --git a/src/nvim/window.c b/src/nvim/window.c index 7b5848c124..9f07f2bddc 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -52,6 +52,7 @@ #include "nvim/search.h" #include "nvim/strings.h" #include "nvim/syntax.h" +#include "nvim/terminal.h" #include "nvim/undo.h" #include "nvim/os/os.h" @@ -1765,6 +1766,12 @@ static int close_last_window_tabpage(win_T *win, int free_buf, tabpage_T *prev_c } buf_T *old_curbuf = curbuf; + Terminal *term = win->w_buffer->terminal; + if (term) { + // Don't free terminal buffers + free_buf = false; + } + /* * Closing the last window in a tab page. First go to another tab * page and then close the window and the tab page. This avoids that @@ -1789,6 +1796,13 @@ static int close_last_window_tabpage(win_T *win, int free_buf, tabpage_T *prev_c if (h != tabline_height()) shell_new_rows(); } + + if (term) { + // When a window containing a terminal buffer is closed, recalculate its + // size + terminal_resize(term, 0, 0); + } + /* Since goto_tabpage_tp above did not trigger *Enter autocommands, do * that now. */ apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, FALSE, curbuf); @@ -1907,6 +1921,8 @@ int win_close(win_T *win, int free_buf) || close_last_window_tabpage(win, free_buf, prev_curtab)) return FAIL; + // let terminal buffers know that this window dimensions may be ignored + win->w_closing = true; /* Free the memory used for the window and get the window that received * the screen space. */ wp = win_free_mem(win, &dir, NULL); @@ -1963,7 +1979,6 @@ int win_close(win_T *win, int free_buf) if (help_window) restore_snapshot(SNAP_HELP_IDX, close_curwin); - redraw_all_later(NOT_VALID); return OK; } @@ -4689,6 +4704,11 @@ void win_new_height(win_T *wp, int height) redraw_win_later(wp, SOME_VALID); wp->w_redr_status = TRUE; invalidate_botline_win(wp); + + if (wp->w_buffer->terminal) { + terminal_resize(wp->w_buffer->terminal, 0, wp->w_height); + redraw_win_later(wp, CLEAR); + } } /* @@ -4706,6 +4726,11 @@ void win_new_width(win_T *wp, int width) } redraw_win_later(wp, NOT_VALID); wp->w_redr_status = TRUE; + + if (wp->w_buffer->terminal) { + terminal_resize(wp->w_buffer->terminal, wp->w_width, 0); + redraw_win_later(wp, CLEAR); + } } void win_comp_scroll(win_T *wp) @@ -5570,4 +5595,3 @@ static int frame_check_width(frame_T *topfrp, int width) return TRUE; } - |