clipboard: avoid error flood during :redir

redir_write():
- This is a "batch" operation which was not yet covered by
  start_batch_changes()

adjust_clipboard_name():
- msg() and friends during :redir will, of course, cause redir_write()
  to try to capture that message, which causes recursion.
- EMSG() here is trouble: if it interrupts :redir it is a mess.
  Rather than deal with the mess, show a non-error message.

closes #7182
closes #7184
closes #7183
ref #6048
ref #7032
This commit is contained in:
Justin M. Keyes
2017-08-20 02:13:04 +02:00
parent b3da396804
commit 9882e25dc4
6 changed files with 99 additions and 54 deletions

View File

@@ -121,14 +121,14 @@ function! s:check_clipboard() abort
call health#report_start('Clipboard (optional)') call health#report_start('Clipboard (optional)')
let clipboard_tool = provider#clipboard#Executable() let clipboard_tool = provider#clipboard#Executable()
if empty(clipboard_tool) if exists('g:clipboard') && empty(clipboard_tool)
call health#report_warn(
\ 'No clipboard tool found. Clipboard registers will not work.',
\ [':help clipboard'])
elseif exists('g:clipboard') && (type({}) != type(g:clipboard)
\ || !has_key(g:clipboard, 'copy') || !has_key(g:clipboard, 'paste'))
call health#report_error( call health#report_error(
\ 'g:clipboard exists but is malformed. It must be a dictionary with the keys documented at :help g:clipboard') \ provider#clipboard#Error(),
\ ["Use the example in :help g:clipboard as a template, or don't set g:clipboard at all."])
elseif empty(clipboard_tool)
call health#report_warn(
\ 'No clipboard tool found. Clipboard registers (`"+` and `"*`) will not work.',
\ [':help clipboard'])
else else
call health#report_ok('Clipboard tool found: '. clipboard_tool) call health#report_ok('Clipboard tool found: '. clipboard_tool)
endif endif

View File

@@ -3,6 +3,7 @@
" available. " available.
let s:copy = {} let s:copy = {}
let s:paste = {} let s:paste = {}
let s:clipboard = {}
" When caching is enabled, store the jobid of the xclip/xsel process keeping " When caching is enabled, store the jobid of the xclip/xsel process keeping
" ownership of the selection, so we know how long the cache is valid. " ownership of the selection, so we know how long the cache is valid.
@@ -23,7 +24,7 @@ function! s:selection.on_exit(jobid, data, event) abort
call provider#clear_stderr(a:jobid) call provider#clear_stderr(a:jobid)
endfunction endfunction
let s:selections = { '*': s:selection, '+': copy(s:selection)} let s:selections = { '*': s:selection, '+': copy(s:selection) }
function! s:try_cmd(cmd, ...) abort function! s:try_cmd(cmd, ...) abort
let argv = split(a:cmd, " ") let argv = split(a:cmd, " ")
@@ -55,6 +56,12 @@ endfunction
function! provider#clipboard#Executable() abort function! provider#clipboard#Executable() abort
if exists('g:clipboard') if exists('g:clipboard')
if type({}) isnot# type(g:clipboard)
\ || type({}) isnot# get(g:clipboard, 'copy', v:null)
\ || type({}) isnot# get(g:clipboard, 'paste', v:null)
let s:err = 'clipboard: invalid g:clipboard'
return ''
endif
let s:copy = get(g:clipboard, 'copy', { '+': v:null, '*': v:null }) let s:copy = get(g:clipboard, 'copy', { '+': v:null, '*': v:null })
let s:paste = get(g:clipboard, 'paste', { '+': v:null, '*': v:null }) let s:paste = get(g:clipboard, 'paste', { '+': v:null, '*': v:null })
let s:cache_enabled = get(g:clipboard, 'cache_enabled', 1) let s:cache_enabled = get(g:clipboard, 'cache_enabled', 1)
@@ -104,16 +111,17 @@ function! provider#clipboard#Executable() abort
return 'tmux' return 'tmux'
endif endif
let s:err = 'clipboard: No clipboard tool available. :help clipboard' let s:err = 'clipboard: No clipboard tool. :help clipboard'
return '' return ''
endfunction endfunction
if empty(provider#clipboard#Executable()) if empty(provider#clipboard#Executable())
" provider#clipboard#Call() *must not* be defined if the provider is broken.
" Otherwise eval_has_provider() thinks the clipboard provider is
" functioning, and eval_call_provider() will happily call it.
finish finish
endif endif
let s:clipboard = {}
function! s:clipboard.get(reg) abort function! s:clipboard.get(reg) abort
if s:selections[a:reg].owner > 0 if s:selections[a:reg].owner > 0
return s:selections[a:reg].data return s:selections[a:reg].data
@@ -154,7 +162,9 @@ function! s:clipboard.set(lines, regtype, reg) abort
echohl WarningMsg echohl WarningMsg
echomsg 'clipboard: failed to execute: '.(s:copy[a:reg]) echomsg 'clipboard: failed to execute: '.(s:copy[a:reg])
echohl None echohl None
return 0
endif endif
return 1
endfunction endfunction
function! provider#clipboard#Call(method, args) abort function! provider#clipboard#Call(method, args) abort

View File

@@ -22775,7 +22775,7 @@ typval_T eval_call_provider(char *provider, char *method, list_T *arguments)
bool eval_has_provider(const char *name) bool eval_has_provider(const char *name)
{ {
#define check_provider(name) \ #define CHECK_PROVIDER(name) \
if (has_##name == -1) { \ if (has_##name == -1) { \
has_##name = !!find_func((char_u *)"provider#" #name "#Call"); \ has_##name = !!find_func((char_u *)"provider#" #name "#Call"); \
if (!has_##name) { \ if (!has_##name) { \
@@ -22791,17 +22791,17 @@ bool eval_has_provider(const char *name)
static int has_python3 = -1; static int has_python3 = -1;
static int has_ruby = -1; static int has_ruby = -1;
if (!strcmp(name, "clipboard")) { if (strequal(name, "clipboard")) {
check_provider(clipboard); CHECK_PROVIDER(clipboard);
return has_clipboard; return has_clipboard;
} else if (!strcmp(name, "python3")) { } else if (strequal(name, "python3")) {
check_provider(python3); CHECK_PROVIDER(python3);
return has_python3; return has_python3;
} else if (!strcmp(name, "python")) { } else if (strequal(name, "python")) {
check_provider(python); CHECK_PROVIDER(python);
return has_python; return has_python;
} else if (!strcmp(name, "ruby")) { } else if (strequal(name, "ruby")) {
check_provider(ruby); CHECK_PROVIDER(ruby);
return has_ruby; return has_ruby;
} }

View File

@@ -2519,6 +2519,7 @@ static void redir_write(const char *const str, const ptrdiff_t maxlen)
if (redirecting()) { if (redirecting()) {
/* If the string doesn't start with CR or NL, go to msg_col */ /* If the string doesn't start with CR or NL, go to msg_col */
if (*s != '\n' && *s != '\r') { if (*s != '\n' && *s != '\r') {
start_batch_changes();
while (cur_col < msg_col) { while (cur_col < msg_col) {
if (capture_ga) { if (capture_ga) {
ga_concat_len(capture_ga, " ", 1); ga_concat_len(capture_ga, " ", 1);
@@ -2535,6 +2536,7 @@ static void redir_write(const char *const str, const ptrdiff_t maxlen)
} }
cur_col++; cur_col++;
} }
end_batch_changes();
} }
size_t len = maxlen == -1 ? STRLEN(s) : (size_t)maxlen; size_t len = maxlen == -1 ? STRLEN(s) : (size_t)maxlen;

View File

@@ -58,8 +58,8 @@ static yankreg_T *y_previous = NULL; /* ptr to last written yankreg */
static bool clipboard_didwarn_unnamed = false; static bool clipboard_didwarn_unnamed = false;
// for behavior between start_batch_changes() and end_batch_changes()) // for behavior between start_batch_changes() and end_batch_changes())
static bool clipboard_delay_update = false; // delay clipboard update
static int batch_change_count = 0; // inside a script static int batch_change_count = 0; // inside a script
static bool clipboard_delay_update = false; // delay clipboard update
static bool clipboard_needs_update = false; // clipboard was updated static bool clipboard_needs_update = false; // clipboard was updated
/* /*
@@ -5524,7 +5524,7 @@ int get_default_register_name(void)
} }
/// Determine if register `*name` should be used as a clipboard. /// Determine if register `*name` should be used as a clipboard.
/// In an unnammed operation, `*name` is `NUL` and will be adjusted to `'*'/'+'` if /// In an unnamed operation, `*name` is `NUL` and will be adjusted to */+ if
/// `clipboard=unnamed[plus]` is set. /// `clipboard=unnamed[plus]` is set.
/// ///
/// @param name The name of register, or `NUL` if unnamed. /// @param name The name of register, or `NUL` if unnamed.
@@ -5535,33 +5535,44 @@ int get_default_register_name(void)
/// if the register isn't a clipboard or provider isn't available. /// if the register isn't a clipboard or provider isn't available.
static yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing) static yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing)
{ {
if (*name == '*' || *name == '+') { #define MSG_NO_CLIP "clipboard: No provider. " \
if(!eval_has_provider("clipboard")) { "Try \":CheckHealth\" or \":h clipboard\"."
if (!quiet) {
EMSG("clipboard: No provider. Try \":CheckHealth\" or " yankreg_T *target = NULL;
"\":h clipboard\"."); bool explicit_cb_reg = (*name == '*' || *name == '+');
} bool implicit_cb_reg = (*name == NUL) && (cb_flags & CB_UNNAMEDMASK);
return NULL; int save_redir_off = redir_off;
} if (!explicit_cb_reg && !implicit_cb_reg) {
return &y_regs[*name == '*' ? STAR_REGISTER : PLUS_REGISTER]; goto end;
} else if ((*name == NUL) && (cb_flags & CB_UNNAMEDMASK)) { }
if(!eval_has_provider("clipboard")) {
if (!quiet && !clipboard_didwarn_unnamed) { if (!eval_has_provider("clipboard")) {
msg((char_u *)"clipboard: No provider. Try \":CheckHealth\" or " if (batch_change_count == 1 && explicit_cb_reg && !quiet) {
"\":h clipboard\"."); redir_off = true; // Avoid recursion from :redir + emsg().
clipboard_didwarn_unnamed = true; // Do NOT error (emsg()) here--if it interrupts :redir we get into
} // a weird state, stuck in "redirect mode".
return NULL; msg((char_u *)MSG_NO_CLIP);
} } else if (batch_change_count == 1 && implicit_cb_reg
if (writing && clipboard_delay_update) { && !quiet && !clipboard_didwarn_unnamed) {
clipboard_needs_update = true; redir_off = true; // Avoid recursion from :redir + emsg().
return NULL; msg((char_u *)MSG_NO_CLIP);
} else if (!writing && clipboard_needs_update) { clipboard_didwarn_unnamed = true;
// use the internal value }
return NULL; // ... else, be silent (avoid a flood of messages).
goto end;
}
if (explicit_cb_reg) {
target = &y_regs[*name == '*' ? STAR_REGISTER : PLUS_REGISTER];
goto end;
} else { // unnamed register: "implicit" clipboard
if (writing && clipboard_delay_update) {
clipboard_needs_update = true;
goto end;
} else if (!writing && clipboard_needs_update) {
goto end; // use the internal value
} }
yankreg_T *target;
if (cb_flags & CB_UNNAMEDPLUS) { if (cb_flags & CB_UNNAMEDPLUS) {
*name = (cb_flags & CB_UNNAMED && writing) ? '"': '+'; *name = (cb_flags & CB_UNNAMED && writing) ? '"': '+';
target = &y_regs[PLUS_REGISTER]; target = &y_regs[PLUS_REGISTER];
@@ -5569,10 +5580,12 @@ static yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing)
*name = '*'; *name = '*';
target = &y_regs[STAR_REGISTER]; target = &y_regs[STAR_REGISTER];
} }
return target; // unnamed register goto end;
} }
// don't do anything for other register names
return NULL; end:
redir_off = save_redir_off;
return target;
} }
static bool get_clipboard(int name, yankreg_T **target, bool quiet) static bool get_clipboard(int name, yankreg_T **target, bool quiet)
@@ -5740,7 +5753,7 @@ static void set_clipboard(int name, yankreg_T *reg)
(void)eval_call_provider("clipboard", "set", args); (void)eval_call_provider("clipboard", "set", args);
} }
/// Avoid clipboard (slow) during batch operations (i.e., a script). /// Avoid slow things (clipboard) during batch operations (while/for-loops).
void start_batch_changes(void) void start_batch_changes(void)
{ {
if (++batch_change_count > 1) { if (++batch_change_count > 1) {
@@ -5750,7 +5763,7 @@ void start_batch_changes(void)
clipboard_needs_update = false; clipboard_needs_update = false;
} }
/// Update the clipboard after batch changes finished. /// Counterpart to start_batch_changes().
void end_batch_changes(void) void end_batch_changes(void)
{ {
if (--batch_change_count > 0) { if (--batch_change_count > 0) {

View File

@@ -4,6 +4,7 @@ local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen') local Screen = require('test.functional.ui.screen')
local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert
local feed_command, expect, eq, eval = helpers.feed_command, helpers.expect, helpers.eq, helpers.eval local feed_command, expect, eq, eval = helpers.feed_command, helpers.expect, helpers.eq, helpers.eval
local command = helpers.command
local function basic_register_test(noblock) local function basic_register_test(noblock)
insert("some words") insert("some words")
@@ -80,15 +81,34 @@ local function basic_register_test(noblock)
expect("two and three and one") expect("two and three and one")
end end
describe('the unnamed register', function() describe('clipboard', function()
before_each(clear) before_each(clear)
it('works without provider', function()
it('unnamed register works without provider', function()
eq('"', eval('v:register')) eq('"', eval('v:register'))
basic_register_test() basic_register_test()
end) end)
it('`:redir @+>` with invalid g:clipboard shows error exactly once', function()
local screen = Screen.new(72, 8)
screen:attach()
command("let g:clipboard = 'bogus'")
feed_command('redir @+> | :call system("cat CONTRIBUTING.md") | redir END')
-- it is made empty
screen:expect([[
~ |
~ |
~ |
Error detected while processing function provider#clipboard#Executable: |
line 5: |
clipboard: invalid g:clipboard |
clipboard: No provider. Try ":CheckHealth" or ":h clipboard". |
Press ENTER or type command to continue^ |
]], nil, {{bold = true, foreground = Screen.colors.Blue}})
end)
end) end)
describe('clipboard usage', function() describe('clipboard', function()
local function reset(...) local function reset(...)
clear('--cmd', 'let &rtp = "test/functional/fixtures,".&rtp', ...) clear('--cmd', 'let &rtp = "test/functional/fixtures,".&rtp', ...)
end end
@@ -139,7 +159,7 @@ describe('clipboard usage', function()
eq({'some\ntext', '\nvery binary\n'}, eval("getreg('*', 1, 1)")) eq({'some\ntext', '\nvery binary\n'}, eval("getreg('*', 1, 1)"))
end) end)
it('support autodectection of regtype', function() it('autodetects regtype', function()
feed_command("let g:test_clip['*'] = ['linewise stuff','']") feed_command("let g:test_clip['*'] = ['linewise stuff','']")
feed_command("let g:test_clip['+'] = ['charwise','stuff']") feed_command("let g:test_clip['+'] = ['charwise','stuff']")
eq("V", eval("getregtype('*')")) eq("V", eval("getregtype('*')"))
@@ -169,7 +189,7 @@ describe('clipboard usage', function()
eq({{' much', 'ktext', ''}, 'b'}, eval("g:test_clip['+']")) eq({{' much', 'ktext', ''}, 'b'}, eval("g:test_clip['+']"))
end) end)
it('supports setreg', function() it('supports setreg()', function()
feed_command('call setreg("*", "setted\\ntext", "c")') feed_command('call setreg("*", "setted\\ntext", "c")')
feed_command('call setreg("+", "explicitly\\nlines", "l")') feed_command('call setreg("+", "explicitly\\nlines", "l")')
feed('"+P"*p') feed('"+P"*p')
@@ -187,7 +207,7 @@ describe('clipboard usage', function()
]]) ]])
end) end)
it('supports let @+ (issue #1427)', function() it('supports :let @+ (issue #1427)', function()
feed_command("let @+ = 'some'") feed_command("let @+ = 'some'")
feed_command("let @* = ' other stuff'") feed_command("let @* = ' other stuff'")
eq({{'some'}, 'v'}, eval("g:test_clip['+']")) eq({{'some'}, 'v'}, eval("g:test_clip['+']"))