Compare commits

...

3 Commits

Author SHA1 Message Date
5165f76b7c
xC: quote text coming from a bracketed paste
Not having this has caused me much annoyance over the years.
2021-10-30 09:27:32 +02:00
92ac13f3c6
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.

Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.

This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.

No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already.  The particular syntax is inspired
by .desktop files and systemd.

["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 09:02:35 +02:00
df4ca74580
xC: make libedit autocomplete less miserable
Omitting even this hack was a huge hit to overall usability.
2021-10-30 08:29:16 +02:00

193
xC.c
View File

@ -238,8 +238,8 @@ struct input_vtable
/// Bind Alt+key to the given named function /// Bind Alt+key to the given named function
void (*bind_meta) (void *input, char key, const char *fn); void (*bind_meta) (void *input, char key, const char *fn);
/// Get the current line input /// Get the current line input and position within
char *(*get_line) (void *input); char *(*get_line) (void *input, int *position);
/// Clear the current line input /// Clear the current line input
void (*clear_line) (void *input); void (*clear_line) (void *input);
/// Insert text at current position /// Insert text at current position
@ -361,9 +361,10 @@ input_rl_insert (void *input, const char *s)
} }
static char * static char *
input_rl_get_line (void *input) input_rl_get_line (void *input, int *position)
{ {
(void) input; (void) input;
if (position) *position = rl_point;
return rl_copy_text (0, rl_end); return rl_copy_text (0, rl_end);
} }
@ -860,10 +861,12 @@ input_el_insert (void *input, const char *s)
} }
static char * static char *
input_el_get_line (void *input) input_el_get_line (void *input, int *position)
{ {
struct input_el *self = input; struct input_el *self = input;
const LineInfo *info = el_line (self->editline); const LineInfo *info = el_line (self->editline);
int point = info->cursor - info->buffer;
if (position) *position = point;
return xstrndup (info->buffer, info->lastchar - info->buffer); return xstrndup (info->buffer, info->lastchar - info->buffer);
} }
@ -2439,6 +2442,13 @@ static struct config_schema g_config_behaviour[] =
.type = CONFIG_ITEM_BOOLEAN, .type = CONFIG_ITEM_BOOLEAN,
.default_ = "on", .default_ = "on",
.on_change = on_config_word_wrapping_change }, .on_change = on_config_word_wrapping_change },
{ .name = "editor_command",
.comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\"",
.type = CONFIG_ITEM_STRING },
{ .name = "process_pasted_text",
.comment = "Normalize newlines and quote the command prefix in pastes",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "date_change_line", { .name = "date_change_line",
.comment = "Input to strftime(3) for the date change line", .comment = "Input to strftime(3) for the date change line",
.type = CONFIG_ITEM_STRING, .type = CONFIG_ITEM_STRING,
@ -6894,7 +6904,7 @@ irc_handle_join (struct server *s, const struct irc_message *msg)
buffer_add (s->ctx, buffer); buffer_add (s->ctx, buffer);
char *input = CALL (s->ctx->input, get_line); char *input = CALL_ (s->ctx->input, get_line, NULL);
if (!*input) if (!*input)
buffer_activate (s->ctx, buffer); buffer_activate (s->ctx, buffer);
else else
@ -12618,8 +12628,6 @@ process_input (struct app_context *ctx, char *user_input)
else else
{ {
struct strv lines = strv_make (); struct strv lines = strv_make ();
// XXX: this interprets commands in pasted text
cstr_split (input, "\r\n", false, &lines); cstr_split (input, "\r\n", false, &lines);
for (size_t i = 0; i < lines.len; i++) for (size_t i = 0; i < lines.len; i++)
(void) process_input_utf8 (ctx, (void) process_input_utf8 (ctx,
@ -13152,7 +13160,7 @@ dump_input_to_file (struct app_context *ctx, char *template, struct error **e)
if (fd < 0) if (fd < 0)
return error_set (e, "%s", strerror (errno)); return error_set (e, "%s", strerror (errno));
char *input = CALL (ctx->input, get_line); char *input = CALL_ (ctx->input, get_line, NULL);
bool success = xwrite (fd, input, strlen (input), e); bool success = xwrite (fd, input, strlen (input), e);
free (input); free (input);
@ -13180,6 +13188,103 @@ try_dump_input_to_file (struct app_context *ctx)
return NULL; return NULL;
} }
static struct strv
build_editor_command (struct app_context *ctx, const char *filename)
{
struct strv argv = strv_make ();
const char *editor = get_config_string
(ctx->config.root, "behaviour.editor_command");
if (!editor)
{
const char *command;
if (!(command = getenv ("VISUAL"))
&& !(command = getenv ("EDITOR")))
command = "vi";
strv_append (&argv, command);
strv_append (&argv, filename);
return argv;
}
int cursor = 0;
char *input = CALL_ (ctx->input, get_line, &cursor);
hard_assert (cursor >= 0);
mbstate_t ps;
memset (&ps, 0, sizeof ps);
wchar_t wch;
size_t len, processed = 0, line_one_based = 1, column = 0;
while (processed < (size_t) cursor
&& (len = mbrtowc (&wch, input + processed, cursor - processed, &ps))
&& len != (size_t) -2 && len != (size_t) -1)
{
// Both VIM and Emacs use the caret notation with columns.
// Consciously leaving tabs broken, they're too difficult to handle.
int width = wcwidth (wch);
if (width < 0)
width = 2;
processed += len;
if (wch == '\n')
{
line_one_based++;
column = 0;
}
else
column += width;
}
free (input);
// Trivially split the command on spaces and substitute our values
struct str argument = str_make ();
for (; *editor; editor++)
{
if (*editor == ' ')
{
if (argument.len)
{
strv_append_owned (&argv, str_steal (&argument));
argument = str_make ();
}
continue;
}
if (*editor != '%' || !editor[1])
{
str_append_c (&argument, *editor);
continue;
}
// None of them are zero-length, thus words don't get lost
switch (*++editor)
{
case 'F':
str_append (&argument, filename);
break;
case 'L':
str_append_printf (&argument, "%zu", line_one_based);
break;
case 'C':
str_append_printf (&argument, "%zu", column + 1);
break;
case 'B':
str_append_printf (&argument, "%d", cursor + 1);
break;
case '%':
case ' ':
str_append_c (&argument, *editor);
break;
default:
print_warning ("unknown substitution variable");
}
}
if (argument.len)
strv_append_owned (&argv, str_steal (&argument));
else
str_free (&argument);
return argv;
}
static bool static bool
on_edit_input (int count, int key, void *user_data) on_edit_input (int count, int key, void *user_data)
{ {
@ -13191,16 +13296,15 @@ on_edit_input (int count, int key, void *user_data)
if (!(filename = try_dump_input_to_file (ctx))) if (!(filename = try_dump_input_to_file (ctx)))
return false; return false;
const char *command; struct strv argv = build_editor_command (ctx, filename);
if (!(command = getenv ("VISUAL")) if (!argv.len)
&& !(command = getenv ("EDITOR"))) strv_append (&argv, "true");
command = "vi";
hard_assert (!ctx->running_editor); hard_assert (!ctx->running_editor);
switch (spawn_helper_child (ctx)) switch (spawn_helper_child (ctx))
{ {
case 0: case 0:
execlp (command, command, filename, NULL); execvp (argv.vector[0], argv.vector);
print_error ("%s: %s", print_error ("%s: %s",
"Failed to launch editor", strerror (errno)); "Failed to launch editor", strerror (errno));
_exit (EXIT_FAILURE); _exit (EXIT_FAILURE);
@ -13213,6 +13317,7 @@ on_edit_input (int count, int key, void *user_data)
ctx->running_editor = true; ctx->running_editor = true;
ctx->editor_filename = filename; ctx->editor_filename = filename;
} }
strv_free (&argv);
return true; return true;
} }
@ -13698,19 +13803,33 @@ on_editline_complete (EditLine *editline, int key)
// Insert the best match instead // Insert the best match instead
el_insertstr (editline, completions[0]); el_insertstr (editline, completions[0]);
// I'm not sure if Readline's menu-complete can at all be implemented
// with Editline--we have no way of detecting what the last executed handler
// was. Employ the formatter's wrapping feature to spew all options.
bool only_match = !completions[1]; bool only_match = !completions[1];
if (!only_match)
{
CALL (ctx->input, hide);
redraw_screen (ctx);
struct formatter f = formatter_make (ctx, NULL);
for (char **p = completions; *++p; )
formatter_add (&f, " #l", *p);
formatter_add (&f, "\n");
formatter_flush (&f, stdout, 0);
formatter_free (&f);
CALL (ctx->input, show);
}
for (char **p = completions; *p; p++) for (char **p = completions; *p; p++)
free (*p); free (*p);
free (completions); free (completions);
// I'm not sure if Readline's menu-complete can at all be implemented
// with Editline. Spamming the terminal with possible completions
// probably isn't what the user wants and we have no way of detecting
// what the last executed handler was.
if (!only_match) if (!only_match)
return CC_REFRESH_BEEP; return CC_REFRESH_BEEP;
// But if there actually is just one match, finish the word // If there actually is just one match, finish the word
el_insertstr (editline, " "); el_insertstr (editline, " ");
return CC_REFRESH; return CC_REFRESH;
} }
@ -14117,6 +14236,40 @@ done:
#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted #define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted
static bool
insert_paste (struct app_context *ctx, char *paste, size_t len)
{
if (!get_config_boolean (ctx->config.root, "behaviour.process_pasted_text"))
return CALL_ (ctx->input, insert, paste);
// Without ICRNL, which Editline keeps but Readline doesn't,
// the terminal sends newlines as carriage returns (seen on urxvt)
for (size_t i = 0; i < len; i++)
if (paste[i] == '\r')
paste[i] = '\n';
int position = 0;
char *input = CALL_ (ctx->input, get_line, &position);
bool quote_first_slash = !position || strchr ("\r\n", input[position - 1]);
free (input);
// Executing commands by accident is much more common than pasting them
// intentionally, although the latter may also have security consequences
struct str processed = str_make ();
str_reserve (&processed, len);
for (size_t i = 0; i < len; i++)
{
if (paste[i] == '/'
&& ((!i && quote_first_slash) || (i && paste[i - 1] == '\n')))
str_append_c (&processed, paste[i]);
str_append_c (&processed, paste[i]);
}
bool success = CALL_ (ctx->input, insert, processed.str);
str_free (&processed);
return success;
}
static void static void
process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx) process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
{ {
@ -14141,7 +14294,7 @@ process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
(int) (text_len = BRACKETED_PASTE_LIMIT)); (int) (text_len = BRACKETED_PASTE_LIMIT));
buf->str[text_len] = '\0'; buf->str[text_len] = '\0';
if (CALL_ (ctx->input, insert, buf->str)) if (insert_paste (ctx, buf->str, text_len))
goto done; goto done;
error: error: