forked from WHC/Hermit
TUI: mouse drag selection with OSC 52 copy; Markdown emphasis follows CommonMark; v0.5.0
- Drag, double-click (word) and triple-click (line) selection in the log, auto-scroll at the edges, soft-wrapped lines joined on copy. - sashidown ANSI renderer: delimiter runs with flanking rules, so intraword underscores (test_apply_patch.c) stay literal; unmatched * _ and backticks are shown as-is; * and _ do not close each other. - tests: test_textsel, test_markdown.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
#include "sashidown.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(c) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK(%s) failed\n", __FILE__, __LINE__, #c); \
|
||||
exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* SASHIDOWN_FMT_ANSI_PLAIN theme */
|
||||
#define ITALIC "\033[3m"
|
||||
#define ITALIC_OFF "\033[23m"
|
||||
#define BOLD "\033[1m"
|
||||
#define BOLD_OFF "\033[22m"
|
||||
#define CODE "\033[7m"
|
||||
|
||||
static char out[4096], plain[4096];
|
||||
|
||||
/* Renders md; `out` keeps the escapes, `plain` has them stripped. */
|
||||
static void render(const char *md) {
|
||||
size_t n = 0;
|
||||
CHECK(sashidown_convert(md, strlen(md), out, sizeof(out) - 1, &n,
|
||||
SASHIDOWN_FMT_ANSI_PLAIN) == 0);
|
||||
out[n] = 0;
|
||||
size_t o = 0;
|
||||
for (size_t i = 0; i < n;) {
|
||||
if (out[i] == '\033' && out[i + 1] == '[') {
|
||||
i += 2;
|
||||
while (i < n && !(out[i] >= 0x40 && out[i] <= 0x7e))
|
||||
i++;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
plain[o++] = out[i++];
|
||||
}
|
||||
plain[o] = 0;
|
||||
}
|
||||
|
||||
static int has(const char *s, const char *needle) {
|
||||
return strstr(s, needle) != NULL;
|
||||
}
|
||||
|
||||
static void test_intraword(void) {
|
||||
render("see test_apply_patch.c now");
|
||||
CHECK(has(plain, "test_apply_patch.c"));
|
||||
CHECK(!has(out, ITALIC));
|
||||
render("snake_case_x and 中文_变量_名 and a__b__c");
|
||||
CHECK(has(plain, "snake_case_x and 中文_变量_名 and a__b__c"));
|
||||
CHECK(!has(out, ITALIC) && !has(out, BOLD));
|
||||
render("foo_bar_ baz");
|
||||
CHECK(has(plain, "foo_bar_ baz") && !has(out, ITALIC));
|
||||
}
|
||||
|
||||
static void test_pairs(void) {
|
||||
render("*it* and _it_");
|
||||
CHECK(has(out, ITALIC "it" ITALIC_OFF " and " ITALIC "it" ITALIC_OFF));
|
||||
render("**b** __b__");
|
||||
CHECK(has(out, BOLD "b" BOLD_OFF " " BOLD "b" BOLD_OFF));
|
||||
render("***x***");
|
||||
CHECK(has(out, BOLD ITALIC "x" ITALIC_OFF BOLD_OFF));
|
||||
render("*a_b* end");
|
||||
CHECK(has(out, ITALIC "a_b" ITALIC_OFF " end"));
|
||||
/* CommonMark: intraword '*' pairs, a standalone __init__ is bold */
|
||||
render("2*3*4");
|
||||
CHECK(has(out, "2" ITALIC "3" ITALIC_OFF "4"));
|
||||
render("call __init__ now");
|
||||
CHECK(has(out, BOLD "init" BOLD_OFF " now"));
|
||||
}
|
||||
|
||||
static void test_unmatched(void) {
|
||||
render("a * b and 2 * 3 * 4");
|
||||
CHECK(has(plain, "a * b and 2 * 3 * 4") && !has(out, ITALIC));
|
||||
render("trailing star*");
|
||||
CHECK(has(plain, "trailing star*") && !has(out, ITALIC));
|
||||
render("*mix_ words");
|
||||
CHECK(has(plain, "*mix_ words") && !has(out, ITALIC));
|
||||
render("**open bold only");
|
||||
CHECK(has(plain, "**open bold only") && !has(out, BOLD));
|
||||
render("a `b");
|
||||
CHECK(has(plain, "a `b") && !has(out, CODE));
|
||||
}
|
||||
|
||||
static void test_code(void) {
|
||||
render("run `a_b_c` and `*x*`");
|
||||
CHECK(has(plain, "run a_b_c and *x*"));
|
||||
CHECK(has(out, CODE) && !has(out, ITALIC));
|
||||
}
|
||||
|
||||
static void test_cjk(void) {
|
||||
render("**粗体**,后文");
|
||||
CHECK(has(out, BOLD "粗体" BOLD_OFF ",后文"));
|
||||
render("看**“引号”**这里");
|
||||
CHECK(has(out, BOLD "“引号”" BOLD_OFF "这里"));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_intraword();
|
||||
test_pairs();
|
||||
test_unmatched();
|
||||
test_code();
|
||||
test_cjk();
|
||||
printf("test_markdown: ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "cst_textsel.h"
|
||||
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(c) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK(%s) failed\n", __FILE__, __LINE__, #c); \
|
||||
exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static char got[512];
|
||||
|
||||
static const char *slice(const char *line, int indent, int c1, int c2,
|
||||
int trim) {
|
||||
cst_selbuf_t b = {0};
|
||||
CHECK(cst_textsel_append(&b, line, indent, c1, c2, trim) == 0);
|
||||
snprintf(got, sizeof(got), "%s", b.s ? b.s : "");
|
||||
cst_selbuf_free(&b);
|
||||
return got;
|
||||
}
|
||||
|
||||
static void test_ascii(void) {
|
||||
CHECK(strcmp(slice("hello world", 0, 0, 4, 1), "hello") == 0);
|
||||
CHECK(strcmp(slice("hello world", 0, 6, -1, 1), "world") == 0);
|
||||
CHECK(strcmp(slice("hello world", 0, 3, 7, 1), "lo wo") == 0);
|
||||
/* indent: columns before the content are blank */
|
||||
CHECK(strcmp(slice("abc", 2, 0, 2, 1), "a") == 0);
|
||||
CHECK(strcmp(slice("abc", 2, 0, 1, 1), "") == 0);
|
||||
/* trailing spaces */
|
||||
CHECK(strcmp(slice("ab ", 0, 0, -1, 1), "ab") == 0);
|
||||
CHECK(strcmp(slice("ab ", 0, 0, -1, 0), "ab ") == 0);
|
||||
/* beyond the end of the content */
|
||||
CHECK(strcmp(slice("ab", 0, 5, 9, 1), "") == 0);
|
||||
}
|
||||
|
||||
static void test_ansi(void) {
|
||||
const char *s = "\033[1;31mred\033[0m and \033[32mgreen\033[0m";
|
||||
CHECK(strcmp(slice(s, 0, 0, -1, 1), "red and green") == 0);
|
||||
CHECK(strcmp(slice(s, 0, 4, 6, 1), "and") == 0);
|
||||
CHECK(strcmp(slice(s, 0, 8, 12, 1), "green") == 0);
|
||||
int c1, c2;
|
||||
cst_textsel_word(s, 0, 10, &c1, &c2);
|
||||
CHECK(c1 == 8 && c2 == 12);
|
||||
}
|
||||
|
||||
static void test_words(void) {
|
||||
const char *s = " open src/tui.c:42 now";
|
||||
int c1, c2;
|
||||
cst_textsel_word(s, 0, 9, &c1, &c2);
|
||||
CHECK(c1 == 7 && c2 == 18);
|
||||
CHECK(strcmp(slice(s, 0, c1, c2, 1), "src/tui.c:42") == 0);
|
||||
cst_textsel_word(s, 0, 6, &c1, &c2); /* on a space */
|
||||
CHECK(c1 == 6 && c2 == 6);
|
||||
cst_textsel_word(s, 0, 40, &c1, &c2); /* past the end */
|
||||
CHECK(c1 == 40 && c2 == 40);
|
||||
cst_textsel_word(s, 1, 3, &c1, &c2); /* indent shifts columns */
|
||||
CHECK(c1 == 3 && c2 == 6);
|
||||
}
|
||||
|
||||
static void test_wide(void) {
|
||||
/* 中文 occupies columns 0-1 and 2-3 */
|
||||
const char *s = "中文 ok";
|
||||
CHECK(strcmp(slice(s, 0, 1, 1, 1), "中") == 0);
|
||||
CHECK(strcmp(slice(s, 0, 1, 2, 1), "中文") == 0);
|
||||
CHECK(strcmp(slice(s, 0, 3, 6, 1), "文 ok") == 0);
|
||||
int c1, c2;
|
||||
cst_textsel_word(s, 0, 3, &c1, &c2);
|
||||
CHECK(c1 == 0 && c2 == 3);
|
||||
cst_textsel_word("「x」", 0, 1, &c1, &c2); /* wide punctuation */
|
||||
CHECK(c1 == 0 && c2 == 1);
|
||||
/* highlight edges never split a wide character */
|
||||
c1 = 1, c2 = 2;
|
||||
cst_textsel_snap(s, 0, &c1, &c2);
|
||||
CHECK(c1 == 0 && c2 == 3);
|
||||
c1 = 2, c2 = 5;
|
||||
cst_textsel_snap(s, 0, &c1, &c2);
|
||||
CHECK(c1 == 2 && c2 == 5);
|
||||
c1 = 3, c2 = -1;
|
||||
cst_textsel_snap(s, 1, &c1, &c2); /* indent 1: 中 at 1-2, 文 at 3-4 */
|
||||
CHECK(c1 == 3 && c2 == -1);
|
||||
c1 = 4, c2 = 1;
|
||||
cst_textsel_snap(s, 1, &c1, &c2);
|
||||
CHECK(c1 == 3 && c2 == 2);
|
||||
}
|
||||
|
||||
static void test_join(void) {
|
||||
cst_selbuf_t b = {0};
|
||||
CHECK(cst_textsel_append(&b, "first line ", 0, 6, -1, 1) == 0);
|
||||
CHECK(cst_selbuf_putc(&b, '\n') == 0);
|
||||
CHECK(cst_textsel_append(&b, "second", 2, 0, 4, 1) == 0);
|
||||
CHECK(strcmp(b.s, "line\nsec") == 0);
|
||||
cst_selbuf_free(&b);
|
||||
CHECK(b.s == NULL && b.n == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ascii();
|
||||
test_ansi();
|
||||
test_words();
|
||||
if (setlocale(LC_CTYPE, "C.UTF-8") || setlocale(LC_CTYPE, "en_US.UTF-8"))
|
||||
test_wide();
|
||||
else
|
||||
printf("test_textsel: no UTF-8 locale, wide-character checks skipped\n");
|
||||
test_join();
|
||||
printf("test_textsel: ok\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user