mirror of
https://github.com/neovim/neovim.git
synced 2024-09-17 20:58:20 -04:00
Merge #5396 from justinmk/tui-throttle
throttle shell output to maintain responsiveness
This commit is contained in:
commit
7c513d646d
@ -255,14 +255,20 @@ g8 Print the hex values of the bytes used in the
|
||||
backslashes are before the newline, only one is
|
||||
removed.
|
||||
|
||||
On Unix the command normally runs in a non-interactive
|
||||
shell. If you want an interactive shell to be used
|
||||
(to use aliases) set 'shellcmdflag' to "-ic".
|
||||
The command runs in a non-interactive shell connected
|
||||
to a pipe (not a terminal). Use |:terminal| to run an
|
||||
interactive shell connected to a terminal.
|
||||
|
||||
For Win32 also see |:!start|.
|
||||
|
||||
After the command has been executed, the timestamp and
|
||||
size of the current file is checked |timestamp|.
|
||||
|
||||
If the command produces too much output some lines may
|
||||
be skipped so the command can execute quickly. No
|
||||
data is lost, this only affects the display. The last
|
||||
few lines are always displayed (never skipped).
|
||||
|
||||
Vim redraws the screen after the command is finished,
|
||||
because it may have printed any text. This requires a
|
||||
hit-enter prompt, so that you can read any messages.
|
||||
|
@ -155,6 +155,10 @@ are always available and may be used simultaneously in separate plugins. The
|
||||
|
||||
|system()| does not support writing/reading "backgrounded" commands. |E5677|
|
||||
|
||||
Nvim may throttle (skip) messages from shell commands (|:!|, |:grep|, |:make|)
|
||||
if there is too much output. No data is lost, this only affects display and
|
||||
makes things faster. |:terminal| output is never throttled.
|
||||
|
||||
|mkdir()| behaviour changed:
|
||||
1. Assuming /tmp/foo does not exist and /tmp can be written to
|
||||
mkdir('/tmp/foo/bar', 'p', 0700) will create both /tmp/foo and /tmp/foo/bar
|
||||
|
@ -92,6 +92,22 @@ void loop_close(Loop *loop, bool wait)
|
||||
kl_destroy(WatcherPtr, loop->children);
|
||||
}
|
||||
|
||||
void loop_purge(Loop *loop)
|
||||
{
|
||||
uv_mutex_lock(&loop->mutex);
|
||||
multiqueue_purge_events(loop->thread_events);
|
||||
multiqueue_purge_events(loop->fast_events);
|
||||
uv_mutex_unlock(&loop->mutex);
|
||||
}
|
||||
|
||||
size_t loop_size(Loop *loop)
|
||||
{
|
||||
uv_mutex_lock(&loop->mutex);
|
||||
size_t rv = multiqueue_size(loop->thread_events);
|
||||
uv_mutex_unlock(&loop->mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void async_cb(uv_async_t *handle)
|
||||
{
|
||||
Loop *l = handle->loop->data;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Multi-level queue for selective async event processing. Multiqueue supports
|
||||
// a parent-child relationship with the following properties:
|
||||
// Multi-level queue for selective async event processing.
|
||||
// Not threadsafe; access must be synchronized externally.
|
||||
//
|
||||
// Multiqueue supports a parent-child relationship with these properties:
|
||||
// - pushing a node to a child queue will push a corresponding link node to the
|
||||
// parent queue
|
||||
// - removing a link node from a parent queue will remove the next node
|
||||
@ -14,8 +15,7 @@
|
||||
// +----------------+
|
||||
// | Main loop |
|
||||
// +----------------+
|
||||
// ^
|
||||
// |
|
||||
//
|
||||
// +----------------+
|
||||
// +-------------->| Event loop |<------------+
|
||||
// | +--+-------------+ |
|
||||
@ -60,7 +60,7 @@ struct multiqueue_item {
|
||||
MultiQueue *queue;
|
||||
struct {
|
||||
Event event;
|
||||
MultiQueueItem *parent;
|
||||
MultiQueueItem *parent_item;
|
||||
} item;
|
||||
} data;
|
||||
bool link; // true: current item is just a link to a node in a child queue
|
||||
@ -69,9 +69,10 @@ struct multiqueue_item {
|
||||
|
||||
struct multiqueue {
|
||||
MultiQueue *parent;
|
||||
QUEUE headtail;
|
||||
QUEUE headtail; // circularly-linked
|
||||
put_callback put_cb;
|
||||
void *data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
||||
@ -88,7 +89,8 @@ MultiQueue *multiqueue_new_parent(put_callback put_cb, void *data)
|
||||
MultiQueue *multiqueue_new_child(MultiQueue *parent)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
assert(!parent->parent);
|
||||
assert(!parent->parent); // parent cannot have a parent, more like a "root"
|
||||
parent->size++;
|
||||
return multiqueue_new(parent, NULL, NULL);
|
||||
}
|
||||
|
||||
@ -97,6 +99,7 @@ static MultiQueue *multiqueue_new(MultiQueue *parent, put_callback put_cb,
|
||||
{
|
||||
MultiQueue *rv = xmalloc(sizeof(MultiQueue));
|
||||
QUEUE_INIT(&rv->headtail);
|
||||
rv->size = 0;
|
||||
rv->parent = parent;
|
||||
rv->put_cb = put_cb;
|
||||
rv->data = data;
|
||||
@ -110,8 +113,8 @@ void multiqueue_free(MultiQueue *this)
|
||||
QUEUE *q = QUEUE_HEAD(&this->headtail);
|
||||
MultiQueueItem *item = multiqueue_node_data(q);
|
||||
if (this->parent) {
|
||||
QUEUE_REMOVE(&item->data.item.parent->node);
|
||||
xfree(item->data.item.parent);
|
||||
QUEUE_REMOVE(&item->data.item.parent_item->node);
|
||||
xfree(item->data.item.parent_item);
|
||||
}
|
||||
QUEUE_REMOVE(q);
|
||||
xfree(item);
|
||||
@ -145,6 +148,15 @@ void multiqueue_process_events(MultiQueue *this)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all events without processing them.
|
||||
void multiqueue_purge_events(MultiQueue *this)
|
||||
{
|
||||
assert(this);
|
||||
while (!multiqueue_empty(this)) {
|
||||
(void)multiqueue_remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool multiqueue_empty(MultiQueue *this)
|
||||
{
|
||||
assert(this);
|
||||
@ -157,6 +169,12 @@ void multiqueue_replace_parent(MultiQueue *this, MultiQueue *new_parent)
|
||||
this->parent = new_parent;
|
||||
}
|
||||
|
||||
/// Gets the count of all events currently in the queue.
|
||||
size_t multiqueue_size(MultiQueue *this)
|
||||
{
|
||||
return this->size;
|
||||
}
|
||||
|
||||
static Event multiqueue_remove(MultiQueue *this)
|
||||
{
|
||||
assert(!multiqueue_empty(this));
|
||||
@ -178,12 +196,13 @@ static Event multiqueue_remove(MultiQueue *this)
|
||||
} else {
|
||||
if (this->parent) {
|
||||
// remove the corresponding link node in the parent queue
|
||||
QUEUE_REMOVE(&item->data.item.parent->node);
|
||||
xfree(item->data.item.parent);
|
||||
QUEUE_REMOVE(&item->data.item.parent_item->node);
|
||||
xfree(item->data.item.parent_item);
|
||||
}
|
||||
rv = item->data.item.event;
|
||||
}
|
||||
|
||||
this->size--;
|
||||
xfree(item);
|
||||
return rv;
|
||||
}
|
||||
@ -196,11 +215,13 @@ static void multiqueue_push(MultiQueue *this, Event event)
|
||||
QUEUE_INSERT_TAIL(&this->headtail, &item->node);
|
||||
if (this->parent) {
|
||||
// push link node to the parent queue
|
||||
item->data.item.parent = xmalloc(sizeof(MultiQueueItem));
|
||||
item->data.item.parent->link = true;
|
||||
item->data.item.parent->data.queue = this;
|
||||
QUEUE_INSERT_TAIL(&this->parent->headtail, &item->data.item.parent->node);
|
||||
item->data.item.parent_item = xmalloc(sizeof(MultiQueueItem));
|
||||
item->data.item.parent_item->link = true;
|
||||
item->data.item.parent_item->data.queue = this;
|
||||
QUEUE_INSERT_TAIL(&this->parent->headtail,
|
||||
&item->data.item.parent_item->node);
|
||||
}
|
||||
this->size++;
|
||||
}
|
||||
|
||||
static MultiQueueItem *multiqueue_node_data(QUEUE *q)
|
||||
|
@ -1,3 +1,8 @@
|
||||
// Queue implemented by circularly-linked list.
|
||||
//
|
||||
// Adapted from libuv. Simpler and more efficient than klist.h for implementing
|
||||
// queues that support arbitrary insertion/removal.
|
||||
//
|
||||
// Copyright (c) 2013, Ben Noordhuis <info@bnoordhuis.nl>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
@ -28,6 +33,8 @@ typedef struct _queue {
|
||||
#define QUEUE_DATA(ptr, type, field) \
|
||||
((type *)((char *)(ptr) - offsetof(type, field)))
|
||||
|
||||
// Important note: mutating the list while QUEUE_FOREACH is
|
||||
// iterating over its elements results in undefined behavior.
|
||||
#define QUEUE_FOREACH(q, h) \
|
||||
for ( /* NOLINT(readability/braces) */ \
|
||||
(q) = (h)->next; (q) != (h); (q) = (q)->next)
|
||||
@ -56,17 +63,6 @@ static inline void QUEUE_ADD(QUEUE *const h, QUEUE *const n)
|
||||
h->prev->next = h;
|
||||
}
|
||||
|
||||
static inline void QUEUE_SPLIT(QUEUE *const h, QUEUE *const q, QUEUE *const n)
|
||||
FUNC_ATTR_ALWAYS_INLINE
|
||||
{
|
||||
n->prev = h->prev;
|
||||
n->prev->next = n;
|
||||
n->next = q;
|
||||
h->prev = q->prev;
|
||||
h->prev->next = h;
|
||||
q->prev = n;
|
||||
}
|
||||
|
||||
static inline void QUEUE_INSERT_HEAD(QUEUE *const h, QUEUE *const q)
|
||||
FUNC_ATTR_ALWAYS_INLINE
|
||||
{
|
||||
|
@ -158,4 +158,7 @@
|
||||
|
||||
#define RGB(r, g, b) ((r << 16) | (g << 8) | b)
|
||||
|
||||
#define STR_(x) #x
|
||||
#define STR(x) STR_(x)
|
||||
|
||||
#endif // NVIM_MACROS_H
|
||||
|
@ -534,7 +534,7 @@ int main(int argc, char **argv)
|
||||
}
|
||||
|
||||
TIME_MSG("before starting main loop");
|
||||
ILOG("Starting Neovim main loop.");
|
||||
ILOG("starting main loop");
|
||||
|
||||
/*
|
||||
* Call the main command loop. This never returns.
|
||||
|
@ -283,18 +283,16 @@ size_t memcnt(const void *data, char c, size_t len)
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/// The xstpcpy() function shall copy the string pointed to by src (including
|
||||
/// the terminating NUL character) into the array pointed to by dst.
|
||||
/// Copies the string pointed to by src (including the terminating NUL
|
||||
/// character) into the array pointed to by dst.
|
||||
///
|
||||
/// The xstpcpy() function shall return a pointer to the terminating NUL
|
||||
/// character copied into the dst buffer. This is the only difference with
|
||||
/// strcpy(), which returns dst.
|
||||
/// @returns pointer to the terminating NUL char copied into the dst buffer.
|
||||
/// This is the only difference with strcpy(), which returns dst.
|
||||
///
|
||||
/// WARNING: If copying takes place between objects that overlap, the behavior is
|
||||
/// undefined.
|
||||
/// WARNING: If copying takes place between objects that overlap, the behavior
|
||||
/// is undefined.
|
||||
///
|
||||
/// This is the Neovim version of stpcpy(3) as defined in POSIX 2008. We
|
||||
/// don't require that supported platforms implement POSIX 2008, so we
|
||||
/// Nvim version of POSIX 2008 stpcpy(3). We do not require POSIX 2008, so
|
||||
/// implement our own version.
|
||||
///
|
||||
/// @param dst
|
||||
@ -306,16 +304,15 @@ char *xstpcpy(char *restrict dst, const char *restrict src)
|
||||
return (char *)memcpy(dst, src, len + 1) + len;
|
||||
}
|
||||
|
||||
/// The xstpncpy() function shall copy not more than n bytes (bytes that follow
|
||||
/// a NUL character are not copied) from the array pointed to by src to the
|
||||
/// array pointed to by dst.
|
||||
/// Copies not more than n bytes (bytes that follow a NUL character are not
|
||||
/// copied) from the array pointed to by src to the array pointed to by dst.
|
||||
///
|
||||
/// If a NUL character is written to the destination, the xstpncpy() function
|
||||
/// shall return the address of the first such NUL character. Otherwise, it
|
||||
/// shall return &dst[maxlen].
|
||||
/// If a NUL character is written to the destination, xstpncpy() returns the
|
||||
/// address of the first such NUL character. Otherwise, it shall return
|
||||
/// &dst[maxlen].
|
||||
///
|
||||
/// WARNING: If copying takes place between objects that overlap, the behavior is
|
||||
/// undefined.
|
||||
/// WARNING: If copying takes place between objects that overlap, the behavior
|
||||
/// is undefined.
|
||||
///
|
||||
/// WARNING: xstpncpy will ALWAYS write maxlen bytes. If src is shorter than
|
||||
/// maxlen, zeroes will be written to the remaining bytes.
|
||||
|
@ -25,7 +25,9 @@
|
||||
#include "nvim/charset.h"
|
||||
#include "nvim/strings.h"
|
||||
|
||||
#define DYNAMIC_BUFFER_INIT {NULL, 0, 0}
|
||||
#define DYNAMIC_BUFFER_INIT { NULL, 0, 0 }
|
||||
#define NS_1_SECOND 1000000000U // 1 second, in nanoseconds
|
||||
#define OUT_DATA_THRESHOLD 1024 * 10U // 10KB, "a few screenfuls" of data.
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
@ -187,6 +189,9 @@ static int do_os_system(char **argv,
|
||||
bool silent,
|
||||
bool forward_output)
|
||||
{
|
||||
out_data_decide_throttle(0); // Initialize throttle decider.
|
||||
out_data_ring(NULL, 0); // Initialize output ring-buffer.
|
||||
|
||||
// the output buffer
|
||||
DynamicBuffer buf = DYNAMIC_BUFFER_INIT;
|
||||
stream_read_cb data_cb = system_data_cb;
|
||||
@ -215,7 +220,7 @@ static int do_os_system(char **argv,
|
||||
proc->err = &err;
|
||||
if (!process_spawn(proc)) {
|
||||
loop_poll_events(&main_loop, 0);
|
||||
// Failed, probably due to `sh` not being executable
|
||||
// Failed, probably due to 'sh' not being executable
|
||||
if (!silent) {
|
||||
MSG_PUTS(_("\nCannot execute "));
|
||||
msg_outtrans((char_u *)prog);
|
||||
@ -253,11 +258,15 @@ static int do_os_system(char **argv,
|
||||
wstream_set_write_cb(&in, shell_write_cb, NULL);
|
||||
}
|
||||
|
||||
// invoke busy_start here so event_poll_until wont change the busy state for
|
||||
// the UI
|
||||
// Invoke busy_start here so LOOP_PROCESS_EVENTS_UNTIL will not change the
|
||||
// busy state.
|
||||
ui_busy_start();
|
||||
ui_flush();
|
||||
int status = process_wait(proc, -1, NULL);
|
||||
if (!got_int && out_data_decide_throttle(0)) {
|
||||
// Last chunk of output was skipped; display it now.
|
||||
out_data_ring(NULL, SIZE_MAX);
|
||||
}
|
||||
ui_busy_stop();
|
||||
|
||||
// prepare the out parameters if requested
|
||||
@ -309,15 +318,130 @@ static void system_data_cb(Stream *stream, RBuffer *buf, size_t count,
|
||||
dbuf->len += nread;
|
||||
}
|
||||
|
||||
/// Tracks output received for the current executing shell command, and displays
|
||||
/// a pulsing "..." when output should be skipped. Tracking depends on the
|
||||
/// synchronous/blocking nature of ":!".
|
||||
//
|
||||
/// Purpose:
|
||||
/// 1. CTRL-C is more responsive. #1234 #5396
|
||||
/// 2. Improves performance of :! (UI, esp. TUI, is the bottleneck).
|
||||
/// 3. Avoids OOM during long-running, spammy :!.
|
||||
///
|
||||
/// Vim does not need this hack because:
|
||||
/// 1. :! in terminal-Vim runs in cooked mode, so CTRL-C is caught by the
|
||||
/// terminal and raises SIGINT out-of-band.
|
||||
/// 2. :! in terminal-Vim uses a tty (Nvim uses pipes), so commands
|
||||
/// (e.g. `git grep`) may page themselves.
|
||||
///
|
||||
/// @param size Length of data, used with internal state to decide whether
|
||||
/// output should be skipped. size=0 resets the internal state and
|
||||
/// returns the previous decision.
|
||||
///
|
||||
/// @returns true if output should be skipped and pulse was displayed.
|
||||
/// Returns the previous decision if size=0.
|
||||
static bool out_data_decide_throttle(size_t size)
|
||||
{
|
||||
static uint64_t started = 0; // Start time of the current throttle.
|
||||
static size_t received = 0; // Bytes observed since last throttle.
|
||||
static size_t visit = 0; // "Pulse" count of the current throttle.
|
||||
static char pulse_msg[] = { ' ', ' ', ' ', '\0' };
|
||||
|
||||
if (!size) {
|
||||
bool previous_decision = (visit > 0);
|
||||
started = received = visit = 0;
|
||||
return previous_decision;
|
||||
}
|
||||
|
||||
received += size;
|
||||
if (received < OUT_DATA_THRESHOLD
|
||||
// Display at least the first chunk of output even if it is big.
|
||||
|| (!started && received < size + 1000)) {
|
||||
return false;
|
||||
} else if (!visit) {
|
||||
started = os_hrtime();
|
||||
} else if (visit % 20 == 0) {
|
||||
uint64_t since = os_hrtime() - started;
|
||||
if (since > (3 * NS_1_SECOND)) {
|
||||
received = visit = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
visit++;
|
||||
// Pulse "..." at the bottom of the screen.
|
||||
size_t tick = (visit % 20 == 0)
|
||||
? 3 // Force all dots "..." on last visit.
|
||||
: (visit % 4);
|
||||
pulse_msg[0] = (tick == 0) ? ' ' : '.';
|
||||
pulse_msg[1] = (tick == 0 || 1 == tick) ? ' ' : '.';
|
||||
pulse_msg[2] = (tick == 0 || 1 == tick || 2 == tick) ? ' ' : '.';
|
||||
if (visit == 1) {
|
||||
screen_del_lines(0, 0, 1, (int)Rows, NULL);
|
||||
}
|
||||
int lastrow = (int)Rows - 1;
|
||||
screen_puts_len((char_u *)pulse_msg, ARRAY_SIZE(pulse_msg), lastrow, 0, 0);
|
||||
ui_flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Saves output in a quasi-ringbuffer. Used to ensure the last ~page of
|
||||
/// output for a shell-command is always displayed.
|
||||
///
|
||||
/// Init mode: Resets the internal state.
|
||||
/// output = NULL
|
||||
/// size = 0
|
||||
/// Print mode: Displays the current saved data.
|
||||
/// output = NULL
|
||||
/// size = SIZE_MAX
|
||||
///
|
||||
/// @param output Data to save, or NULL to invoke a special mode.
|
||||
/// @param size Length of `output`.
|
||||
static void out_data_ring(char *output, size_t size)
|
||||
{
|
||||
#define MAX_CHUNK_SIZE (OUT_DATA_THRESHOLD / 2)
|
||||
static char last_skipped[MAX_CHUNK_SIZE]; // Saved output.
|
||||
static size_t last_skipped_len = 0;
|
||||
|
||||
assert(output != NULL || (size == 0 || size == SIZE_MAX));
|
||||
|
||||
if (output == NULL && size == 0) { // Init mode
|
||||
last_skipped_len = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (output == NULL && size == SIZE_MAX) { // Print mode
|
||||
out_data_append_to_screen(last_skipped, last_skipped_len, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// This is basically a ring-buffer...
|
||||
if (size >= MAX_CHUNK_SIZE) { // Save mode
|
||||
size_t start = size - MAX_CHUNK_SIZE;
|
||||
memcpy(last_skipped, output + start, MAX_CHUNK_SIZE);
|
||||
last_skipped_len = MAX_CHUNK_SIZE;
|
||||
} else if (size > 0) {
|
||||
// Length of the old data that can be kept.
|
||||
size_t keep_len = MIN(last_skipped_len, MAX_CHUNK_SIZE - size);
|
||||
size_t keep_start = last_skipped_len - keep_len;
|
||||
// Shift the kept part of the old data to the start.
|
||||
if (keep_start) {
|
||||
memmove(last_skipped, last_skipped + keep_start, keep_len);
|
||||
}
|
||||
// Copy the entire new data to the remaining space.
|
||||
memcpy(last_skipped + keep_len, output, size);
|
||||
last_skipped_len = keep_len + size;
|
||||
}
|
||||
}
|
||||
|
||||
/// Continue to append data to last screen line.
|
||||
///
|
||||
/// @param output Data to append to screen lines.
|
||||
/// @param remaining Size of data.
|
||||
/// @param new_line If true, next data output will be on a new line.
|
||||
static void append_to_screen_end(char *output, size_t remaining, bool new_line)
|
||||
static void out_data_append_to_screen(char *output, size_t remaining,
|
||||
bool new_line)
|
||||
{
|
||||
// Column of last row to start appending data to.
|
||||
static colnr_T last_col = 0;
|
||||
static colnr_T last_col = 0; // Column of last row to append to.
|
||||
|
||||
size_t off = 0;
|
||||
int last_row = (int)Rows - 1;
|
||||
@ -370,7 +494,14 @@ static void out_data_cb(Stream *stream, RBuffer *buf, size_t count, void *data,
|
||||
size_t cnt;
|
||||
char *ptr = rbuffer_read_ptr(buf, &cnt);
|
||||
|
||||
append_to_screen_end(ptr, cnt, eof);
|
||||
if (ptr != NULL && cnt > 0
|
||||
&& out_data_decide_throttle(cnt)) { // Skip output above a threshold.
|
||||
// Save the skipped output. If it is the final chunk, we display it later.
|
||||
out_data_ring(ptr, cnt);
|
||||
} else {
|
||||
out_data_append_to_screen(ptr, cnt, eof);
|
||||
}
|
||||
|
||||
if (cnt) {
|
||||
rbuffer_consumed(buf, cnt);
|
||||
}
|
||||
|
@ -319,8 +319,6 @@ static bool handle_forced_escape(TermInput *input)
|
||||
return false;
|
||||
}
|
||||
|
||||
static void restart_reading(void **argv);
|
||||
|
||||
static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data,
|
||||
bool eof)
|
||||
{
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "nvim/lib/kvec.h"
|
||||
|
||||
#include "nvim/vim.h"
|
||||
#include "nvim/log.h"
|
||||
#include "nvim/ui.h"
|
||||
#include "nvim/map.h"
|
||||
#include "nvim/main.h"
|
||||
@ -32,6 +33,8 @@
|
||||
#define CNORM_COMMAND_MAX_SIZE 32
|
||||
#define OUTBUF_SIZE 0xffff
|
||||
|
||||
#define TOO_MANY_EVENTS 1000000
|
||||
|
||||
typedef struct {
|
||||
int top, bot, left, right;
|
||||
} Rect;
|
||||
@ -591,6 +594,18 @@ static void tui_flush(UI *ui)
|
||||
TUIData *data = ui->data;
|
||||
UGrid *grid = &data->grid;
|
||||
|
||||
size_t nrevents = loop_size(data->loop);
|
||||
if (nrevents > TOO_MANY_EVENTS) {
|
||||
ILOG("TUI event-queue flooded (thread_events=%zu); purging", nrevents);
|
||||
// Back-pressure: UI events may accumulate much faster than the terminal
|
||||
// device can serve them. Even if SIGINT/CTRL-C is received, user must still
|
||||
// wait for the TUI event-queue to drain, and if there are ~millions of
|
||||
// events in the queue, it could take hours. Clearing the queue allows the
|
||||
// UI to recover. #1234 #5396
|
||||
loop_purge(data->loop);
|
||||
tui_busy_stop(ui); // avoid hidden cursor
|
||||
}
|
||||
|
||||
while (kv_size(data->invalid_regions)) {
|
||||
Rect r = kv_pop(data->invalid_regions);
|
||||
int currow = -1;
|
||||
|
@ -88,18 +88,17 @@ void ui_builtin_start(void)
|
||||
#ifdef FEAT_TUI
|
||||
tui_start();
|
||||
#else
|
||||
fprintf(stderr, "Neovim was built without a Terminal UI," \
|
||||
"press Ctrl+C to exit\n");
|
||||
|
||||
fprintf(stderr, "Nvim headless-mode started.\n");
|
||||
size_t len;
|
||||
char **addrs = server_address_list(&len);
|
||||
if (addrs != NULL) {
|
||||
fprintf(stderr, "currently listening on the following address(es)\n");
|
||||
fprintf(stderr, "Listening on:\n");
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
fprintf(stderr, "\t%s\n", addrs[i]);
|
||||
}
|
||||
xfree(addrs);
|
||||
}
|
||||
fprintf(stderr, "Press CTRL+C to exit.\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
// UI wrapper that sends UI requests to the UI thread.
|
||||
// Used by the built-in TUI and external libnvim-based UIs.
|
||||
// UI wrapper that sends requests to the UI thread.
|
||||
// Used by the built-in TUI and libnvim-based UIs.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "nvim/log.h"
|
||||
#include "nvim/main.h"
|
||||
#include "nvim/vim.h"
|
||||
#include "nvim/ui.h"
|
||||
@ -19,10 +20,30 @@
|
||||
|
||||
#define UI(b) (((UIBridgeData *)b)->ui)
|
||||
|
||||
// Call a function in the UI thread
|
||||
#if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL
|
||||
static size_t uilog_seen = 0;
|
||||
static argv_callback uilog_event = NULL;
|
||||
#define UI_CALL(ui, name, argc, ...) \
|
||||
do { \
|
||||
if (uilog_event == ui_bridge_##name##_event) { \
|
||||
uilog_seen++; \
|
||||
} else { \
|
||||
if (uilog_seen > 0) { \
|
||||
DLOG("UI bridge: ...%zu times", uilog_seen); \
|
||||
} \
|
||||
DLOG("UI bridge: " STR(name)); \
|
||||
uilog_seen = 0; \
|
||||
uilog_event = ui_bridge_##name##_event; \
|
||||
} \
|
||||
((UIBridgeData *)ui)->scheduler( \
|
||||
event_create(1, ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui)); \
|
||||
} while (0)
|
||||
#else
|
||||
// Schedule a function call on the UI bridge thread.
|
||||
#define UI_CALL(ui, name, argc, ...) \
|
||||
((UIBridgeData *)ui)->scheduler( \
|
||||
event_create(1, ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui))
|
||||
#endif
|
||||
|
||||
#define INT2PTR(i) ((void *)(uintptr_t)i)
|
||||
#define PTR2INT(p) ((int)(uintptr_t)p)
|
||||
@ -218,16 +239,16 @@ static void ui_bridge_mode_change_event(void **argv)
|
||||
}
|
||||
|
||||
static void ui_bridge_set_scroll_region(UI *b, int top, int bot, int left,
|
||||
int right)
|
||||
int right)
|
||||
{
|
||||
UI_CALL(b, set_scroll_region, 5, b, INT2PTR(top), INT2PTR(bot),
|
||||
INT2PTR(left), INT2PTR(right));
|
||||
INT2PTR(left), INT2PTR(right));
|
||||
}
|
||||
static void ui_bridge_set_scroll_region_event(void **argv)
|
||||
{
|
||||
UI *ui = UI(argv[0]);
|
||||
ui->set_scroll_region(ui, PTR2INT(argv[1]), PTR2INT(argv[2]),
|
||||
PTR2INT(argv[3]), PTR2INT(argv[4]));
|
||||
PTR2INT(argv[3]), PTR2INT(argv[4]));
|
||||
}
|
||||
|
||||
static void ui_bridge_scroll(UI *b, int count)
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Bridge for communication between a UI thread and nvim core.
|
||||
// Used by the built-in TUI and external libnvim-based UIs.
|
||||
// Used by the built-in TUI and libnvim-based UIs.
|
||||
#ifndef NVIM_UI_BRIDGE_H
|
||||
#define NVIM_UI_BRIDGE_H
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "nvim/iconv.h"
|
||||
#include "nvim/version.h"
|
||||
#include "nvim/charset.h"
|
||||
#include "nvim/macros.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/message.h"
|
||||
@ -22,9 +23,6 @@
|
||||
// version info generated by the build system
|
||||
#include "auto/versiondef.h"
|
||||
|
||||
#define STR_(x) #x
|
||||
#define STR(x) STR_(x)
|
||||
|
||||
// for ":version", ":intro", and "nvim --version"
|
||||
#ifndef NVIM_VERSION_MEDIUM
|
||||
#define NVIM_VERSION_MEDIUM STR(NVIM_VERSION_MAJOR) "." STR(NVIM_VERSION_MINOR)\
|
||||
|
@ -12,16 +12,16 @@ local feed = helpers.feed
|
||||
describe('execute()', function()
|
||||
before_each(clear)
|
||||
|
||||
it('returns the same result with :redir', function()
|
||||
it('captures the same result as :redir', function()
|
||||
eq(redir_exec('messages'), funcs.execute('messages'))
|
||||
end)
|
||||
|
||||
it('returns the output of the commands if the argument is List', function()
|
||||
it('captures the concatenated outputs of a List of commands', function()
|
||||
eq("foobar", funcs.execute({'echon "foo"', 'echon "bar"'}))
|
||||
eq("\nfoo\nbar", funcs.execute({'echo "foo"', 'echo "bar"'}))
|
||||
end)
|
||||
|
||||
it('supports the nested redirection', function()
|
||||
it('supports nested redirection', function()
|
||||
source([[
|
||||
function! g:Foo()
|
||||
let a = ''
|
||||
@ -43,17 +43,17 @@ describe('execute()', function()
|
||||
eq('42', funcs.execute([[echon execute("echon execute('echon 42')")]]))
|
||||
end)
|
||||
|
||||
it('returns the transformed string', function()
|
||||
it('captures a transformed string', function()
|
||||
eq('^A', funcs.execute('echon "\\<C-a>"'))
|
||||
end)
|
||||
|
||||
it('returns the empty string if the argument list is empty', function()
|
||||
it('returns empty string if the argument list is empty', function()
|
||||
eq('', funcs.execute({}))
|
||||
eq(0, exc_exec('let g:ret = execute(v:_null_list)'))
|
||||
eq('', eval('g:ret'))
|
||||
end)
|
||||
|
||||
it('returns the errors', function()
|
||||
it('captures errors', function()
|
||||
local ret
|
||||
ret = exc_exec('call execute(0.0)')
|
||||
eq('Vim(call):E806: using Float as a String', ret)
|
||||
@ -69,6 +69,11 @@ describe('execute()', function()
|
||||
eq('Vim(call):E729: using Funcref as a String', ret)
|
||||
end)
|
||||
|
||||
-- This matches Vim behavior.
|
||||
it('does not capture shell-command output', function()
|
||||
eq('\n:!echo "foo"\13\n', funcs.execute('!echo "foo"'))
|
||||
end)
|
||||
|
||||
it('silences command run inside', function()
|
||||
local screen = Screen.new(40, 5)
|
||||
screen:attach()
|
||||
|
@ -51,6 +51,7 @@ local function screen_setup(extra_height, command)
|
||||
[7] = {foreground = 130},
|
||||
[8] = {foreground = 15, background = 1}, -- error message
|
||||
[9] = {foreground = 4},
|
||||
[10] = {foreground = 2}, -- "Press ENTER" in embedded :terminal session.
|
||||
})
|
||||
|
||||
screen:attach({rgb=false})
|
||||
|
@ -39,4 +39,25 @@ describe("shell command :!", function()
|
||||
{3:-- TERMINAL --} |
|
||||
]])
|
||||
end)
|
||||
|
||||
it("throttles shell-command output greater than ~10KB", function()
|
||||
screen.timeout = 20000 -- Avoid false failure on slow systems.
|
||||
child_session.feed_data(
|
||||
":!for i in $(seq 2 3000); do echo XXXXXXXXXX $i; done\n")
|
||||
|
||||
-- If we observe any line starting with a dot, then throttling occurred.
|
||||
screen:expect("\n.", nil, nil, nil, true)
|
||||
|
||||
-- Final chunk of output should always be displayed, never skipped.
|
||||
-- (Throttling is non-deterministic, this test is merely a sanity check.)
|
||||
screen:expect([[
|
||||
XXXXXXXXXX 2996 |
|
||||
XXXXXXXXXX 2997 |
|
||||
XXXXXXXXXX 2998 |
|
||||
XXXXXXXXXX 2999 |
|
||||
XXXXXXXXXX 3000 |
|
||||
{10:Press ENTER or type command to continue}{1: } |
|
||||
{3:-- TERMINAL --} |
|
||||
]])
|
||||
end)
|
||||
end)
|
||||
|
@ -126,7 +126,7 @@ end
|
||||
do
|
||||
local spawn, nvim_prog = helpers.spawn, helpers.nvim_prog
|
||||
local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--embed'})
|
||||
local status, rv = session:request('vim_get_color_map')
|
||||
local status, rv = session:request('nvim_get_color_map')
|
||||
if not status then
|
||||
print('failed to get color map')
|
||||
os.exit(1)
|
||||
@ -207,7 +207,15 @@ function Screen:try_resize(columns, rows)
|
||||
uimeths.try_resize(columns, rows)
|
||||
end
|
||||
|
||||
function Screen:expect(expected, attr_ids, attr_ignore, condition)
|
||||
-- Asserts that `expected` eventually matches the screen state.
|
||||
--
|
||||
-- expected: Expected screen state (string).
|
||||
-- attr_ids: Text attribute definitions.
|
||||
-- attr_ignore: Ignored text attributes.
|
||||
-- condition: Function asserting some arbitrary condition.
|
||||
-- any: true: Succeed if `expected` matches ANY screen line(s).
|
||||
-- false (default): `expected` must match screen exactly.
|
||||
function Screen:expect(expected, attr_ids, attr_ignore, condition, any)
|
||||
-- remove the last line and dedent
|
||||
expected = dedent(expected:gsub('\n[ ]+$', ''))
|
||||
local expected_rows = {}
|
||||
@ -229,21 +237,34 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition)
|
||||
for i = 1, self._height do
|
||||
actual_rows[i] = self:_row_repr(self._rows[i], ids, ignore)
|
||||
end
|
||||
for i = 1, self._height do
|
||||
if expected_rows[i] ~= actual_rows[i] then
|
||||
local msg_expected_rows = {}
|
||||
for j = 1, #expected_rows do
|
||||
msg_expected_rows[j] = expected_rows[j]
|
||||
end
|
||||
msg_expected_rows[i] = '*' .. msg_expected_rows[i]
|
||||
actual_rows[i] = '*' .. actual_rows[i]
|
||||
|
||||
if any then
|
||||
-- Search for `expected` anywhere in the screen lines.
|
||||
local actual_screen_str = table.concat(actual_rows, '\n')
|
||||
if nil == string.find(actual_screen_str, expected) then
|
||||
return (
|
||||
'Row ' .. tostring(i) .. ' didn\'t match.\n'
|
||||
.. 'Expected:\n|' .. table.concat(msg_expected_rows, '|\n|') .. '|\n'
|
||||
.. 'Actual:\n|' .. table.concat(actual_rows, '|\n|') .. '|\n\n' .. [[
|
||||
'Failed to match any screen lines.\n'
|
||||
.. 'Expected (anywhere): "' .. expected .. '"\n'
|
||||
.. 'Actual:\n |' .. table.concat(actual_rows, '|\n |') .. '|\n\n')
|
||||
end
|
||||
else
|
||||
-- `expected` must match the screen lines exactly.
|
||||
for i = 1, self._height do
|
||||
if expected_rows[i] ~= actual_rows[i] then
|
||||
local msg_expected_rows = {}
|
||||
for j = 1, #expected_rows do
|
||||
msg_expected_rows[j] = expected_rows[j]
|
||||
end
|
||||
msg_expected_rows[i] = '*' .. msg_expected_rows[i]
|
||||
actual_rows[i] = '*' .. actual_rows[i]
|
||||
return (
|
||||
'Row ' .. tostring(i) .. ' did not match.\n'
|
||||
..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n'
|
||||
..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[
|
||||
To print the expect() call that would assert the current screen state, use
|
||||
screen:snaphot_util(). In case of non-deterministic failures, use
|
||||
screen:redraw_debug() to show all intermediate screen states. ]])
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
@ -36,6 +36,27 @@ describe("multiqueue (multi-level event-queue)", function()
|
||||
put(child3, 'c3i2')
|
||||
end)
|
||||
|
||||
it('keeps count of added events', function()
|
||||
eq(3, multiqueue.multiqueue_size(child1))
|
||||
eq(4, multiqueue.multiqueue_size(child2))
|
||||
eq(2, multiqueue.multiqueue_size(child3))
|
||||
end)
|
||||
|
||||
it('keeps count of removed events', function()
|
||||
multiqueue.multiqueue_get(child1)
|
||||
eq(2, multiqueue.multiqueue_size(child1))
|
||||
multiqueue.multiqueue_get(child1)
|
||||
eq(1, multiqueue.multiqueue_size(child1))
|
||||
multiqueue.multiqueue_get(child1)
|
||||
eq(0, multiqueue.multiqueue_size(child1))
|
||||
put(child1, 'c2ixx')
|
||||
eq(1, multiqueue.multiqueue_size(child1))
|
||||
multiqueue.multiqueue_get(child1)
|
||||
eq(0, multiqueue.multiqueue_size(child1))
|
||||
multiqueue.multiqueue_get(child1)
|
||||
eq(0, multiqueue.multiqueue_size(child1))
|
||||
end)
|
||||
|
||||
it('removing from parent removes from child', function()
|
||||
eq('c1i1', get(parent))
|
||||
eq('c1i2', get(parent))
|
Loading…
Reference in New Issue
Block a user