diff --git a/Makefile b/Makefile index df7d1ecdc..21da5bbc7 100644 --- a/Makefile +++ b/Makefile @@ -307,6 +307,7 @@ TIG_OBJS = \ src/stash.o \ src/grep.o \ src/ui.o \ + src/registers.o \ $(GRAPH_OBJS) \ $(COMPAT_OBJS) diff --git a/doc/manual.adoc b/doc/manual.adoc index b32842da6..67f9ca082 100644 --- a/doc/manual.adoc +++ b/doc/manual.adoc @@ -193,6 +193,8 @@ following variables. |%(repo:is-inside-work-tree) |Whether Tig is running inside a work tree, either `true` or `false`. +|%(register:x) |A user-defined register value, where `x` is an ASCII + character. |============================================================================= Example user-defined commands: @@ -488,7 +490,8 @@ Prompt |:script |Execute commands from ``. |:exec |Execute command using `` with external user-defined command option flags defined in ``. -|:echo |Display text in the status bar. +|:echo |Display text in the status bar. +|:set-register |Load a value into the register named ``. |============================================================================= [[external-commands]] diff --git a/doc/tigrc.5.adoc b/doc/tigrc.5.adoc index f1337b901..2c5da591d 100644 --- a/doc/tigrc.5.adoc +++ b/doc/tigrc.5.adoc @@ -644,6 +644,8 @@ the command that should be executed. of output to the status bar. |? |Prompt the user before executing the command. |< |Exit Tig after executing the command. +|>(x) |Run the command synchronously, and store the first + line of output in the register named `x`. |============================================================================= Unless otherwise specified, commands are run in the foreground with their @@ -695,6 +697,8 @@ following variable names, which are substituted before commands are run: |%(repo:is-inside-work-tree) |Whether Tig is running inside a work tree, either `true` or `false`. +|%(register:x) |A user-defined register value, where `x` is an ASCII + character. |============================================================================= Examples: diff --git a/include/tig/argv.h b/include/tig/argv.h index 156ae2aab..b5c123d6a 100644 --- a/include/tig/argv.h +++ b/include/tig/argv.h @@ -15,6 +15,7 @@ #define TIG_ARGV_H #include "tig/tig.h" +#include "tig/registers.h" /* * Argument array helpers. @@ -62,6 +63,7 @@ typedef unsigned long argv_number; struct argv_env { ARGV_ENV_INFO(ARGV_ENV_FIELDS) unsigned long goto_lineno; + char *registers[SIZEOF_REGISTERS]; char search[SIZEOF_STR]; char none[1]; }; diff --git a/include/tig/display.h b/include/tig/display.h index 63954b6a1..c9c48009b 100644 --- a/include/tig/display.h +++ b/include/tig/display.h @@ -52,7 +52,7 @@ bool save_view(struct view *view, const char *path); bool vertical_split_is_enabled(enum vertical_split vsplit, int height, int width); int apply_vertical_split(int base_width); -bool open_external_viewer(const char *argv[], const char *dir, bool silent, bool confirm, bool echo, bool refresh, const char *notice); +bool open_external_viewer(const char *argv[], const char *dir, bool silent, bool confirm, bool echo, char register_key, bool refresh, const char *notice); void open_editor(const char *file, unsigned int lineno); void enable_mouse(bool enable); diff --git a/include/tig/keys.h b/include/tig/keys.h index 45739c6d7..d09782bc4 100644 --- a/include/tig/keys.h +++ b/include/tig/keys.h @@ -83,6 +83,7 @@ struct run_request_flags { bool exit; bool internal; bool echo; + char register_key; }; struct run_request { diff --git a/include/tig/registers.h b/include/tig/registers.h new file mode 100644 index 000000000..657b5e08e --- /dev/null +++ b/include/tig/registers.h @@ -0,0 +1,159 @@ +/* Copyright (c) 2006-2017 Jonas Fonseca + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef TIG_REGISTERS_H +#define TIG_REGISTERS_H + +#include "tig/types.h" + +/* Index 0 of the register array, corresponding to character key ASCII + * space, is kept empty and reserved for internal use as "no register". + */ +#define REGISTER_KEY_MIN '!' /* corresponding to index 1 */ +#define REGISTER_KEY_MAX '~' /* corresponding to index 94 */ +#define REGISTER_KEY_OFFSET 0x20 +#define SIZEOF_REGISTERS 1 + REGISTER_KEY_MAX - REGISTER_KEY_OFFSET + +#define REGISTER_FLAG_OPEN_STR ">(" +#define REGISTER_FLAG_CLOSE_STR ")" +#define REGISTER_ESC_CHAR '\\' + +#define is_register_esc_char(ch) \ + ((ch) == REGISTER_ESC_CHAR) + +#define is_register_meta_char(ch) \ + (is_register_esc_char(ch) \ + || ((ch) == REGISTER_FLAG_OPEN_STR[1]) \ + || ((ch) == REGISTER_FLAG_CLOSE_STR[0]) \ + || ((ch) == '"') \ + || ((ch) == '\'')) + +#define at_register_flag_open(p) \ + (((p)[0] == REGISTER_FLAG_OPEN_STR[0]) && ((p)[1] == REGISTER_FLAG_OPEN_STR[1])) + +#define at_register_flag_close(p) \ + ((p)[0] == REGISTER_FLAG_CLOSE_STR[0]) + +#define at_register_escd_pair(p) \ + (is_register_esc_char((p)[0]) && is_register_meta_char((p)[1])) + +#define register_key_to_index(key) \ + ((((key) >= REGISTER_KEY_MIN) && ((key) <= REGISTER_KEY_MAX)) ? (unsigned int) (key) - REGISTER_KEY_OFFSET : 0) + +bool register_set(const char key, const char *value); +const char *register_get(const char key); + +/* metacharacters occur twice, once as an escaped sequence */ +#define REGISTER_INFO(_) \ + _("\\\\", '\\') \ + _("\\(", '(') \ + _("\\)", ')') \ + _("\\\"", '"') \ + _("\\'", '\'') \ + _("!", '!') \ + _("\"", '"') \ + _("#", '#') \ + _("$", '$') \ + _("%", '%') \ + _("&", '&') \ + _("'", '\'') \ + _("(", '(') \ + _(")", ')') \ + _("*", '*') \ + _("+", '+') \ + _(",", ',') \ + _("-", '-') \ + _(".", '.') \ + _("/", '/') \ + _("0", '0') \ + _("1", '1') \ + _("2", '2') \ + _("3", '3') \ + _("4", '4') \ + _("5", '5') \ + _("6", '6') \ + _("7", '7') \ + _("8", '8') \ + _("9", '9') \ + _(":", ':') \ + _(";", ';') \ + _("<", '<') \ + _("=", '=') \ + _(">", '>') \ + _("?", '?') \ + _("@", '@') \ + _("A", 'A') \ + _("B", 'B') \ + _("C", 'C') \ + _("D", 'D') \ + _("E", 'E') \ + _("F", 'F') \ + _("G", 'G') \ + _("H", 'H') \ + _("I", 'I') \ + _("J", 'J') \ + _("K", 'K') \ + _("L", 'L') \ + _("M", 'M') \ + _("N", 'N') \ + _("O", 'O') \ + _("P", 'P') \ + _("Q", 'Q') \ + _("R", 'R') \ + _("S", 'S') \ + _("T", 'T') \ + _("U", 'U') \ + _("V", 'V') \ + _("W", 'W') \ + _("X", 'X') \ + _("Y", 'Y') \ + _("Z", 'Z') \ + _("[", '[') \ + _("\\", '\\') \ + _("]", ']') \ + _("^", '^') \ + _("_", '_') \ + _("`", '`') \ + _("a", 'a') \ + _("b", 'b') \ + _("c", 'c') \ + _("d", 'd') \ + _("e", 'e') \ + _("f", 'f') \ + _("g", 'g') \ + _("h", 'h') \ + _("i", 'i') \ + _("j", 'j') \ + _("k", 'k') \ + _("l", 'l') \ + _("m", 'm') \ + _("n", 'n') \ + _("o", 'o') \ + _("p", 'p') \ + _("q", 'q') \ + _("r", 'r') \ + _("s", 's') \ + _("t", 't') \ + _("u", 'u') \ + _("v", 'v') \ + _("w", 'w') \ + _("x", 'x') \ + _("y", 'y') \ + _("z", 'z') \ + _("{", '{') \ + _("|", '|') \ + _("}", '}') \ + _("~", '~') + +#endif +/* vim: set ts=8 sw=8 noexpandtab: */ diff --git a/src/argv.c b/src/argv.c index b159c056c..c59758164 100644 --- a/src/argv.c +++ b/src/argv.c @@ -16,6 +16,7 @@ #include "tig/repo.h" #include "tig/options.h" #include "tig/prompt.h" +#include "tig/registers.h" static bool concat_argv(const char *argv[], char *buf, size_t buflen, const char *sep, bool quoted) @@ -376,6 +377,12 @@ format_append_arg(struct format_context *format, const char ***dst_argv, const c while (arg) { const char *var = strstr(arg, "%("); const char *closing = var ? strchr(var, ')') : NULL; + + /* todo hardcoding is robust and compact but ugly */ + if (var && + (!strcmp(var, "%(register:\\))") || !strcmp(var, "%(register:))"))) + closing++; + const char *next = closing ? closing + 1 : NULL; const int len = var ? var - arg : strlen(arg); @@ -468,6 +475,10 @@ argv_format(struct argv_env *argv_env, const char ***dst_argv, const char *src_a #define FORMAT_REPO_VAR(type, name) \ { "%(repo:" #name ")", STRING_SIZE("%(repo:" #name ")"), type ## _formatter, &repo.name, "" }, REPO_INFO(FORMAT_REPO_VAR) +#define FORMAT_REGISTER_VAR(namestr, keychar) \ + { "%(register:" namestr ")", STRING_SIZE("%(register:" namestr ")"), argv_string_formatter, \ + argv_env->registers[ MIN(keychar,REGISTER_KEY_MAX) - REGISTER_KEY_OFFSET ], "" }, + REGISTER_INFO(FORMAT_REGISTER_VAR) }; struct format_context format = { vars, ARRAY_SIZE(vars), "", 0, file_filter }; int argc; diff --git a/src/display.c b/src/display.c index 2b48498d3..6938c30cf 100644 --- a/src/display.c +++ b/src/display.c @@ -20,6 +20,7 @@ #include "tig/draw.h" #include "tig/display.h" #include "tig/watch.h" +#include "tig/registers.h" static void set_terminal_modes(void); @@ -56,19 +57,29 @@ open_script(const char *path) } bool -open_external_viewer(const char *argv[], const char *dir, bool silent, bool confirm, bool echo, bool refresh, const char *notice) +open_external_viewer(const char *argv[], const char *dir, bool silent, bool confirm, bool echo, char register_key, bool refresh, const char *notice) { bool ok; - if (echo) { + if (echo || register_key) { char buf[SIZEOF_STR] = ""; io_run_buf(argv, buf, sizeof(buf), dir, false); if (*buf) { - report("%s", buf); + if (register_key) + register_set(register_key, buf); + if (echo) + report("%s", buf); + else + report_clear(); return true; } else { - report("No output"); + if (register_key) + register_set(register_key, ""); + if (echo) + report("No output"); + else + report_clear(); return false; } } else if (silent || is_script_executing()) { @@ -138,7 +149,7 @@ open_editor(const char *file, unsigned int lineno) if (lineno && opt_editor_line_number && string_format(lineno_cmd, "+%u", lineno)) editor_argv[argc++] = lineno_cmd; editor_argv[argc] = file; - if (!open_external_viewer(editor_argv, repo.cdup, false, false, false, true, EDITOR_LINENO_MSG)) + if (!open_external_viewer(editor_argv, repo.cdup, false, false, false, 0, true, EDITOR_LINENO_MSG)) opt_editor_line_number = false; } diff --git a/src/keys.c b/src/keys.c index ef3c0ec93..429567c8e 100644 --- a/src/keys.c +++ b/src/keys.c @@ -450,7 +450,7 @@ static size_t run_requests; DEFINE_ALLOCATOR(realloc_run_requests, struct run_request, 8) -#define COMMAND_FLAGS ":!?@<+" +#define COMMAND_FLAGS ":!?@<>+" enum status_code parse_run_request_flags(struct run_request_flags *flags, const char **argv) @@ -474,6 +474,21 @@ parse_run_request_flags(struct run_request_flags *flags, const char **argv) flags->exit = 1; } else if (*argv[0] == '+') { flags->echo = 1; + } else if (at_register_flag_open(argv[0]) + && at_register_escd_pair(argv[0] + 2) + && at_register_flag_close(argv[0] + 4)) { + flags->register_key = argv[0][3]; + argv[0] += 5; + continue; + } else if (at_register_flag_open(argv[0]) + && argv[0][2] >= REGISTER_KEY_MIN + && argv[0][2] <= REGISTER_KEY_MAX + && at_register_flag_close(argv[0] + 3)) { + flags->register_key = argv[0][2]; + argv[0] += 4; + continue; + } else if (at_register_flag_open(argv[0])) { + return error("Invalid register flag"); } else if (*argv[0] != '!') { break; } @@ -518,7 +533,7 @@ get_run_request(enum request request) const char * format_run_request_flags(const struct run_request *req) { - static char flags[8]; + static char flags[16]; int flagspos = 0; memset(flags, 0, sizeof(flags)); @@ -529,13 +544,21 @@ format_run_request_flags(const struct run_request *req) flags[flagspos] = '!'; /* Optional, if other flags are defined */ if (req->flags.silent) - flags[flagspos++] = '@'; + flags[flagspos++] = '@'; if (req->flags.confirm) - flags[flagspos++] = '?'; + flags[flagspos++] = '?'; if (req->flags.exit) flags[flagspos++] = '<'; if (req->flags.echo) flags[flagspos++] = '+'; + if (req->flags.register_key) { + flags[flagspos++] = REGISTER_FLAG_OPEN_STR[0]; + flags[flagspos++] = REGISTER_FLAG_OPEN_STR[1]; + if (is_register_meta_char(req->flags.register_key)) + flags[flagspos++] = REGISTER_ESC_CHAR; + flags[flagspos++] = req->flags.register_key; + flags[flagspos++] = REGISTER_FLAG_CLOSE_STR[0]; + } if (flagspos > 1) flags[flagspos++] = 0; diff --git a/src/prompt.c b/src/prompt.c index 0152fafda..2940f1bf2 100644 --- a/src/prompt.c +++ b/src/prompt.c @@ -210,6 +210,7 @@ readline_variable_generator(const char *text, int state) #define FORMAT_VAR(type, name) "%(repo:" #name ")", REPO_INFO(FORMAT_VAR) #undef FORMAT_VAR + "%(register:", NULL }; @@ -255,6 +256,7 @@ readline_action_generator(const char *text, int state) "save-options", "exec", "echo", + "set-register", #define REQ_GROUP(help) #define REQ_(req, help) #req REQ_INFO, @@ -816,6 +818,67 @@ prompt_update_display(enum view_flag flags) } } +bool +parse_set_register(char *key, const char ***dst_argv, const char **src_argv) +{ + if (!src_argv || !src_argv[0] || !src_argv[1]) + return false; + + src_argv++; + + /* src_argv is the product of one pass of argv_from_string and arrives + * here in a halfway state. Hack src_argv to tokenize as + * |set-register|"|hello there| + * instead of + * |set-register|" hello there| + * Consider adjusting argv_from_string instead. + */ + char prepend_tok[SIZEOF_STR] = ""; + if (strlen(*src_argv) > 2 + && at_register_escd_pair(*src_argv) + && isspace(src_argv[0][2])) { + string_ncopy(prepend_tok, *src_argv + 1, 1); + *src_argv += 2; + *src_argv += strspn(*src_argv, " "); + argv_prepend(&src_argv, prepend_tok); + } else if (strlen(*src_argv) > 1 + && register_key_to_index(src_argv[0][0]) + && isspace(src_argv[0][1])) { + string_ncopy(prepend_tok, *src_argv, 1); + *src_argv += 1; + *src_argv += strspn(*src_argv, " "); + argv_prepend(&src_argv, prepend_tok); + } + /* end of first hack */ + + + if (strlen(*src_argv) == 2 + && at_register_escd_pair(*src_argv)) { + *key = src_argv[0][1]; + } else if (strlen(*src_argv) == 1 + && register_key_to_index(src_argv[0][0])) { + *key = src_argv[0][0]; + } else { + return false; + } + src_argv++; + + + /* Since we src_argv was hacked above, make extra certain it refers to sane + * content. This is reasonable logic, but likely appearing in the wrong place. + */ + while (*src_argv && !src_argv[0][0] && src_argv[1]) { + src_argv++; + *src_argv += strspn(*src_argv, " "); + } + /* end of second hack */ + + if (!*src_argv || !src_argv[0][0] || !argv_copy(dst_argv, src_argv)) + return false; + + return true; +} + enum request run_prompt_command(struct view *view, const char *argv[]) { @@ -899,6 +962,35 @@ run_prompt_command(struct view *view, const char *argv[]) report("%s", text); return REQ_NONE; + } else if (!strcmp(cmd, "set-register")) { + const char **value_argv = NULL; + const char **fmt_argv = NULL; + char value[SIZEOF_STR] = ""; + char key; + + if (!parse_set_register(&key, &value_argv, argv)) { + report("Error: set-register "); + return REQ_NONE; + } + + /* todo string escaping and quote unwrapping, with + * appropriate handling of empty strings. The user + * should be able to clear a register with + * :set-register b "" + */ + + if (!argv_format(view->env, &fmt_argv, value_argv, false, true) + || !argv_to_string(&fmt_argv[0], value, sizeof(value), " ")) { + report("Failed to copy set-register string"); + return REQ_NONE; + } + + if (!register_set(key, value)) + report("Failed to set register"); + else + /* this message would be nicer if the key was distinguished in color */ + report("Register %c %s", key, value); + } else if (!strcmp(cmd, "save-display")) { const char *path = argv[1] ? argv[1] : "tig-display.txt"; @@ -1047,7 +1139,8 @@ exec_run_request(struct view *view, struct run_request *req) if (confirmed) open_external_viewer(argv, repo.cdup, req->flags.silent, - !req->flags.exit, req->flags.echo, false, ""); + !req->flags.exit, req->flags.echo, + req->flags.register_key, false, ""); } if (argv) diff --git a/src/registers.c b/src/registers.c new file mode 100644 index 000000000..ab919efdf --- /dev/null +++ b/src/registers.c @@ -0,0 +1,44 @@ +/* Copyright (c) 2006-2017 Jonas Fonseca + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "tig/registers.h" +#include "tig/argv.h" + +bool +register_set(const char key, const char *value) +{ + unsigned int idx = register_key_to_index(key); + + if (!idx) + return false; + if (!argv_env.registers[idx]) + argv_env.registers[idx] = calloc(1, SIZEOF_STR); + if (!argv_env.registers[idx]) + return false; + + string_ncopy_do(argv_env.registers[idx], SIZEOF_STR - 1, value, strlen(value)); + return true; +} + +const char * +register_get(const char key) +{ + unsigned int idx = register_key_to_index(key); + + if (!idx) + return NULL; + + return argv_env.registers[idx]; +} + +/* vim: set ts=8 sw=8 noexpandtab: */ diff --git a/src/status.c b/src/status.c index 4e4f33d9c..5657896d5 100644 --- a/src/status.c +++ b/src/status.c @@ -676,7 +676,7 @@ open_mergetool(const char *file) { const char *mergetool_argv[] = { "git", "mergetool", file, NULL }; - open_external_viewer(mergetool_argv, repo.cdup, false, true, false, true, ""); + open_external_viewer(mergetool_argv, repo.cdup, false, true, false, 0, true, ""); } static enum request diff --git a/test/script/register-test b/test/script/register-test new file mode 100755 index 000000000..5c1a0532a --- /dev/null +++ b/test/script/register-test @@ -0,0 +1,109 @@ +#!/bin/sh + +. libtest.sh +. libgit.sh + +LINES=5 + +in_work_dir create_repo_from_tgz "$base_dir/files/scala-js-benchmarks.tgz" + +tigrc <(a)echo shell_value + :exec !assert-var %(register:a) == shell_value + + :exec !>(a)echo bang_shell_value + :exec !assert-var %(register:a) == bang_shell_value + + :set-register b %(commit) + :exec !assert-var %(register:b) == ee912870202200a0b9cf4fd86ba57243212d341e + + :exec !assert-var X%(register:c) == X + + :set-register \\c value + :exec !assert-var X%(register:c) == X + + :set-register cc value + :exec !assert-var X%(register:c) == X + + :set-register cvalue + :exec !assert-var X%(register:c) == X + + :exec >(\\c)echo value + :exec !assert-var X%(register:c) == X + + :set-register \\( value + :exec !assert-var %(register:\\() == value + + :set-register ( unescaped_value + :exec !assert-var %(register:() == unescaped_value + :exec !assert-var %(register:\\() == unescaped_value + + :exec >(\\()echo shell_value + :exec !assert-var %(register:\\() == shell_value + + :exec >(()echo unescaped_shell_value + :exec !assert-var %(register:\\() == unescaped_shell_value + + :set-register \\\" value + :exec !assert-var '%(register:\\\")' == value + + :set-register \" unescaped_value + :exec !assert-var '%(register:\")' == unescaped_value + :exec !assert-var '%(register:\\\")' == unescaped_value + + :exec >(\\\")echo shell_value + :exec !assert-var '%(register:\\\")' == shell_value + + :exec >(\")echo unescaped_shell_value + :exec !assert-var '%(register:\\\")' == unescaped_shell_value + + :set-register \\' value + :exec !assert-var \"%(register:\\')\" == value + + :set-register ' unescaped_value + :exec !assert-var \"%(register:')\" == unescaped_value + :exec !assert-var \"%(register:\\')\" == unescaped_value + + :exec >(\\')echo shell_value + :exec !assert-var \"%(register:\\')\" == shell_value + + :exec >(')echo unescaped_shell_value + :exec !assert-var \"%(register:\\')\" == unescaped_shell_value + + :set-register b strip leading space + :exec !assert-var %(register:b) == 'strip leading space' + + :set-register ' strip leading space handle irregular unbalanced + :exec !assert-var \"%(register:')\" == 'strip leading space handle irregular unbalanced' + + :set-register ' strip leading space coerce irregular balanced' + :exec !assert-var \"%(register:')\" == \"strip leading space coerce irregular balanced'\" + + :save-display main.screen +" +test_tig + +assert_equals stderr <(r)save-to-register command +bind main 0 !@?<+>(r)all modifiers # Non-ending multi-line command c\\ @@ -99,9 +100,9 @@ tig warning: ~/.tigrc:25: Unknown color attribute: normally tig warning: ~/.tigrc:34: Unknown option \`visibility' for column line-number tig warning: ~/.tigrc:36: Invalid key binding: bind keymap key action tig warning: ~/.tigrc:37: Invalid key binding: bind keymap key action -tig warning: ~/.tigrc:38: Unknown command flag '%'; expected one of :!?@<+ -tig warning: ~/.tigrc:39: Unknown command flag '|'; expected one of :!?@<+ -tig warning: ~/.tigrc:57: Unknown option command: c +tig warning: ~/.tigrc:38: Unknown command flag '%'; expected one of :!?@<>+ +tig warning: ~/.tigrc:39: Unknown command flag '|'; expected one of :!?@<>+ +tig warning: ~/.tigrc:58: Unknown option command: c tig warning: Errors while loading HOME/.tigrc. EOF @@ -122,7 +123,8 @@ External commands: 3 ?prompted command 4 (r)save-to-register command + 0 @?<+>(r)all modifiers @@ -133,6 +135,5 @@ External commands: - -[help] - line 1 of 17 100% +[help] - line 1 of 18 100% EOF