1
0
mirror of https://github.com/janet-lang/janet synced 2024-11-15 13:14:48 +00:00
janet/src/mainclient/line.c

707 lines
20 KiB
C
Raw Normal View History

2018-03-14 03:39:49 +00:00
/*
2020-01-12 16:50:37 +00:00
* Copyright (c) 2020 Calvin Rose
2018-03-14 03:39:49 +00:00
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
2019-12-31 01:24:40 +00:00
#if !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 200112L
#endif
#ifndef JANET_AMALG
2018-03-14 03:39:49 +00:00
#include "line.h"
#endif
2018-03-14 03:39:49 +00:00
static JANET_THREAD_LOCAL JanetTable *gbl_complete_env;
2018-03-14 03:39:49 +00:00
/* Common */
Janet janet_line_getter(int32_t argc, Janet *argv) {
janet_arity(argc, 0, 3);
2019-01-31 17:34:22 +00:00
const char *str = (argc >= 1) ? (const char *) janet_getstring(argv, 0) : "";
JanetBuffer *buf = (argc >= 2) ? janet_getbuffer(argv, 1) : janet_buffer(10);
gbl_complete_env = (argc >= 3) ? janet_gettable(argv, 2) : NULL;
janet_line_get(str, buf);
gbl_complete_env = NULL;
2019-01-31 17:34:22 +00:00
return janet_wrap_buffer(buf);
2018-03-14 03:39:49 +00:00
}
2018-09-06 02:18:42 +00:00
static void simpleline(JanetBuffer *buffer) {
FILE *in = janet_dynfile("in", stdin);
2018-03-14 03:39:49 +00:00
buffer->count = 0;
2018-11-18 19:17:50 +00:00
int c;
2018-03-14 03:39:49 +00:00
for (;;) {
c = fgetc(in);
if (feof(in) || c < 0) {
break;
}
2018-09-06 02:18:42 +00:00
janet_buffer_push_u8(buffer, (uint8_t) c);
2018-03-14 03:39:49 +00:00
if (c == '\n') break;
}
}
/* Windows */
2018-09-06 02:18:42 +00:00
#ifdef JANET_WINDOWS
2018-03-14 03:39:49 +00:00
2018-09-06 02:18:42 +00:00
void janet_line_init() {
2018-03-14 03:39:49 +00:00
;
}
2018-09-06 02:18:42 +00:00
void janet_line_deinit() {
2018-03-14 03:39:49 +00:00
;
}
2019-01-31 17:34:22 +00:00
void janet_line_get(const char *p, JanetBuffer *buffer) {
FILE *out = janet_dynfile("err", stderr);
fputs(p, out);
2019-06-18 04:04:29 +00:00
fflush(out);
2018-03-14 03:39:49 +00:00
simpleline(buffer);
}
/* Posix */
#else
/*
https://github.com/antirez/linenoise/blob/master/linenoise.c
*/
#include <janet.h>
2018-03-14 03:39:49 +00:00
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <unistd.h>
2018-03-18 13:13:21 +00:00
#include <string.h>
2018-06-24 17:17:56 +00:00
#include <signal.h>
2018-03-14 03:39:49 +00:00
/* static state */
2018-09-06 02:18:42 +00:00
#define JANET_LINE_MAX 1024
#define JANET_MATCH_MAX 256
2018-09-06 02:18:42 +00:00
#define JANET_HISTORY_MAX 100
static JANET_THREAD_LOCAL int gbl_israwmode = 0;
static JANET_THREAD_LOCAL const char *gbl_prompt = "> ";
static JANET_THREAD_LOCAL int gbl_plen = 2;
static JANET_THREAD_LOCAL char gbl_buf[JANET_LINE_MAX];
static JANET_THREAD_LOCAL int gbl_len = 0;
static JANET_THREAD_LOCAL int gbl_pos = 0;
static JANET_THREAD_LOCAL int gbl_cols = 80;
static JANET_THREAD_LOCAL char *gbl_history[JANET_HISTORY_MAX];
static JANET_THREAD_LOCAL int gbl_history_count = 0;
static JANET_THREAD_LOCAL int gbl_historyi = 0;
static JANET_THREAD_LOCAL int gbl_sigint_flag = 0;
static JANET_THREAD_LOCAL struct termios gbl_termios_start;
static JANET_THREAD_LOCAL JanetByteView gbl_matches[JANET_MATCH_MAX];
static JANET_THREAD_LOCAL int gbl_match_count = 0;
2018-03-14 03:39:49 +00:00
/* Unsupported terminal list from linenoise */
static const char *badterms[] = {
"cons25",
"dumb",
"emacs",
NULL
2018-03-14 03:39:49 +00:00
};
2018-03-18 13:13:21 +00:00
static char *sdup(const char *s) {
size_t len = strlen(s) + 1;
char *mem = malloc(len);
if (!mem) {
return NULL;
}
return memcpy(mem, s, len);
}
2018-03-14 03:39:49 +00:00
/* Ansi terminal raw mode */
static int rawmode(void) {
struct termios t;
2018-03-14 03:39:49 +00:00
if (!isatty(STDIN_FILENO)) goto fatal;
if (tcgetattr(STDIN_FILENO, &gbl_termios_start) == -1) goto fatal;
t = gbl_termios_start;
t.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
t.c_cflag |= (CS8);
t.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t) < 0) goto fatal;
gbl_israwmode = 1;
2018-03-14 03:39:49 +00:00
return 0;
fatal:
errno = ENOTTY;
return -1;
}
/* Disable raw mode */
static void norawmode(void) {
if (gbl_israwmode && tcsetattr(STDIN_FILENO, TCSAFLUSH, &gbl_termios_start) != -1)
gbl_israwmode = 0;
2018-03-14 03:39:49 +00:00
}
static int curpos(void) {
2018-03-14 03:39:49 +00:00
char buf[32];
int cols, rows;
unsigned int i = 0;
if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1;
while (i < sizeof(buf) - 1) {
if (read(STDIN_FILENO, buf + i, 1) != 1) break;
2018-03-14 03:39:49 +00:00
if (buf[i] == 'R') break;
i++;
}
buf[i] = '\0';
if (buf[0] != 27 || buf[1] != '[') return -1;
2018-03-14 03:39:49 +00:00
if (sscanf(buf + 2, "%d;%d", &rows, &cols) != 2) return -1;
return cols;
}
static int getcols(void) {
2018-03-14 03:39:49 +00:00
struct winsize ws;
if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
int start, cols;
start = curpos();
if (start == -1) goto failed;
if (write(STDOUT_FILENO, "\x1b[999C", 6) != 6) goto failed;
cols = curpos();
if (cols == -1) goto failed;
if (cols > start) {
char seq[32];
snprintf(seq, 32, "\x1b[%dD", cols - start);
if (write(STDOUT_FILENO, seq, strlen(seq)) == -1) {
exit(1);
}
2018-03-14 03:39:49 +00:00
}
return cols;
} else {
return ws.ws_col;
}
failed:
return 80;
}
static void clear(void) {
if (write(STDOUT_FILENO, "\x1b[H\x1b[2J", 7) <= 0) {
exit(1);
}
2018-03-14 03:39:49 +00:00
}
static void refresh(void) {
2018-03-14 03:39:49 +00:00
char seq[64];
2018-09-06 02:18:42 +00:00
JanetBuffer b;
2018-03-14 03:39:49 +00:00
/* Keep cursor position on screen */
char *_buf = gbl_buf;
int _len = gbl_len;
int _pos = gbl_pos;
while ((gbl_plen + _pos) >= gbl_cols) {
2018-03-14 03:39:49 +00:00
_buf++;
_len--;
_pos--;
}
while ((gbl_plen + _len) > gbl_cols) {
2018-03-14 03:39:49 +00:00
_len--;
}
2018-09-06 02:18:42 +00:00
janet_buffer_init(&b, 0);
/* Cursor to left edge, gbl_prompt and buffer */
2018-09-06 02:18:42 +00:00
janet_buffer_push_u8(&b, '\r');
janet_buffer_push_cstring(&b, gbl_prompt);
2018-09-06 02:18:42 +00:00
janet_buffer_push_bytes(&b, (uint8_t *) _buf, _len);
2018-03-14 03:39:49 +00:00
/* Erase to right */
2018-09-06 02:18:42 +00:00
janet_buffer_push_cstring(&b, "\x1b[0K");
2018-03-14 03:39:49 +00:00
/* Move cursor to original position. */
snprintf(seq, 64, "\r\x1b[%dC", (int)(_pos + gbl_plen));
2018-09-06 02:18:42 +00:00
janet_buffer_push_cstring(&b, seq);
if (write(STDOUT_FILENO, b.data, b.count) == -1) {
exit(1);
}
2018-09-06 02:18:42 +00:00
janet_buffer_deinit(&b);
2018-03-14 03:39:49 +00:00
}
static int insert(char c, int draw) {
if (gbl_len < JANET_LINE_MAX - 1) {
if (gbl_len == gbl_pos) {
gbl_buf[gbl_pos++] = c;
gbl_buf[++gbl_len] = '\0';
if (draw) {
if (gbl_plen + gbl_len < gbl_cols) {
/* Avoid a full update of the line in the
* trivial case. */
if (write(STDOUT_FILENO, &c, 1) == -1) return -1;
} else {
refresh();
}
2018-03-14 03:39:49 +00:00
}
} else {
memmove(gbl_buf + gbl_pos + 1, gbl_buf + gbl_pos, gbl_len - gbl_pos);
gbl_buf[gbl_pos++] = c;
gbl_buf[++gbl_len] = '\0';
if (draw) refresh();
2018-03-14 03:39:49 +00:00
}
}
return 0;
}
static void historymove(int delta) {
if (gbl_history_count > 1) {
free(gbl_history[gbl_historyi]);
gbl_history[gbl_historyi] = sdup(gbl_buf);
2018-03-14 03:39:49 +00:00
gbl_historyi += delta;
if (gbl_historyi < 0) {
gbl_historyi = 0;
2018-03-14 03:39:49 +00:00
return;
} else if (gbl_historyi >= gbl_history_count) {
gbl_historyi = gbl_history_count - 1;
2018-03-14 03:39:49 +00:00
return;
}
strncpy(gbl_buf, gbl_history[gbl_historyi], JANET_LINE_MAX - 1);
gbl_pos = gbl_len = strlen(gbl_buf);
gbl_buf[gbl_len] = '\0';
2018-03-14 03:39:49 +00:00
refresh();
}
}
static void addhistory(void) {
2018-03-14 03:39:49 +00:00
int i, len;
char *newline = sdup(gbl_buf);
2018-03-14 03:39:49 +00:00
if (!newline) return;
len = gbl_history_count;
2018-09-06 02:18:42 +00:00
if (len < JANET_HISTORY_MAX) {
gbl_history[gbl_history_count++] = newline;
2018-03-14 03:39:49 +00:00
len++;
} else {
free(gbl_history[JANET_HISTORY_MAX - 1]);
2018-03-14 03:39:49 +00:00
}
for (i = len - 1; i > 0; i--) {
gbl_history[i] = gbl_history[i - 1];
2018-03-14 03:39:49 +00:00
}
gbl_history[0] = newline;
2018-03-14 03:39:49 +00:00
}
static void replacehistory(void) {
/* History count is always > 0 here */
if (gbl_len == 0 || (gbl_history_count > 1 && !strcmp(gbl_buf, gbl_history[1]))) {
/* Delete history */
free(gbl_history[0]);
for (int i = 1; i < gbl_history_count; i++) {
gbl_history[i - 1] = gbl_history[i];
}
gbl_history_count--;
} else {
char *newline = sdup(gbl_buf);
if (!newline) return;
free(gbl_history[0]);
gbl_history[0] = newline;
}
2018-03-14 03:39:49 +00:00
}
static void kleft(void) {
if (gbl_pos > 0) {
gbl_pos--;
2018-03-14 03:39:49 +00:00
refresh();
}
}
static void kright(void) {
if (gbl_pos != gbl_len) {
gbl_pos++;
2018-03-14 03:39:49 +00:00
refresh();
}
}
static void kbackspace(void) {
if (gbl_pos > 0) {
memmove(gbl_buf + gbl_pos - 1, gbl_buf + gbl_pos, gbl_len - gbl_pos);
gbl_pos--;
gbl_buf[--gbl_len] = '\0';
2018-03-14 03:39:49 +00:00
refresh();
}
}
static void kdelete(void) {
2019-11-17 00:01:52 +00:00
if (gbl_pos != gbl_len) {
memmove(gbl_buf + gbl_pos, gbl_buf + gbl_pos + 1, gbl_len - gbl_pos);
gbl_buf[--gbl_len] = '\0';
refresh();
}
}
/* See tools/symchargen.c */
static int is_symbol_char_gen(uint8_t c) {
if (c & 0x80) return 1;
if (c >= 'a' && c <= 'z') return 1;
if (c >= 'A' && c <= 'Z') return 1;
if (c >= '0' && c <= '9') return 1;
return (c == '!' ||
c == '$' ||
c == '%' ||
c == '&' ||
c == '*' ||
c == '+' ||
c == '-' ||
c == '.' ||
c == '/' ||
c == ':' ||
c == '<' ||
c == '?' ||
c == '=' ||
c == '>' ||
c == '@' ||
c == '^' ||
c == '_');
}
static JanetByteView get_symprefix(void) {
/* Calculate current partial symbol. Maybe we could actually hook up the Janet
* parser here...*/
int i;
JanetByteView ret;
ret.len = 0;
for (i = gbl_pos - 1; i >= 0; i--) {
uint8_t c = (uint8_t) gbl_buf[i];
if (!is_symbol_char_gen(c)) break;
ret.len++;
}
/* Will be const for duration of match checking */
ret.bytes = (const uint8_t *)(gbl_buf + i + 1);
return ret;
}
static int compare_bytes(JanetByteView a, JanetByteView b) {
int32_t minlen = a.len < b.len ? a.len : b.len;
int result = strncmp((const char *) a.bytes, (const char *) b.bytes, minlen);
if (result) return result;
return a.len < b.len ? -1 : a.len > b.len ? 1 : 0;
}
static void check_match(JanetByteView src, const uint8_t *testsym, int32_t testlen) {
JanetByteView test;
test.bytes = testsym;
test.len = testlen;
if (src.len > test.len || strncmp((const char *) src.bytes, (const char *) test.bytes, src.len)) return;
JanetByteView mm = test;
for (int i = 0; i < gbl_match_count; i++) {
if (compare_bytes(mm, gbl_matches[i]) < 0) {
JanetByteView temp = mm;
mm = gbl_matches[i];
gbl_matches[i] = temp;
}
}
if (gbl_match_count == JANET_MATCH_MAX) return;
gbl_matches[gbl_match_count++] = mm;
}
static void check_cmatch(JanetByteView src, const char *cstr) {
check_match(src, (const uint8_t *) cstr, (int32_t) strlen(cstr));
}
static JanetByteView longest_common_prefix(void) {
JanetByteView bv;
if (gbl_match_count == 0) {
bv.len = 0;
bv.bytes = NULL;
} else {
bv = gbl_matches[0];
for (int i = 0; i < gbl_match_count; i++) {
JanetByteView other = gbl_matches[i];
int32_t minlen = other.len < bv.len ? other.len : bv.len;
for (bv.len = 0; bv.len < minlen; bv.len++) {
if (bv.bytes[bv.len] != other.bytes[bv.len]) {
break;
}
}
}
}
return bv;
}
static void check_specials(JanetByteView src) {
check_cmatch(src, "break");
check_cmatch(src, "def");
check_cmatch(src, "do");
check_cmatch(src, "fn");
check_cmatch(src, "if");
check_cmatch(src, "quasiquote");
check_cmatch(src, "quote");
check_cmatch(src, "set");
check_cmatch(src, "splice");
check_cmatch(src, "unquote");
check_cmatch(src, "var");
check_cmatch(src, "while");
}
static void kshowcomp(void) {
JanetTable *env = gbl_complete_env;
if (env == NULL) {
insert(' ', 0);
insert(' ', 0);
return;
}
/* Advance while on symbol char */
while (is_symbol_char_gen(gbl_buf[gbl_pos]))
gbl_pos++;
JanetByteView prefix = get_symprefix();
if (prefix.len == 0) return;
/* Find all matches */
gbl_match_count = 0;
while (NULL != env) {
JanetKV *kvend = env->data + env->capacity;
for (JanetKV *kv = env->data; kv < kvend; kv++) {
if (!janet_checktype(kv->key, JANET_SYMBOL)) continue;
const uint8_t *sym = janet_unwrap_symbol(kv->key);
check_match(prefix, sym, janet_string_length(sym));
}
env = env->proto;
}
check_specials(prefix);
JanetByteView lcp = longest_common_prefix();
for (int i = prefix.len; i < lcp.len; i++) {
insert(lcp.bytes[i], 0);
}
if (prefix.len != lcp.len) return;
int32_t maxlen = 0;
for (int i = 0; i < gbl_match_count; i++)
if (gbl_matches[i].len > maxlen)
maxlen = gbl_matches[i].len;
int num_cols = getcols();
if (gbl_match_count >= 2) {
norawmode();
/* Second pass, print */
int col_width = maxlen + 4;
int cols = num_cols / col_width;
if (cols == 0) cols = 1;
int current_col = 0;
for (int i = 0; i < gbl_match_count; i++) {
if (current_col == 0) putc('\n', stderr);
JanetByteView s = gbl_matches[i];
fprintf(stderr, "%s", (const char *) s.bytes);
for (int j = s.len; j < col_width; j++) {
putc(' ', stderr);
}
current_col = (current_col + 1) % cols;
}
putc('\n', stderr);
fflush(stderr);
rawmode();
}
}
2018-03-14 03:39:49 +00:00
static int line() {
gbl_cols = getcols();
gbl_plen = 0;
gbl_len = 0;
gbl_pos = 0;
while (gbl_prompt[gbl_plen]) gbl_plen++;
gbl_buf[0] = '\0';
2018-03-14 03:39:49 +00:00
addhistory();
if (write(STDOUT_FILENO, gbl_prompt, gbl_plen) == -1) return -1;
2018-03-14 03:39:49 +00:00
for (;;) {
char c;
int nread;
char seq[3];
nread = read(STDIN_FILENO, &c, 1);
if (nread <= 0) return -1;
switch (c) {
default:
if (insert(c, 1)) return -1;
break;
case 9: /* tab */
kshowcomp();
refresh();
break;
case 13: /* enter */
return 0;
case 3: /* ctrl-c */
errno = EAGAIN;
gbl_sigint_flag = 1;
return -1;
case 127: /* backspace */
case 8: /* ctrl-h */
kbackspace();
break;
case 4: /* ctrl-d, eof */
return -1;
case 1: /* ctrl-a */
gbl_pos = 0;
refresh();
break;
case 2: /* ctrl-b */
kleft();
break;
2020-01-16 04:38:06 +00:00
case 5: /* ctrl-e */
gbl_pos = gbl_len;
refresh();
2020-01-16 04:38:06 +00:00
break;
case 6: /* ctrl-f */
kright();
break;
case 21:
gbl_buf[0] = '\0';
gbl_pos = gbl_len = 0;
refresh();
break;
case 26: /* ctrl-z */
norawmode();
kill(getpid(), SIGSTOP);
rawmode();
refresh();
break;
case 12:
clear();
refresh();
break;
case 27: /* escape sequence */
/* Read the next two bytes representing the escape sequence.
* Use two calls to handle slow terminals returning the two
* chars at different times. */
if (read(STDIN_FILENO, seq, 1) == -1) break;
if (read(STDIN_FILENO, seq + 1, 1) == -1) break;
if (seq[0] == '[') {
if (seq[1] >= '0' && seq[1] <= '9') {
/* Extended escape, read additional byte. */
if (read(STDIN_FILENO, seq + 2, 1) == -1) break;
if (seq[2] == '~') {
switch (seq[1]) {
2019-11-17 00:07:15 +00:00
case '3': /* delete */
kdelete();
break;
default:
break;
}
}
} else {
switch (seq[1]) {
default:
break;
case 'A':
historymove(1);
break;
case 'B':
historymove(-1);
break;
case 'C': /* Right */
kright();
break;
case 'D': /* Left */
kleft();
break;
case 'H':
gbl_pos = 0;
refresh();
break;
case 'F':
gbl_pos = gbl_len;
refresh();
break;
2018-03-14 03:39:49 +00:00
}
}
} else if (seq[0] == 'O') {
2018-03-14 03:39:49 +00:00
switch (seq[1]) {
default:
break;
case 'H':
gbl_pos = 0;
refresh();
break;
case 'F':
gbl_pos = gbl_len;
refresh();
break;
2018-03-14 03:39:49 +00:00
}
}
break;
2018-03-14 03:39:49 +00:00
}
}
return 0;
}
2018-09-06 02:18:42 +00:00
void janet_line_init() {
2018-03-14 03:39:49 +00:00
;
}
2018-09-06 02:18:42 +00:00
void janet_line_deinit() {
int i;
2018-03-14 03:39:49 +00:00
norawmode();
for (i = 0; i < gbl_history_count; i++)
free(gbl_history[i]);
gbl_historyi = 0;
}
static int checktermsupport() {
const char *t = getenv("TERM");
int i;
if (!t) return 1;
for (i = 0; badterms[i]; i++)
2018-03-18 13:13:21 +00:00
if (!strcmp(t, badterms[i])) return 0;
return 1;
2018-03-14 03:39:49 +00:00
}
2019-01-31 17:34:22 +00:00
void janet_line_get(const char *p, JanetBuffer *buffer) {
gbl_prompt = p;
2018-03-14 03:39:49 +00:00
buffer->count = 0;
gbl_historyi = 0;
FILE *out = janet_dynfile("err", stderr);
if (!isatty(STDIN_FILENO) || !checktermsupport()) {
simpleline(buffer);
return;
}
2018-03-14 03:39:49 +00:00
if (rawmode()) {
simpleline(buffer);
return;
}
if (line()) {
norawmode();
if (gbl_sigint_flag) {
raise(SIGINT);
} else {
fputc('\n', out);
}
2018-03-14 03:39:49 +00:00
return;
}
fflush(stdin);
2018-03-14 03:39:49 +00:00
norawmode();
fputc('\n', out);
janet_buffer_ensure(buffer, gbl_len + 1, 2);
memcpy(buffer->data, gbl_buf, gbl_len);
buffer->data[gbl_len] = '\n';
buffer->count = gbl_len + 1;
2018-03-14 03:39:49 +00:00
replacehistory();
}
#endif