diff --git a/autoload/esearch/buf.vim b/autoload/esearch/buf.vim index 3982cbd95..4bfc841a1 100644 --- a/autoload/esearch/buf.vim +++ b/autoload/esearch/buf.vim @@ -117,16 +117,6 @@ fu! esearch#buf#rename_qf(name) abort let w:quickfix_title = a:name endfu -fu! s:bufdo(bufnr, cmd, bang) abort - let cur_buffer = esearch#buf#stay() - try - exe (bufnr('%') == a:bufnr ? '' : a:bufnr.'bufdo ') . a:cmd . (a:bang ? '!' : '') - return 1 - catch | call esearch#util#warn(v:exception) | return 0 - finally | call cur_buffer.restore() - endtry -endfu - fu! esearch#buf#import() abort return copy(s:Buf) endfu @@ -155,6 +145,16 @@ else endfu endif +fu! s:Buf.bufdo(cmd, ...) abort dict + let cur_buffer = esearch#buf#stay() + try + exe (bufnr('%') == self.bufnr ? '' : self.bufnr.'bufdo ') . a:cmd . (a:0 && a:1 ? '!' : '') + return 1 + catch | call esearch#util#warn(v:exception) | return 0 + finally | call cur_buffer.restore() + endtry +endfu + if exists('*nvim_buf_line_count') fu! s:Buf.oneliner() abort dict return nvim_buf_line_count(self.bufnr) == 1 @@ -170,7 +170,7 @@ else endif fu! s:Buf.goto() abort dict - exe 'buffer!' self.bufnr + exe 'buffer!' self.bufnr endfu fu! s:Buf.getline(lnum) abort dict @@ -195,7 +195,7 @@ fu! s:Buf.deleteline(lnum) abort dict endfu fu! s:Buf.write(bang) dict abort - return s:bufdo(self.bufnr, 'write', a:bang) + return self.bufdo('write', a:bang) endfu fu! s:Buf.open(opener, ...) dict abort @@ -204,11 +204,11 @@ fu! s:Buf.open(opener, ...) dict abort endfu fu! s:Buf.bdelete(...) dict abort - return s:bufdo(self.bufnr, 'bdelete', get(a:, 1)) + return self.bufdo('bdelete', get(a:, 1)) endfu fu! s:Buf.bwipeout(...) dict abort - return s:bufdo(self.bufnr, 'bwipeout', get(a:, 1)) + return self.bufdo('bwipeout', get(a:, 1)) endfu fu! esearch#buf#stay() abort @@ -222,5 +222,5 @@ fu! s:CurrentBufferGuard.new() abort dict endfu fu! s:CurrentBufferGuard.restore() abort dict - if self.bufnr != bufnr('') | exe self.bufnr 'buffer!' | endif + if self.bufnr != bufnr('') | noau exe self.bufnr 'buffer!' | endif endfu diff --git a/autoload/esearch/out/win.vim b/autoload/esearch/out/win.vim index c458ca35f..1a169a70c 100644 --- a/autoload/esearch/out/win.vim +++ b/autoload/esearch/out/win.vim @@ -31,6 +31,7 @@ let g:esearch#out#win#filename_re = '^[^ ]' let g:esearch#out#win#separator_re = '^$' let g:esearch#out#win#linenr_fmt = ' %3d ' let g:esearch#out#win#entry_fmt = ' %3d %s' +let g:esearch#out#win#entry_with_sign_fmt = ' %s %3d %s' let g:esearch#out#win#searches_with_stopped_highlights = esearch#cache#expiring#new({'max_age': 120, 'size': 1024}) diff --git a/autoload/esearch/out/win/diff.vim b/autoload/esearch/out/win/diff.vim index 40c41205a..ad9ea95a0 100644 --- a/autoload/esearch/out/win/diff.vim +++ b/autoload/esearch/out/win/diff.vim @@ -1,9 +1,14 @@ -let s:Dict = vital#esearch#import('Data.Dict') -let s:by_key = function('esearch#util#by_key') +let s:Dict = vital#esearch#import('Data.Dict') +let s:by_key = function('esearch#util#by_key') +let s:Filepath = vital#esearch#import('System.Filepath') +let s:String = vital#esearch#import('Data.String') + +let s:path_separator = s:Filepath.separator() let s:broken_entry_fmt = 'Unexpected entry format at line %d. Must match /^ (sign)? (line_number) (text)/.' let s:broken_header_fmt = 'Broken header at line %d.' let s:unexpected_filename_fmt = 'Unexpected filename at line %d. Each filename must be preceded with a blank line separator.' +let s:unexpected_slash_fmt = 'Unexpected trailing "'.s:path_separator.'" in the filename at line %d. Forgot to insert a basename?' let s:unexpected_prepend_fmt = 'Unexpected "^" at line %d. Prepended lines must be placed before the base or appended lines.' let s:unexpected_append_fmt = 'Unexpected "+" at line %d. Appended lines must be placed after the base line.' let s:duplicate_set_fmt = 'Duplicate line number at line %d.' @@ -14,13 +19,13 @@ let s:unexpected_separator_fmt = 'Unexpected blank line parator at line %d.' let s:unexpected_sign_fmt = 'Unexpected sign at line %d.' fu! esearch#out#win#diff#do() abort - let stats = {'deleted': 0, 'modified': 0, 'added': 0, 'files': 0} + let stats = {'deleted': 0, 'modified': 0, 'added': 0, 'files': 0, 'renamed': 0} let diffs = {'by_id': {}, 'stats': stats} let iter = s:DiffsIterator.new(getline(1, '$'), b:esearch, stats) while iter.has_next() let diff = iter.next() - if !empty(diff.edits) + if !empty(diff) let diffs.by_id[diff.ctx.id] = diff let stats.files += 1 endif @@ -70,11 +75,11 @@ fu! s:DiffsIterator.next_deleted() abort dict let ctx = self.contexts[id] let [edits, deleted_lines_a] = [{}, copy(ctx.lines)] let [begin, lnums_b, texts_b] = [-1, [], []] - return s:Diff.new(self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b) + return s:Diff.new(self.stats, self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b) endfu -fu! s:DiffsIterator.next_modified() abort - let [filename_b, lnum_was, sign_was] = ['', -1, ''] " backtrack one line back +fu! s:DiffsIterator.next_modified() abort dict + let [filename_b, lnum_was, sign_was] = ['', -1, ''] " look one line back let [edits, deleted_lines_a, begin, lnums_b, texts_b] = [{}, {}, -1, [], []] while self.wlnum < len(self.lines) @@ -83,7 +88,7 @@ fu! s:DiffsIterator.next_modified() abort if empty(line) if empty(filename_b) | throw s:err(s:unexpected_separator_fmt, self.wlnum) | endif let self.wlnum += 1 - return s:Diff.new(self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b) + return s:Diff.new(self.stats, self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b, filename_b) endif if line[0] ==# ' ' @@ -135,9 +140,12 @@ fu! s:DiffsIterator.next_modified() abort let ctx = self.contexts[self.state[self.wlnum]] silent! unlet self.deleted_ctxs_a[ctx.id] - if !empty(self.lines[self.wlnum - 1]) || filename_b !=# fnameescape(ctx.filename) + if !empty(self.lines[self.wlnum - 1]) throw s:err(s:unexpected_filename_fmt, self.wlnum) endif + if s:String.ends_with(filename_b, s:path_separator) + throw s:err(s:unexpected_slash_fmt, self.wlnum) + endif let lines_a = ctx.lines let deleted_lines_a = copy(lines_a) @@ -147,7 +155,7 @@ fu! s:DiffsIterator.next_modified() abort let self.wlnum += 1 endwhile - return s:Diff.new(self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b) + return s:Diff.new(self.stats, self.add_deletes(edits, deleted_lines_a), begin, ctx, lnums_b, texts_b, filename_b) endfu fu! s:DiffsIterator.add_deletes(edits, deleted_lines_a) abort dict @@ -164,19 +172,32 @@ endfu let s:Diff = {} -fu! s:Diff.new(edits, begin, ctx, lnums_b, texts_b) abort - if empty(a:edits) | return {'edits': []} | endif - let edits = s:reverse_flatten(a:edits) - let win_undos = s:win_undos(a:ctx.lines, a:ctx, a:ctx.begin, a:lnums_b) - let [win_edits, lines_b] = s:win_write_post_edits(edits, a:lnums_b, a:texts_b, a:begin) - return { +fu! s:Diff.new(stats, edits, begin, ctx, lnums_b, texts_b, ...) abort dict + let diff = {} + + if a:0 && simplify(fnameescape(a:ctx.filename)) !=# simplify(a:1) + let diff.filename = esearch#shell#fnameunescape(a:1) + let a:stats.renamed += 1 + endif + + if empty(a:edits) + if empty(diff) | return diff | endif + + let [edits, win_undos, win_edits, lines_b] = [[], [], [], a:ctx.lines] + else + let edits = s:reverse_flatten(a:edits) + let win_undos = s:win_undos(a:ctx.lines, a:ctx, a:ctx.begin, a:lnums_b) + let [win_edits, lines_b] = s:win_write_post_edits(edits, a:lnums_b, a:texts_b, a:begin) + endif + + return extend(diff, { \ 'ctx': a:ctx, + \ 'begin': a:begin, \ 'edits': edits, \ 'win_edits': win_edits, \ 'win_undos': win_undos, - \ 'begin': a:begin, \ 'lines_b': lines_b, - \} + \}) endfu fu! s:win_write_post_edits(edits, lnums_b, texts_b, begin) abort diff --git a/autoload/esearch/out/win/modifiable.vim b/autoload/esearch/out/win/modifiable.vim index d4f7e5e2f..f0eacc7bb 100644 --- a/autoload/esearch/out/win/modifiable.vim +++ b/autoload/esearch/out/win/modifiable.vim @@ -55,16 +55,23 @@ fu! s:write_cmd() abort return s:Log.error(substitute(v:exception, '^DiffError:', '', '')) endtry - if diff.stats.files == 0 | echo 'Nothing to save' | return | endi - - let [kinds, total_changes] = [[], diff.stats.modified + diff.stats.deleted + diff.stats.added] - if diff.stats.added > 0 | let kinds += [diff.stats.added . ' added'] | endif - if diff.stats.modified > 0 | let kinds += [diff.stats.modified . ' modified'] | endif - if diff.stats.deleted > 0 | let kinds += [diff.stats.deleted . ' deleted'] | endif + if diff.stats.files == 0 | echo 'Nothing to save' | return | endif + + let stats = diff.stats + let [kinds, total_changes] = [[], stats.modified + stats.deleted + stats.added] + if stats.added > 0 | let kinds += [stats.added . ' added'] | endif + if stats.modified > 0 | let kinds += [stats.modified . ' modified'] | endif + if stats.deleted > 0 | let kinds += [stats.deleted . ' deleted'] | endif + if stats.renamed > 0 + let renamed = ', ' . stats.renamed . ' renamed ' . (stats.renamed == 1 ? 'file' : 'files') + else + let renamed = '' + endif - let message = printf('Write changes? (%s %s in %d %s)', + let message = printf('Write changes? (%s %s in %d %s%s)', \ join(kinds, ', '), total_changes == 1 ? 'line' : 'lines', - \ diff.stats.files, diff.stats.files == 1 ? 'file' : 'files') + \ stats.files, stats.files == 1 ? 'file' : 'files', + \ renamed) if !get(g:, 'esearch_yes') && confirm(message, "&Yes\n&Cancel") != 1 | return |endif call esearch#writer#do(diff, b:esearch, v:cmdbang) diff --git a/autoload/esearch/out/win/update.vim b/autoload/esearch/out/win/update.vim index 2fd5efc2d..b5ac466bd 100644 --- a/autoload/esearch/out/win/update.vim +++ b/autoload/esearch/out/win/update.vim @@ -189,4 +189,5 @@ fu! esearch#out#win#update#finish(bufnr) abort cal setbufvar(a:bufnr, '&modified', 0) cal esearch#out#win#modifiable#init() if es.win_ui_nvim_syntax | cal luaeval('esearch.buf_attach_ui()') | en + let es.linecount = line('$') endfu diff --git a/autoload/esearch/shell.vim b/autoload/esearch/shell.vim index 0a370cf4b..0293f4158 100644 --- a/autoload/esearch/shell.vim +++ b/autoload/esearch/shell.vim @@ -229,7 +229,12 @@ elseif g:esearch#has#vms else let s:path_esc_chars = " \t\n*?[{`$\\%#'\"|!<" endif +let s:path_unesc_re = '\\\(' . join(map(split(s:path_esc_chars, '\zs'), 'escape(v:val, ''^$~.*[]\'')'), '\|') . '\)' fu! s:fnameescape(string) abort return escape(a:string, s:metachars . s:path_esc_chars) endfu + +fu! esearch#shell#fnameunescape(fname) abort + return substitute(a:fname, s:path_unesc_re, '\1', 'g') +endfu diff --git a/autoload/esearch/util.vim b/autoload/esearch/util.vim index e10d7dd54..918bb318e 100644 --- a/autoload/esearch/util.vim +++ b/autoload/esearch/util.vim @@ -304,3 +304,19 @@ endfu fu! esearch#util#noop(...) abort return 0 endfu + +fu! esearch#util#file_exists(path) abort + let [dirname, basename] = [s:Filepath.dirname(a:path), s:Filepath.basename(a:path)] + return !empty(globpath(dirname, basename, 1)) +endfu + +fu! esearch#util#list2dict(list) abort + let [i, dict] = [0, {}] + + while i < len(a:list) + let dict[i + 1] = a:list[i] + let i += 1 + endwhile + + return dict +endfu diff --git a/autoload/esearch/writer.vim b/autoload/esearch/writer.vim index 481f0f466..024973561 100644 --- a/autoload/esearch/writer.vim +++ b/autoload/esearch/writer.vim @@ -1,3 +1,4 @@ +let s:Prelude = vital#esearch#import('Prelude') let s:Filepath = vital#esearch#import('System.Filepath') let s:List = vital#esearch#import('Data.List') let s:Buf = esearch#buf#import() @@ -11,24 +12,28 @@ let s:Writer = {} fu! s:Writer.new(diffs, esearch) abort dict return extend(deepcopy(self), { - \ 'diffs': a:diffs, - \ 'esearch': a:esearch, - \ 'search_buf': s:Buf.for(a:esearch.bufnr), - \ 'win_edits': [], - \ 'win_undos': [], - \ 'conflicts': [], + \ 'diffs': a:diffs, + \ 'esearch': a:esearch, + \ 'search_buf': s:Buf.for(a:esearch.bufnr), + \ 'win_edits': [], + \ 'win_undos': [], + \ 'state_undos': [], + \ 'renames': [], + \ 'conflicts': [], \}) endfu fu! s:Writer.write(bang) abort dict + let contexts = self.esearch.contexts + let linecount = self.esearch.linecount let l:WriteCb = self.esearch.write_cb let [current_window, current_buffer, view] = [esearch#win#stay(), esearch#buf#stay(), winsaveview()] call esearch#util#doautocmd('User esearch_write_pre') - " Wipeout preview buffer if was viewed ignoring swap to prevent missing - " swapexists messages later + " Wipeout preview buffer if was viewed ignoring swap to prevent missing + " swapexists messages later call esearch#preview#wipeout() - aug esearch_write + aug __esearch_write__ au! au SwapExists * let v:swapchoice = 'q' aug END @@ -48,15 +53,21 @@ fu! s:Writer.write(bang) abort dict if !self.verify_not_modified(diff, buf) | continue | endif endif - let self.win_edits = diff.win_edits + self.win_edits - let self.win_undos = self.update_file(buf, diff) + self.win_undos - let self.esearch.contexts[diff.ctx.id].lines = diff.lines_b - let self.esearch.contexts[diff.ctx.id].begin = diff.begin + let self.win_edits = diff.win_edits + self.win_edits + let self.win_undos = self.update_file(buf, diff) + self.win_undos + let contexts[diff.ctx.id].lines = diff.lines_b + let contexts[diff.ctx.id].begin = diff.begin + + if has_key(diff, 'filename') + call add(self.renames, [buf, contexts[diff.ctx.id], diff.filename]) + endif if !empty(WriteCb) | call WriteCb(buf, a:bang) | endif endfor + + call self.rename(a:bang) finally - au! esearch_write + au! __esearch_write__ call current_window.restore() call current_buffer.restore() call self.create_undo_entry() @@ -68,6 +79,105 @@ fu! s:Writer.write(bang) abort dict call esearch#util#try_defer(function('esearch#util#doautocmd'), 'User esearch_write_post') endfu +fu! s:Writer.rename(bang) abort + let contexts = self.esearch.contexts + let linecount = self.esearch.linecount + let edits = [] + + for [buf_a, ctx_a, relative_filename_b] in self.renames + let filename_a = simplify(esearch#util#abspath(self.esearch.cwd, ctx_a.filename)) + let filename_b = simplify(esearch#util#abspath(self.esearch.cwd, relative_filename_b)) + let filename_a = s:Prelude.substitute_path_separator(filename_a) + let dir_b = s:Prelude.substitute_path_separator(fnamemodify(filename_b, ':h')) + let filename_b = s:Prelude.substitute_path_separator(filename_b) + + if esearch#util#file_exists(filename_b) + if !a:bang + if has_key(g:, 'esearch_overwrite') + if !g:esearch_overwrite | continue | endif + else + if confirm(filename_b . ' exists. Overwrite?', "&Yes\n&Cancel") == 2 | continue | endif + endif + endif + + if isdirectory(filename_b) + return add(self.conflicts, [relative_filename_b, "can't overwite the directory"]) + endif + if filereadable(filename_b) + " TODO edits applying orders when :undo is used + return add(self.conflicts, [relative_filename_b, "can't overwite the file"]) + let overrided_lines_b = readfile(filename_b) + endif + endif + + let edits += [{'func': 'mkdir', 'args': [dir_b, 'p']}] + let edits += [{'func': 'rename', 'args': [filename_a, filename_b]}] + if bufloaded(buf_a.bufnr) + let edits += [{'func': 's:rename_buffer', 'args': [buf_a, filename_b]}] + endif + + if exists('overrided_lines_b') + let win_lines_b = s:win_lines_b(relative_filename_b, overrided_lines_b) + let edits += [{'func': 's:add_context', 'args': [self.esearch, linecount, relative_filename_b, win_lines_b, overrided_lines_b]}] + let edits += [{'func': 's:add_undos', 'args': [self.esearch, self.win_undos, self.state_undos, win_lines_b]}] + let linecount += len(win_lines_b) + unlet overrided_lines_b + endif + + let ctx_a.filename = relative_filename_b + endfor + let edits += [{'func': 's:set_linecount', 'args': [self.esearch, linecount]}] + + + for edit in edits + call call(edit.func, edit.args) + endfor +endfu + +fu! s:win_lines_b(relative_filename_b, overrided_lines_b) abort + let fmt = g:esearch#out#win#entry_with_sign_fmt + let i = 0 + let win_lines_b = copy(a:overrided_lines_b) + while i < len(win_lines_b) + let win_lines_b[i] = printf(fmt, '', i + 1, win_lines_b[i]) + let i += 1 + endw + return ['', fnameescape(a:relative_filename_b)] + win_lines_b +endfu + +fu! s:add_undos(esearch, win_undos, state_undos, win_lines_b) abort + let ids = [a:esearch.contexts[-2].id] + repeat([a:esearch.contexts[-1].id], len(a:win_lines_b) - 1) + call extend(a:win_undos, [{'func': 'appendline', 'args': ['$', a:win_lines_b]}]) + call extend(a:state_undos, [{'func': 'extend', 'args': [ids]}]) +endfu + +fu! s:add_context(esearch, linecount, relative_filename_b, win_lines_b, overrided_lines_b) abort + let [begin, end] = [a:linecount + 2, a:linecount + len(a:win_lines_b)] + call esearch#out#win#update#add_context(a:esearch.contexts, a:relative_filename_b, begin, 0) + let a:esearch.contexts[-1].lines = esearch#util#list2dict(a:overrided_lines_b) + let a:esearch.contexts[-1].end = end +endfu + +fu! s:set_linecount(esearch, linecount) abort + let a:esearch.linecount = a:linecount +endfu + +fu! s:rename_buffer(buf_a, filename_b) abort + " commands are inspired by defx.nvim + let hidden = esearch#let#restorable({'&l:hidden': 1}) + silent call a:buf_a.bufdo('silent! saveas! ' . fnameescape(a:filename_b) . '| bdelete! # ') + call hidden.restore() +endfu + +fu! s:Writer.update_state(state, edits) abort + let state = a:state + + for edit in a:edits + call call(edit.func, [state] + edit.args) + endfor + return state +endfu + fu! s:Writer.create_undo_entry() abort dict let original_register = @s call self.search_buf.goto() @@ -78,7 +188,7 @@ fu! s:Writer.create_undo_entry() abort dict exe 'undo' self.esearch.undotree.written.changenr call self.update_win(self.win_undos) - let undone_state = self.esearch.undotree.written.state + let undone_state = self.update_state(self.esearch.undotree.written.state, self.state_undos) call self.esearch.undotree.squash(undone_state) call esearch#util#squash_undo() keepjumps %delete _ @@ -96,6 +206,8 @@ fu! s:Writer.update_file(buf, diff) abort call a:buf.goto() let edits = a:diff.edits + if empty(edits) | return a:diff.win_undos | endif + for edit in edits[:-2] call call(a:buf[edit.func], edit.args) endfor @@ -116,7 +228,7 @@ fu! s:Writer.update_win(win_edits) abort endfu fu! s:Writer.handle_existing_swap(path) abort dict - call add(self.conflicts, {'filename': a:path, 'reason': 'swapfile exists'}) + call add(self.conflicts, [a:path, 'swapfile exists']) " Ensure the buffer is deleted after quitting the swap prompt if bufexists(a:path) @@ -134,17 +246,17 @@ fu! s:Writer.log() abort dict return setbufvar(self.esearch.bufnr, '&modified', 0) end - let reasons = map(self.conflicts, 'printf("\n %s (%s)", v:val.filename, v:val.reason)') - let message = "Can't write changes to the following files:".join(reasons, '') + let reasons = map(self.conflicts, 'printf("\n %s (%s)", v:val[0], v:val[1])') + let message = "Can't apply changes for following files:".join(reasons, '') call esearch#util#warn(message) endfu fu! s:Writer.verify_readable(diff, path) abort dict if filereadable(a:path) | return 1 | endif if get(a:diff.ctx, 'rev') - call add(self.conflicts, {'filename': a:path, 'reason': 'is a git blob'}) + call add(self.conflicts, [a:path, 'is a git blob']) else - call add(self.conflicts, {'filename': a:path, 'reason': 'is not readable'}) + call add(self.conflicts, [a:path, 'is not readable']) endif return 0 endfu @@ -156,10 +268,7 @@ fu! s:Writer.verify_not_modified(diff, buf) abort dict continue endif - call add(self.conflicts, { - \ 'filename': a:diff.ctx.filename, - \ 'reason': 'line '.edit.args[0].' has changed', - \}) + call add(self.conflicts, [a:diff.ctx.filename, 'line '.edit.args[0].' has changed']) return 0 endfor diff --git a/spec/helper.vader b/spec/helper.vader index 8c9b93974..d6526aad4 100644 --- a/spec/helper.vader +++ b/spec/helper.vader @@ -29,28 +29,35 @@ Execute(global setup): endfu fu! UndotreeIsConsistent() abort - let lines = getline(1, '$') - let g:state = [0] - let id = 0 - if lines !=# [''] - if get(lines, 0, '') =~# '^Matches' | let g:state += [0] | endif - if get(lines, 1, 0) is# '' | let g:state += [0] | endif + try + let lines = getline(1, '$') + let g:state = [0] + let id = 0 + if lines !=# [''] + if get(lines, 0, '') =~# '^Matches' | let g:state += [0] | endif + if get(lines, 1, 0) is# '' | let g:state += [0] | endif - for line in lines[2:] - if line =~# g:esearch#out#win#filename_re - let id = filter(copy(b:esearch.contexts), 'fnameescape(v:val.filename) == line')[0].id - endif - call add(g:state, id) - endfor - endif + for line in lines[2:] + if line =~# g:esearch#out#win#filename_re + let id = filter(copy(b:esearch.contexts), 'fnameescape(v:val.filename) == line')[0].id + endif + call add(g:state, id) + endfor + endif - let g:state = map(g:state, '+v:val') - let head_state = map(copy(b:esearch.undotree.head.state), '+v:val') - return head_state ==# g:state && (len(head_state) - 1 == line('$') || head_state == [0]) + let g:state = map(g:state, '+v:val') + let head_state = map(copy(b:esearch.undotree.head.state), '+v:val') + return head_state ==# g:state && (len(head_state) - 1 == line('$') || head_state == [0]) + catch + let g:exception = v:exception + return 0 + endtry endfu fu! UndotreeIsInconsistentMsg() abort - return 'undotree: '.string(g:state).string(b:esearch.state) + let exception = get(g:, 'exception') + unlet! g:exception + return 'undotree: '.string(g:state).string(b:esearch.state).string(exception) endfu " linear congruential generator diff --git a/spec/rename.vader b/spec/rename.vader new file mode 100644 index 000000000..a0891917e --- /dev/null +++ b/spec/rename.vader @@ -0,0 +1,180 @@ +Include: helper.vader + +Before: + let g:esearch.cwd = 'spec/fixtures/rename'.g:test_number.next().'/' + let g:esearch.pattern = 'l\d' + let g:content = ['l1', 'l2'] + let g:file_a = g:esearch.cwd.'file.txt' + let g:file_b = g:esearch.cwd.'elif.txt' + exe 'bwipe' Fixture(g:file_a, g:content) + Save g:esearch_overwrite +After: + messages clear + Restore g:esearch_overwrite + silent! call delete(g:file_a) + silent! call delete(g:file_b) + silent! call delete(g:file_b, 'd') + +Execute (Rename non-opened): + call esearch#init() + exe "norm /file.txt\ciwelif" + write +Then: + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + undo | doau TextChanged | write + AssertEqual readfile(g:file_a), g:content + Assert !filereadable(g:file_b) + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + undo | doau TextChanged | write + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + +Execute (Rename opened with :split): + call esearch#init() + exe "norm S" + exe "norm /file.txt\ciwelif" + write +Then: + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + AssertEqual getbufline(g:file_b, 1, '$'), g:content + undo | doau TextChanged | write + AssertEqual readfile(g:file_a), g:content + AssertEqual getbufline(g:file_a, 1, '$'), g:content + Assert !filereadable(g:file_b) + undo | doau TextChanged | write + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + AssertEqual getbufline(g:file_b, 1, '$'), g:content + +Execute (Rename opened and then closed): + call esearch#init() + exe "norm S" + exe "norm /file.txt\ciwelif" + write +Then: + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + AssertEqual getbufline(g:file_b, 1, '$'), g:content + exe bufwinnr(g:file_b) 'close' + undo | doau TextChanged | write + AssertEqual readfile(g:file_a), g:content + Assert !bufloaded(g:file_a) + Assert !filereadable(g:file_b) + undo | doau TextChanged | write + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + Assert !bufloaded(g:file_b) + +Execute (Rename into nonexisting directory): + call esearch#init() + let g:file_b = g:esearch.cwd.'elif/nested/dir.txt' + exe "norm /file.txt\ccelif/nested/dir.txt" + write +Then: + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + undo | doau TextChanged | write + AssertEqual readfile(g:file_a), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + Assert !filereadable(g:file_b) + undo | doau TextChanged | write + Assert !filereadable(g:file_a) + AssertEqual readfile(g:file_b), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + +Execute (Rename with overwriting a file and say (N)o): + let g:file_b = g:esearch.cwd.'elif/nested/dir.txt' + exe 'bwipe' Fixture(g:file_b, 'contentb') + call esearch#init() + exe "norm /file.txt\ccelif/nested/dir.txt" +Then: + let g:esearch_overwrite = 0 + write + AssertEqual readfile(g:file_a), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + AssertEqual readfile(g:file_b), ['contentb'] + +Execute (Rename with overwriting a dir using :write!): + let g:file_b = g:esearch.cwd.'elif/nested/dir' + call mkdir(g:file_b, 'p') + call esearch#init() + exe "norm /file.txt\ccelif/nested/dir" +Then: + write! + AssertEqual readfile(g:file_a), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + Assert split(execute('messages'), "\n")[-1] =~# "can't overwite the directory" + +Execute (Rename with overwriting a dir and say (N)o): + let g:file_b = g:esearch.cwd.'elif/nested/dir' + call mkdir(g:file_b, 'p') + call esearch#init() + exe "norm /file.txt\ccelif/nested/dir" +Then: + let g:esearch_overwrite = 0 + write + AssertEqual readfile(g:file_a), g:content + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + Assert !filereadable(g:file_b) + +" Execute (Rename with overwriting a file using :write!): +" let g:file_b = g:esearch.cwd.'elif/nested/dir.txt' +" exe 'bwipe' Fixture(g:file_b, 'contentb') +" call esearch#init() +" exe "norm /file.txt\ccelif/nested/dir.txt" +" Then: +" write! +" Assert !filereadable(g:file_a) +" AssertEqual readfile(g:file_b), g:content +" Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) +" undo | doau TextChanged | write +" " Assert filereadable(g:file_b), 'TODO can be fixed in the future' +" AssertEqual readfile(g:file_a), g:content +" Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + +" Execute (Rename with overwriting a file and say (Y)es): +" let g:file_b = g:esearch.cwd.'elif/nested/dir.txt' +" exe 'bwipe' Fixture(g:file_b, 'contentb') +" call esearch#init() +" exe "norm /file.txt\ccelif/nested/dir.txt" +" Then: +" let g:esearch_overwrite = 1 +" write +" Assert !filereadable(g:file_a) +" AssertEqual readfile(g:file_b), g:content +" Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) +" undo | doau TextChanged | write +" Assert !filereadable(g:file_b), "TODO can be fixed in the future" +" AssertEqual readfile(g:file_a), g:content +" Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + +" Execute (Rename with overwriting a dir and say (Y)es): +" finish " TODO +" let g:file_b = g:esearch.cwd.'elif/nested/dir' +" call mkdir(g:file_b, 'p') +" call esearch#init() +" exe "norm /file.txt\ccelif/nested/dir" +" Then: +" let g:esearch_overwrite = 1 +" write +" AssertEqual readfile(g:file_a), g:content +" Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) +" Assert split(execute('messages'), "\n")[-1] =~# "can't overwite the directory" + +Execute (Rename deleted after the search is done): + let g:file_b = g:esearch.cwd.'elif/nested/dir' + call mkdir(g:file_b, 'p') + call esearch#init() + call delete(g:file_a) + exe "norm /file.txt\ccelif/nested/dir" +Then: + let g:esearch_overwrite = 1 + write + Assert !filereadable(g:file_a) && !filereadable(g:file_b) + Assert !bufloaded(g:file_a) && !bufloaded(g:file_b) + Assert split(execute('messages'), "\n")[-1] =~# "is not readable" diff --git a/spec/support/helpers/open.rb b/spec/support/helpers/open.rb deleted file mode 100644 index 730ed284c..000000000 --- a/spec/support/helpers/open.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# For testing opening entries of both window and quickfix output - -module Helpers::Open - extend RSpec::Matchers::DSL - - def tabpage_buffers_list - editor.echo func('tabpagebuflist') - end - - def tabpage_windows_list - (1..editor.echo(func('tabpagewinnr', func('tabpagenr'), '$'))).to_a - end - - def tabpages_list - (1..editor.echo(func('tabpagenr', '$'))).to_a - end - - def start_editing(path) - change { editor.current_buffer_name } - .to(path.to_s) - end - - def windows - editor.echo func('nvim_list_wins') - end - - def open_window(path) - change { editor.current_buffer_name } - .to end_with path.to_s - end - - def open_tab(path) - change { tabpages_list.count } - .by(1) - .and change { editor.current_buffer_name } - .to end_with path - end -end