diff --git a/.gitmodules b/.gitmodules index 473c3044..cde56e2f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "spec/support/vim-elixir"] path = spec/support/vim-elixir url = https://github.com/elixir-editors/vim-elixir +[submodule "spec/support/R-Vim-runtime"] + path = spec/support/R-Vim-runtime + url = https://github.com/jalvesaq/R-Vim-runtime diff --git a/autoload/sj/r.vim b/autoload/sj/r.vim new file mode 100644 index 00000000..acb5ba7d --- /dev/null +++ b/autoload/sj/r.vim @@ -0,0 +1,198 @@ +" Only real syntax that's interesting is cParen and cConditional +let s:skip = sj#SkipSyntax(['rComment']) + +" function! sj#r#SplitFuncall() +" +" Split the R function call if the cursor lies within the arguments of a +" function call +" +function! sj#r#SplitFuncall() + if !s:IsValidSelection("va(") + return 0 + endif + + call sj#PushCursor() + let items = s:ParseJsonFromMotion("va(\vi(") + let items = map(items, {k, v -> v . (k+1 < len(items) ? "," : "")}) + + let r_indent_align_args = get(g:, 'r_indent_align_args', 1) + if r_indent_align_args && len(items) + let items[0] = "(" . items[0] + let items[-1] = items[-1] . ")" + let lines = items + else + let lines = ["("] + items + [")"] + endif + + call sj#PopCursor() + call s:ReplaceMotionPreserveCursor('va(', lines) + + return 1 +endfunction + +" function! sj#r#JoinFuncall() +" +" Join an R function call if the cursor lies within the arguments of a +" function call +" +function! sj#r#JoinFuncall() + if !s:IsValidSelection("va(") + return 0 + endif + + call sj#PushCursor() + + let existing_text = sj#GetMotion("va(\vi(") + let items = s:ParseJsonObject(existing_text) + let text = join(items, ", ") + + " if replacement wouldn't have any effect, fail to attempt a latter callback + if text == existing_text + return 0 + endif + + call sj#PopCursor() + call s:ReplaceMotionPreserveCursor("va(", ["(" . text . ")"]) + + return 1 +endfunction + +" function! sj#r#JoinSmart() +" +" Reexecute :SplitjoinJoin at the end of the line, where it is more likely +" to find a code block relevant to being joined. +" +function! sj#r#JoinSmart() + try + call sj#PushCursor() + + let cur_pos = getpos(".") + silent normal! $ + let end_pos = getpos(".") + + if cur_pos[1:2] != end_pos[1:2] + execute ":SplitjoinJoin" + return 1 + else + return 0 + endif + finally + call sj#PopCursor() + endtr +endfunction + +" function! s:DoMotion(motion) +" +" Perform a normal-mode motion command +" +function s:DoMotion(motion) + call sj#PushCursor() + execute "silent normal! " . a:motion . "\" + execute "silent normal! \" + call sj#PopCursor() +endfunction + +" function! s:MoveCursor(lines, cols) +" +" Reposition cursor given relative lines offset and columns from the start of +" the line +" +function! s:MoveCursor(lines, cols) + let y = a:lines > 0 ? a:lines . 'j^' : a:lines < 0 ? a:lines . 'k^' : '' + let x = a:cols > 0 ? a:cols . 'l' : a:cols < 0 ? a:cols . 'h' : '' + let motion = y . x + if len(motion) + execute 'silent normal! ' . motion + endif +endfunction + +" function! s:ParseJsonObject(text) +" +" Wrapper around sj#argparser#js#Construct to simply parse a given string +" +function! s:ParseJsonObject(text) + let parser = sj#argparser#js#Construct(0, len(a:text), a:text) + call parser.Process() + return parser.args +endfunction + +" function! s:ParseJsonFromMotion(motion) +" +" Parse a json object from the visual selection of a given normal-mode motion +" string +" +function! s:ParseJsonFromMotion(motion) + let text = sj#GetMotion(a:motion) + return s:ParseJsonObject(text) +endfunction + +" function! s:IsValidSelection(motion) +" +" Test whether a visual selection contains more than a single character after +" performing the given normal-mode motion string +" +function! s:IsValidSelection(motion) + call s:DoMotion(a:motion) + return getpos("'<") != getpos("'>") +endfunction + +" function! s:ReplaceMotionPreserveCursor(motion, rep) {{{2 +" +" Replace the normal mode "motion" selection with a list of replacement lines, +" "rep", separated by line breaks, Assuming the non-whitespace content of +" "motion" is identical to the non-whitespace content of the joined lines of +" "rep", the cursor will be repositioned to the resulting location of the +" current character under the cursor. +" +function! s:ReplaceMotionPreserveCursor(motion, rep) + " default to interpretting all lines of text as originally from text to replace + let rep = a:rep + + " do motion and get bounds & text + call s:DoMotion(a:motion) + let ini = split(sj#GetByPosition(getpos("'<"), getpos(".")), "\n") + let ini = map(ini, {k, v -> sj#Ltrim(v)}) + + " do replacement + let body = join(a:rep, "\n") + call sj#ReplaceMotion(a:motion, body) + + " go back to start of selection + silent normal! `< + + " try to reconcile initial selection against replacement lines + let [cursory, cursorx, leading_ws] = [0, 0, 0] + while len(ini) && len(rep) + let i = stridx(ini[0], rep[0]) + let j = stridx(rep[0], ini[0]) + if i >= 0 + " if an entire line of the replacement text found in initial then we'll + " need our cursor to move to the next line if more lines are insered + let ini[0] = sj#Ltrim(ini[0][i+len(rep[0]):]) + let cursorx += i + len(rep[0]) + let ini = len(ini[0]) ? ini : ini[1:] + let rep = rep[1:] + if len(ini) + let cursory += 1 + let cursorx = 0 + endif + elseif j >= 0 + " if an entire line of the initial is found in the replacement then + " we'll need our cursor to move rightward through length of the initial + let rep[0] = rep[0][j+len(ini[0]):] + let leading_ws = len(rep[0]) + let rep[0] = sj#Ltrim(rep[0]) + let leading_ws = leading_ws - len(rep[0]) + let cursorx += j + len(ini[0]) + let ini = ini[1:] + let cursorx += (len(ini) && len(ini[0]) ? leading_ws : 0) + else + let ini = [] + endif + endwhile + + call s:MoveCursor(cursory, max([cursorx-1, 0])) + call sj#PushCursor() +endfunction + + diff --git a/doc/splitjoin.txt b/doc/splitjoin.txt index 33464047..2327fec9 100644 --- a/doc/splitjoin.txt +++ b/doc/splitjoin.txt @@ -22,6 +22,7 @@ CONTENTS *splitjoin* *splitjoin-content Perl...................................: |splitjoin-perl| PHP....................................: |splitjoin-php| Python.................................: |splitjoin-python| + R......................................: |splitjoin-r| Ruby...................................: |splitjoin-ruby| Rust...................................: |splitjoin-rust| SCSS/Less..............................: |splitjoin-scss| |splitjoin-less| @@ -786,6 +787,25 @@ Note that splitting `a, b = b, a` would not result in an expression that works the same way, due to the special handling by python of this case to swap two values. +============================================================================== +R *splitjoin-r* + +Function calls ~ +> + print(1, 2, 3) + + # with g:r_indent_align_args = 0 + print( + 1, + 2, + 3 + ) + + # with g:r_indent_align_args = 1 + print(1, + 2, + 3) +< ============================================================================== RUBY *splitjoin-ruby* diff --git a/ftplugin/r/splitjoin.vim b/ftplugin/r/splitjoin.vim new file mode 100644 index 00000000..b19f1708 --- /dev/null +++ b/ftplugin/r/splitjoin.vim @@ -0,0 +1,12 @@ +if !exists('b:splitjoin_split_callbacks') + let b:splitjoin_split_callbacks = [ + \ 'sj#r#SplitFuncall' + \ ] +endif + +if !exists('b:splitjoin_join_callbacks') + let b:splitjoin_join_callbacks = [ + \ 'sj#r#JoinFuncall', + \ 'sj#r#JoinSmart' + \ ] +endif diff --git a/spec/plugin/r_spec.rb b/spec/plugin/r_spec.rb new file mode 100644 index 00000000..5515cb2f --- /dev/null +++ b/spec/plugin/r_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe "R" do + let(:filename) { 'test.r' } + + before :each do + vim.set(:expandtab) + vim.set(:shiftwidth, 2) + end + + after :each do + vim.command('silent! unlet g:r_indent_align_args') + end + + specify "function calls with align_args = 0" do + vim.command('let g:r_indent_align_args = 0') + + set_file_contents 'print(1, a = 2, 3)' + vim.search('1,') + split + + assert_file_contents <<~EOF + print( + 1, + a = 2, + 3 + ) + EOF + + join + + assert_file_contents 'print(1, a = 2, 3)' + end + + specify "function calls with align_args = 1" do + vim.command('let g:r_indent_align_args = 1') + + set_file_contents 'print(1, a = 2, 3)' + vim.search('1,') + split + + assert_file_contents <<~EOF + print(1, + a = 2, + 3) + EOF + + join + + assert_file_contents 'print(1, a = 2, 3)' + end + + specify "function calls with nested calls" do + vim.command('let g:r_indent_align_args = 1') + set_file_contents 'print(1, c(1, 2, 3), 3)' + + # On start of nested function + vim.search('c(') + split + + assert_file_contents <<~EOF + print(1, + c(1, 2, 3), + 3) + EOF + + join + + assert_file_contents 'print(1, c(1, 2, 3), 3)' + + # Inside nested function + vim.search('c(') + vim.search('1,') + split + + assert_file_contents <<~EOF + print(1, c(1, + 2, + 3), 3) + EOF + + join + + assert_file_contents 'print(1, c(1, 2, 3), 3)' + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 13c67bef..0b5e0342 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,6 +16,7 @@ vim.prepend_runtimepath(plugin_path.join('spec/support/vim-javascript')) vim.prepend_runtimepath(plugin_path.join('spec/support/vim-elm-syntax')) vim.prepend_runtimepath(plugin_path.join('spec/support/vim-elixir')) + vim.prepend_runtimepath(plugin_path.join('spec/support/R-Vim-runtime')) # Alignment tool for alignment tests: vim.add_plugin(plugin_path.join('spec/support/tabular'), 'plugin/Tabular.vim') diff --git a/spec/support/R-Vim-runtime b/spec/support/R-Vim-runtime new file mode 160000 index 00000000..8060cc15 --- /dev/null +++ b/spec/support/R-Vim-runtime @@ -0,0 +1 @@ +Subproject commit 8060cc1561a37b479f3c95142cc7081918faa346