mirror of
https://github.com/neovim/neovim.git
synced 2026-02-11 06:18:50 +00:00
448 lines
11 KiB
C
448 lines
11 KiB
C
#include <assert.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
|
|
#include <uv.h>
|
|
|
|
#include "nvim/api/private/defs.h"
|
|
#include "nvim/os/input.h"
|
|
#include "nvim/os/event.h"
|
|
#include "nvim/os/rstream_defs.h"
|
|
#include "nvim/os/rstream.h"
|
|
#include "nvim/ascii.h"
|
|
#include "nvim/vim.h"
|
|
#include "nvim/ui.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/keymap.h"
|
|
#include "nvim/mbyte.h"
|
|
#include "nvim/fileio.h"
|
|
#include "nvim/ex_cmds2.h"
|
|
#include "nvim/getchar.h"
|
|
#include "nvim/term.h"
|
|
#include "nvim/main.h"
|
|
#include "nvim/misc1.h"
|
|
|
|
#define READ_BUFFER_SIZE 0xfff
|
|
#define INPUT_BUFFER_SIZE (READ_BUFFER_SIZE * 4)
|
|
|
|
typedef enum {
|
|
kInputNone,
|
|
kInputAvail,
|
|
kInputEof
|
|
} InbufPollResult;
|
|
|
|
static RStream *read_stream;
|
|
static RBuffer *read_buffer, *input_buffer;
|
|
static bool eof = false, started_reading = false;
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "os/input.c.generated.h"
|
|
#endif
|
|
// Helper function used to push bytes from the 'event' key sequence partially
|
|
// between calls to os_inchar when maxlen < 3
|
|
|
|
void input_init(void)
|
|
{
|
|
input_buffer = rbuffer_new(INPUT_BUFFER_SIZE + MAX_KEY_CODE_LEN);
|
|
|
|
if (abstract_ui) {
|
|
return;
|
|
}
|
|
|
|
read_buffer = rbuffer_new(READ_BUFFER_SIZE);
|
|
read_stream = rstream_new(read_cb, read_buffer, NULL);
|
|
rstream_set_file(read_stream, read_cmd_fd);
|
|
}
|
|
|
|
void input_teardown(void)
|
|
{
|
|
if (abstract_ui) {
|
|
return;
|
|
}
|
|
|
|
rstream_free(read_stream);
|
|
}
|
|
|
|
// Listen for input
|
|
void input_start(void)
|
|
{
|
|
if (abstract_ui) {
|
|
return;
|
|
}
|
|
|
|
rstream_start(read_stream);
|
|
}
|
|
|
|
// Stop listening for input
|
|
void input_stop(void)
|
|
{
|
|
if (abstract_ui) {
|
|
return;
|
|
}
|
|
|
|
rstream_stop(read_stream);
|
|
}
|
|
|
|
// Low level input function.
|
|
int os_inchar(uint8_t *buf, int maxlen, int ms, int tb_change_cnt)
|
|
{
|
|
if (rbuffer_pending(input_buffer)) {
|
|
return (int)rbuffer_read(input_buffer, (char *)buf, (size_t)maxlen);
|
|
}
|
|
|
|
InbufPollResult result;
|
|
if (ms >= 0) {
|
|
if ((result = inbuf_poll(ms)) == kInputNone) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if ((result = inbuf_poll((int)p_ut)) == kInputNone) {
|
|
if (trigger_cursorhold() && maxlen >= 3
|
|
&& !typebuf_changed(tb_change_cnt)) {
|
|
buf[0] = K_SPECIAL;
|
|
buf[1] = KS_EXTRA;
|
|
buf[2] = KE_CURSORHOLD;
|
|
return 3;
|
|
}
|
|
|
|
before_blocking();
|
|
result = inbuf_poll(-1);
|
|
}
|
|
}
|
|
|
|
// If input was put directly in typeahead buffer bail out here.
|
|
if (typebuf_changed(tb_change_cnt)) {
|
|
return 0;
|
|
}
|
|
|
|
if (rbuffer_pending(input_buffer)) {
|
|
// Safe to convert rbuffer_read to int, it will never overflow since we use
|
|
// relatively small buffers.
|
|
return (int)rbuffer_read(input_buffer, (char *)buf, (size_t)maxlen);
|
|
}
|
|
|
|
// If there are deferred events, return the keys directly
|
|
if (event_has_deferred()) {
|
|
return push_event_key(buf, maxlen);
|
|
}
|
|
|
|
if (result == kInputEof) {
|
|
read_error_exit();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Check if a character is available for reading
|
|
bool os_char_avail(void)
|
|
{
|
|
return inbuf_poll(0) == kInputAvail;
|
|
}
|
|
|
|
// Check for CTRL-C typed by reading all available characters.
|
|
// In cooked mode we should get SIGINT, no need to check.
|
|
void os_breakcheck(void)
|
|
{
|
|
if (curr_tmode == TMODE_RAW)
|
|
input_poll(0);
|
|
}
|
|
|
|
/// Test whether a file descriptor refers to a terminal.
|
|
///
|
|
/// @param fd File descriptor.
|
|
/// @return `true` if file descriptor refers to a terminal.
|
|
bool os_isatty(int fd)
|
|
{
|
|
return uv_guess_handle(fd) == UV_TTY;
|
|
}
|
|
|
|
/// Return the contents of the input buffer and make it empty. The returned
|
|
/// pointer must be passed to `input_buffer_restore()` later.
|
|
String input_buffer_save(void)
|
|
{
|
|
size_t inbuf_size = rbuffer_pending(input_buffer);
|
|
String rv = {
|
|
.data = xmemdup(rbuffer_read_ptr(input_buffer), inbuf_size),
|
|
.size = inbuf_size
|
|
};
|
|
rbuffer_consumed(input_buffer, inbuf_size);
|
|
return rv;
|
|
}
|
|
|
|
/// Restore the contents of the input buffer and free `str`
|
|
void input_buffer_restore(String str)
|
|
{
|
|
rbuffer_consumed(input_buffer, rbuffer_pending(input_buffer));
|
|
rbuffer_write(input_buffer, str.data, str.size);
|
|
free(str.data);
|
|
}
|
|
|
|
size_t input_enqueue(String keys)
|
|
{
|
|
char *ptr = keys.data, *end = ptr + keys.size;
|
|
|
|
while (rbuffer_available(input_buffer) >= 6 && ptr < end) {
|
|
uint8_t buf[6] = {0};
|
|
int new_size = trans_special((uint8_t **)&ptr, buf, false);
|
|
|
|
if (!new_size) {
|
|
// copy the character unmodified
|
|
*buf = (uint8_t)*ptr++;
|
|
new_size = 1;
|
|
}
|
|
|
|
new_size = handle_mouse_event(&ptr, buf, new_size);
|
|
// TODO(tarruda): Don't produce past unclosed '<' characters, except if
|
|
// there's a lot of characters after the '<'
|
|
rbuffer_write(input_buffer, (char *)buf, (size_t)new_size);
|
|
}
|
|
|
|
size_t rv = (size_t)(ptr - keys.data);
|
|
process_interrupts();
|
|
return rv;
|
|
}
|
|
|
|
// Mouse event handling code(Extract row/col if available and detect multiple
|
|
// clicks)
|
|
static int handle_mouse_event(char **ptr, uint8_t *buf, int bufsize)
|
|
{
|
|
int mouse_code = 0;
|
|
|
|
if (bufsize == 3) {
|
|
mouse_code = buf[2];
|
|
} else if (bufsize == 6) {
|
|
// prefixed with K_SPECIAL KS_MODIFIER mod
|
|
mouse_code = buf[5];
|
|
}
|
|
|
|
if (mouse_code < KE_LEFTMOUSE || mouse_code > KE_RIGHTRELEASE) {
|
|
return bufsize;
|
|
}
|
|
|
|
// a <[COL],[ROW]> sequence can follow and will set the mouse_row/mouse_col
|
|
// global variables. This is ugly but its how the rest of the code expects to
|
|
// find mouse coordinates, and it would be too expensive to refactor this
|
|
// now.
|
|
int col, row, advance;
|
|
if (sscanf(*ptr, "<%d,%d>%n", &col, &row, &advance)) {
|
|
if (col >= 0 && row >= 0) {
|
|
mouse_row = row;
|
|
mouse_col = col;
|
|
}
|
|
*ptr += advance;
|
|
}
|
|
|
|
static int orig_num_clicks = 0;
|
|
static int orig_mouse_code = 0;
|
|
static int orig_mouse_col = 0;
|
|
static int orig_mouse_row = 0;
|
|
static uint64_t orig_mouse_time = 0; // time of previous mouse click
|
|
uint64_t mouse_time = os_hrtime(); // time of current mouse click
|
|
|
|
// compute the time elapsed since the previous mouse click and
|
|
// convert p_mouse from ms to ns
|
|
uint64_t timediff = mouse_time - orig_mouse_time;
|
|
uint64_t mouset = (uint64_t)p_mouset * 1000000;
|
|
if (mouse_code == orig_mouse_code
|
|
&& timediff < mouset
|
|
&& orig_num_clicks != 4
|
|
&& orig_mouse_col == mouse_col
|
|
&& orig_mouse_row == mouse_row) {
|
|
orig_num_clicks++;
|
|
} else {
|
|
orig_num_clicks = 1;
|
|
}
|
|
orig_mouse_code = mouse_code;
|
|
orig_mouse_col = mouse_col;
|
|
orig_mouse_row = mouse_row;
|
|
orig_mouse_time = mouse_time;
|
|
|
|
int modifiers = 0;
|
|
if (orig_num_clicks == 2) {
|
|
modifiers |= MOD_MASK_2CLICK;
|
|
} else if (orig_num_clicks == 3) {
|
|
modifiers |= MOD_MASK_3CLICK;
|
|
} else if (orig_num_clicks == 4) {
|
|
modifiers |= MOD_MASK_4CLICK;
|
|
}
|
|
|
|
if (modifiers) {
|
|
if (buf[1] != KS_MODIFIER) {
|
|
// no modifiers in the buffer yet, shift the bytes 3 positions
|
|
memcpy(buf + 3, buf, 3);
|
|
// add the modifier sequence
|
|
buf[0] = K_SPECIAL;
|
|
buf[1] = KS_MODIFIER;
|
|
buf[2] = (uint8_t)modifiers;
|
|
bufsize += 3;
|
|
} else {
|
|
buf[2] |= (uint8_t)modifiers;
|
|
}
|
|
}
|
|
|
|
return bufsize;
|
|
}
|
|
|
|
static bool input_poll(int ms)
|
|
{
|
|
if (do_profiling == PROF_YES && ms) {
|
|
prof_inchar_enter();
|
|
}
|
|
|
|
event_poll_until(ms, input_ready());
|
|
|
|
if (do_profiling == PROF_YES && ms) {
|
|
prof_inchar_exit();
|
|
}
|
|
|
|
return input_ready();
|
|
}
|
|
|
|
// This is a replacement for the old `WaitForChar` function in os_unix.c
|
|
static InbufPollResult inbuf_poll(int ms)
|
|
{
|
|
if (typebuf_was_filled || rbuffer_pending(input_buffer)) {
|
|
return kInputAvail;
|
|
}
|
|
|
|
if (input_poll(ms)) {
|
|
return eof && rstream_pending(read_stream) == 0 ?
|
|
kInputEof :
|
|
kInputAvail;
|
|
}
|
|
|
|
return kInputNone;
|
|
}
|
|
|
|
static void stderr_switch(void)
|
|
{
|
|
int mode = cur_tmode;
|
|
// We probably set the wrong file descriptor to raw mode. Switch back to
|
|
// cooked mode
|
|
settmode(TMODE_COOK);
|
|
// Stop the idle handle
|
|
rstream_stop(read_stream);
|
|
// Use stderr for stdin, also works for shell commands.
|
|
read_cmd_fd = 2;
|
|
// Initialize and start the input stream
|
|
rstream_set_file(read_stream, read_cmd_fd);
|
|
rstream_start(read_stream);
|
|
// Set the mode back to what it was
|
|
settmode(mode);
|
|
}
|
|
|
|
static void read_cb(RStream *rstream, void *data, bool at_eof)
|
|
{
|
|
if (at_eof) {
|
|
if (!started_reading
|
|
&& rstream_is_regular_file(rstream)
|
|
&& os_isatty(STDERR_FILENO)) {
|
|
// Read error. Since stderr is a tty we switch to reading from it. This
|
|
// is for handling for cases like "foo | xargs vim" because xargs
|
|
// redirects stdin from /dev/null. Previously, this was done in ui.c
|
|
stderr_switch();
|
|
} else {
|
|
eof = true;
|
|
}
|
|
}
|
|
|
|
convert_input();
|
|
process_interrupts();
|
|
started_reading = true;
|
|
}
|
|
|
|
static void convert_input(void)
|
|
{
|
|
if (abstract_ui || !rbuffer_available(input_buffer)) {
|
|
// No input buffer space
|
|
return;
|
|
}
|
|
|
|
bool convert = input_conv.vc_type != CONV_NONE;
|
|
// Set unconverted data/length
|
|
char *data = rbuffer_read_ptr(read_buffer);
|
|
size_t data_length = rbuffer_pending(read_buffer);
|
|
size_t converted_length = data_length;
|
|
|
|
if (convert) {
|
|
// Perform input conversion according to `input_conv`
|
|
size_t unconverted_length = 0;
|
|
data = (char *)string_convert_ext(&input_conv,
|
|
(uint8_t *)data,
|
|
(int *)&converted_length,
|
|
(int *)&unconverted_length);
|
|
data_length -= unconverted_length;
|
|
}
|
|
|
|
// The conversion code will be gone eventually, for now assume `input_buffer`
|
|
// always has space for the converted data(it's many times the size of
|
|
// `read_buffer`, so it's hard to imagine a scenario where the converted data
|
|
// doesn't fit)
|
|
assert(converted_length <= rbuffer_available(input_buffer));
|
|
// Write processed data to input buffer.
|
|
(void)rbuffer_write(input_buffer, data, converted_length);
|
|
// Adjust raw buffer pointers
|
|
rbuffer_consumed(read_buffer, data_length);
|
|
|
|
if (convert) {
|
|
// data points to memory allocated by `string_convert_ext`, free it.
|
|
free(data);
|
|
}
|
|
}
|
|
|
|
static void process_interrupts(void)
|
|
{
|
|
if (mapped_ctrl_c) {
|
|
return;
|
|
}
|
|
|
|
char *inbuf = rbuffer_read_ptr(input_buffer);
|
|
size_t count = rbuffer_pending(input_buffer), consume_count = 0;
|
|
|
|
for (int i = (int)count - 1; i >= 0; i--) {
|
|
if (inbuf[i] == 3) {
|
|
got_int = true;
|
|
consume_count = (size_t)i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (got_int) {
|
|
// Remove everything typed before the CTRL-C
|
|
rbuffer_consumed(input_buffer, consume_count);
|
|
}
|
|
}
|
|
|
|
static int push_event_key(uint8_t *buf, int maxlen)
|
|
{
|
|
static const uint8_t key[3] = { K_SPECIAL, KS_EXTRA, KE_EVENT };
|
|
static int key_idx = 0;
|
|
int buf_idx = 0;
|
|
|
|
do {
|
|
buf[buf_idx++] = key[key_idx++];
|
|
key_idx %= 3;
|
|
} while (key_idx > 0 && buf_idx < maxlen);
|
|
|
|
return buf_idx;
|
|
}
|
|
|
|
// Check if there's pending input
|
|
static bool input_ready(void)
|
|
{
|
|
return typebuf_was_filled || // API call filled typeahead
|
|
rbuffer_pending(input_buffer) > 0 || // Stdin input
|
|
event_has_deferred() || // Events must be processed
|
|
(!abstract_ui && eof); // Stdin closed
|
|
}
|
|
|
|
// Exit because of an input read error.
|
|
static void read_error_exit(void)
|
|
{
|
|
if (silent_mode) /* Normal way to exit for "ex -s" */
|
|
getout(0);
|
|
STRCPY(IObuff, _("Vim: Error reading input, exiting...\n"));
|
|
preserve_exit();
|
|
}
|