Commits

Audrius Kažukauskas  committed a3f33a7

Add sideways.vim plugin.

  • Participants
  • Parent commits 3dee0e3

Comments (0)

Files changed (7)

File autoload/sideways.vim

+function! sideways#Left(definitions)
+  let items = sideways#parsing#Parse(a:definitions)
+  if empty(items)
+    return 0
+  end
+
+  let last_index   = len(items) - 1
+  let active_index = s:FindActiveItem(items)
+  if active_index < 0
+    return 0
+  endif
+
+  if active_index == 0
+    let first             = items[active_index]
+    let second            = items[last_index]
+    let new_cursor_column = second[0] + s:Delta(second, first)
+  else
+    let first             = items[active_index - 1]
+    let second            = items[active_index]
+    let new_cursor_column = first[0]
+  endif
+
+  call s:Swap(first, second, new_cursor_column)
+  silent! call repeat#set(":call sideways#Left()\<cr>")
+  return 1
+endfunction
+
+function! sideways#Right(definitions)
+  let items = sideways#parsing#Parse(a:definitions)
+  if empty(items)
+    return 0
+  end
+
+  let last_index   = len(items) - 1
+  let active_index = s:FindActiveItem(items)
+  if active_index < 0
+    return 0
+  endif
+
+  if active_index == last_index
+    let first             = items[0]
+    let second            = items[last_index]
+    let new_cursor_column = first[0]
+  else
+    let first             = items[active_index]
+    let second            = items[active_index + 1]
+    let new_cursor_column = second[0] + s:Delta(second, first)
+  endif
+
+  call s:Swap(first, second, new_cursor_column)
+  silent! call repeat#set(":call sideways#Right()\<cr>")
+  return 1
+endfunction
+
+" Swaps the a:first and a:second items in the buffer. Both first arguments are
+" expected to be pairs of start and end columns. The last argument is a
+" number, the new column to position the cursor on.
+"
+" In order to avoid having to consider eventual changes in column positions,
+" a:first is expected to be positioned before a:second. Assuming that, the
+" function first places the second item and then the first one, ensuring that
+" the column number remain consistent until it's done.
+function! s:Swap(first, second, new_cursor_column)
+  let [first_start, first_end]   = a:first
+  let [second_start, second_end] = a:second
+
+  let first_body  = sideways#util#GetCols(first_start, first_end)
+  let second_body = sideways#util#GetCols(second_start, second_end)
+
+  let position = getpos('.')
+
+  call sideways#util#ReplaceCols(second_start, second_end, first_body)
+  call sideways#util#ReplaceCols(first_start, first_end, second_body)
+
+  let position[2] = a:new_cursor_column
+  call setpos('.', position)
+endfunction
+
+" Finds an item in the given list of column pairs, which the cursor is
+" currently positioned in.
+"
+" Returns the index of the found item, or -1 if it's not found.
+function! s:FindActiveItem(items)
+  let column = col('.')
+
+  let index = 0
+  for item in a:items
+    let [start, end] = item
+
+    if start <= column && column <= end
+      return index
+    endif
+
+    let index += 1
+  endfor
+
+  return -1
+endfunction
+
+" Return the difference in length between the first start-end column pair and
+" the second one.
+"
+" It is assumed that a:first is positioned before a:second. This is used to
+" account for the column positions becoming inconsistent after replacing text
+" in the current line.
+function! s:Delta(first, second)
+  return (a:first[1] - a:first[0]) - (a:second[1] - a:second[0])
+endfunction

File autoload/sideways/parsing.vim

+" Extract column positions for "arguments" on the current line. Returns a list
+" of pairs, each pair contains the start and end columns of the item
+"
+" Example:
+"
+" On the following line:
+"
+"   def function(one, two):
+"
+" The result would be:
+"
+"   [ [14, 16], [19, 21] ]
+"
+function! sideways#parsing#Parse(definitions)
+  let definitions = a:definitions
+  let items       = []
+
+  for definition in definitions
+    let start_pattern     = definition.start
+    let end_pattern       = definition.end
+    let delimiter_pattern = definition.delimiter
+    let skip_pattern      = definition.skip
+
+    let [opening_brackets, closing_brackets] = definition.brackets
+
+    silent! normal! zO
+    call sideways#util#PushCursor()
+
+    if searchpair(start_pattern, '', end_pattern, 'bW', '', line('.')) <= 0
+      call sideways#util#PopCursor()
+      continue
+    else
+      call search(start_pattern, 'Wce', line('.'))
+    endif
+
+    normal! l
+
+    let current_item = [col('.'), -1]
+    let remainder_of_line = s:RemainderOfLine()
+
+    " TODO (2012-09-07) Figure out how to work with RemainderOfLine
+    while s:RemainderOfLine() !~ '^'.end_pattern
+      let remainder_of_line = s:RemainderOfLine()
+      let bracket_match = s:BracketMatch(remainder_of_line, opening_brackets)
+
+      if bracket_match >= 0
+        " then try to jump to the closing bracket
+        let opening_bracket = opening_brackets[bracket_match]
+        let closing_bracket = closing_brackets[bracket_match]
+
+        call searchpair('\V'.opening_bracket, '', '\V'.closing_bracket, 'W', '', line('.'))
+        " move rightwards regardless of the result
+        normal! l
+      elseif remainder_of_line =~ delimiter_pattern
+        " then store the current item
+        let current_item[1] = col('.') - 1
+        call add(items, current_item)
+
+        normal! l
+
+        " skip some whitespace TODO consider removing
+        while s:RemainderOfLine() =~ skip_pattern
+          normal! l
+        endwhile
+
+        " initialize a new "current item"
+        let current_item = [col('.'), -1]
+      elseif col('.') == col('$') - 1
+        " then we're at the end of the line, finish up
+        let current_item[1] = col('$') - 1
+        break
+      else
+        " move rightwards
+        normal! l
+      endif
+    endwhile
+
+    if current_item[1] < 0
+      let current_item[1] = col('.') - 1
+    endif
+    call add(items, current_item)
+
+    if !empty(items)
+      call sideways#util#PopCursor()
+      break
+    endif
+
+    call sideways#util#PopCursor()
+  endfor
+
+  return items
+endfunction
+
+function! s:BracketMatch(text, brackets)
+  let index = 0
+  for char in split(a:brackets, '\zs')
+    if a:text[0] ==# char
+      return index
+    else
+      let index += 1
+    endif
+  endfor
+
+  return -1
+endfunction
+
+" Returns the remainder of the line from the current cursor position to the
+" end.
+function! s:RemainderOfLine()
+  return strpart(getline('.'), col('.') - 1)
+endfunction
+
+" Simple debugging
+function! s:DebugItems(items)
+  Decho a:items
+  Decho map(copy(a:items), 'sideways#util#GetCols(v:val[0], v:val[1])')
+endfunction

File autoload/sideways/util.vim

+" vim: foldmethod=marker
+
+" Cursor stack manipulation {{{1
+"
+" In order to make the pattern of saving the cursor and restoring it
+" afterwards easier, these functions implement a simple cursor stack. The
+" basic usage is:
+"
+"   call sideways#util#PushCursor()
+"   " Do stuff that move the cursor around
+"   call sideways#util#PopCursor()
+
+" function! sideways#util#PushCursor() {{{2
+"
+" Adds the current cursor position to the cursor stack.
+function! sideways#util#PushCursor()
+  if !exists('b:cursor_position_stack')
+    let b:cursor_position_stack = []
+  endif
+
+  call add(b:cursor_position_stack, getpos('.'))
+endfunction
+
+" function! sideways#util#PopCursor() {{{2
+"
+" Restores the cursor to the latest position in the cursor stack, as added
+" from the sideways#util#PushCursor function. Removes the position from the stack.
+function! sideways#util#PopCursor()
+  if !exists('b:cursor_position_stack')
+    let b:cursor_position_stack = []
+  endif
+
+  call setpos('.', remove(b:cursor_position_stack, -1))
+endfunction
+
+" function! sideways#util#PeekCursor() {{{2
+"
+" Returns the last saved cursor position from the cursor stack.
+" Note that if the cursor hasn't been saved at all, this will raise an error.
+function! sideways#util#PeekCursor()
+  return b:cursor_position_stack[-1]
+endfunction
+
+" Text replacement {{{1
+"
+" Vim doesn't seem to have a whole lot of functions to aid in text replacement
+" within a buffer. The ":normal!" command usually works just fine, but it
+" could be difficult to maintain sometimes. These functions encapsulate a few
+" common patterns for this.
+
+" function! sideways#util#ReplaceMotion(motion, text) {{{2
+"
+" Replace the normal mode "motion" with "text". This is mostly just a wrapper
+" for a normal! command with a paste, but doesn't pollute any registers.
+"
+"   Examples:
+"     call sideways#util#ReplaceMotion('Va{', 'some text')
+"     call sideways#util#ReplaceMotion('V', 'replacement line')
+"
+" Note that the motion needs to include a visual mode key, like "V", "v" or
+" "gv"
+function! sideways#util#ReplaceMotion(motion, text)
+  let original_reg      = getreg('z')
+  let original_reg_type = getregtype('z')
+
+  let @z = a:text
+  exec 'normal! '.a:motion.'"zp'
+
+  call setreg('z', original_reg, original_reg_type)
+endfunction
+
+" function! sideways#util#ReplaceCols(start, end, text) {{{2
+"
+" Replace the area defined by the 'start' and 'end' columns on the current
+" line with 'text'
+"
+" TODO Multibyte characters break it
+function! sideways#util#ReplaceCols(start, end, text)
+  let start    = a:start - 1
+  let interval = a:end - a:start
+
+  if start > 0 && interval > 0
+    let motion = '0'.start.'lv'.interval.'l'
+  elseif start > 0
+    let motion = '0'.start.'lv'
+  elseif interval > 0
+    let motion = '0v'.interval.'l'
+  else
+    return 0
+  endif
+
+  call sideways#util#ReplaceMotion(motion, a:text)
+  return 1
+endfunction
+
+" Text retrieval {{{1
+"
+" These functions are similar to the text replacement functions, only retrieve
+" the text instead.
+
+" function! sideways#util#GetMotion(motion) {{{2
+"
+" Execute the normal mode motion "motion" and return the text it marks.
+"
+" Note that the motion needs to include a visual mode key, like "V", "v" or
+" "gv"
+function! sideways#util#GetMotion(motion)
+  call sideways#util#PushCursor()
+
+  let original_reg      = getreg('z')
+  let original_reg_type = getregtype('z')
+
+  exec 'normal! '.a:motion.'"zy'
+  let text = @z
+
+  call setreg('z', original_reg, original_reg_type)
+  call sideways#util#PopCursor()
+
+  return text
+endfunction
+
+" function! sideways#util#GetCols(start, end) {{{2
+"
+" Retrieve the text from columns "start" to "end" on the current line.
+function! sideways#util#GetCols(start, end)
+  return strpart(getline('.'), a:start - 1, a:end - a:start + 1)
+endfunction

File doc/sideways.txt

+==============================================================================
+CONTENTS                                           *sideways* *sideways-contents*
+
+    Installation............................: |sideways-installation|
+    Usage...................................: |sideways-usage|
+    Issues..................................: |sideways-issues|
+
+
+==============================================================================
+INSTALLATION                                            *sideways-installation*
+
+There are several ways to install the plugin. The recommended one is by using
+Tim Pope's pathogen (http://www.vim.org/scripts/script.php?script_id=2332). In
+that case, you can clone the plugin's git repository like so:
+>
+    git clone git://github.com/AndrewRadev/sideways.vim.git ~/.vim/bundle/sideways
+<
+If your vim configuration is under git version control, you could also set up
+the repository as a submodule, which would allow you to update more easily.
+The command is (provided you're in ~/.vim):
+>
+    git submodule add git://github.com/AndrewRadev/sideways.vim.git bundle/sideways
+<
+
+Another way is to simply copy all the essential directories inside the ~/.vim
+directory: plugin, autoload, doc.
+
+==============================================================================
+USAGE                                *sideways-usage* *SidewaysLeft* *SidewaysRight*
+
+The plugin defines two commands, |:SidewaysLeft| and |:SidewaysRight|, which
+move the item under the cursor left or right, where an "item" is defined by a
+delimiter. As an example:
+>
+    def function(one, two, three):
+        pass
+<
+Placing the cursor on "two" and executing |:SidewaysLeft|, the "one" and "two"
+arguments will switch their places, resulting in this:
+>
+    def function(two, one, three):
+        pass
+<
+In this case, the delimiter is a comma. The plugin currently works with
+various other cases and it's intended to make the process configurable. While
+this particular example is in python, this should work for arguments in many
+different languages that use round braces to denote function calls.
+
+Apart from functions, it works for square-bracket lists in dynamic languages:
+>
+    list = [one, [two, four, five], three]
+<
+Notice that, if you experiment with this example, you'll find that you can
+move the entire second list around. The plugin takes into consideration nested
+structures.
+
+Apart from functions, it works for lists in CSS declarations:
+>
+    border-radius: 20px 0 0 20px;
+<
+Also for single-line CSS declarations:
+>
+    a { color: #fff; background: blue; text-decoration: underline; }
+<
+And, it also works for cucumber tables:
+>
+    Examples:
+      | input_1 | input_2 | button | output |
+      | 20      | 30      | add    | 50     |
+      | 2       | 5       | add    | 7      |
+      | 0       | 40      | add    | 40     |
+<
+
+It's highly suggested to map the two commands to convenient keys. For example,
+mapping them to <c-h> and <c-l> would look like this:
+>
+    nnoremap <c-h> :SidewaysLeft<cr>
+    nnoremap <c-l> :SidewaysRight<cr>
+<
+
+The plugin is intended to be highly customizable. In the future, it should be
+able to work with ruby function arguments and it may also contain an "argument"
+text object (since the machinery to detect arguments is already there).
+
+==============================================================================
+ISSUES                                                        *sideways-issues*
+
+Any issues and suggestions are very welcome on the github bugtracker:
+https://github.com/AndrewRadev/sideways.vim/issues
+
+
+vim:tw=78:sw=4:ft=help:norl:
 GundoLicense	gundo.txt	/*GundoLicense*
 GundoUsage	gundo.txt	/*GundoUsage*
 MatchError	matchit.txt	/*MatchError*
+SidewaysLeft	sideways.txt	/*SidewaysLeft*
+SidewaysRight	sideways.txt	/*SidewaysRight*
 Tabular.txt	Tabular.txt	/*Tabular.txt*
 VCSCommandCVSDiffOpt	vcscommand.txt	/*VCSCommandCVSDiffOpt*
 VCSCommandCVSExec	vcscommand.txt	/*VCSCommandCVSExec*
 session-references	session.txt	/*session-references*
 session.txt	session.txt	/*session.txt*
 sessionoptions	session.txt	/*sessionoptions*
+sideways	sideways.txt	/*sideways*
+sideways-contents	sideways.txt	/*sideways-contents*
+sideways-installation	sideways.txt	/*sideways-installation*
+sideways-issues	sideways.txt	/*sideways-issues*
+sideways-usage	sideways.txt	/*sideways-usage*
 surround	surround.txt	/*surround*
 surround-author	surround.txt	/*surround-author*
 surround-customizing	surround.txt	/*surround-customizing*

File plugin/sideways.vim

+if exists("g:loaded_sideways") || &cp
+  finish
+endif
+
+let g:loaded_sideways = '0.0.1' " version number
+let s:keepcpo = &cpo
+set cpo&vim
+
+let g:sideways_definitions =
+      \ [
+      \   {
+      \     'start':     '(\s*',
+      \     'end':       '\s*)',
+      \     'delimiter': '^,\s*',
+      \     'skip':      '^\s',
+      \     'brackets':  ['([''"', ')]''"']
+      \   },
+      \   {
+      \     'start':     '\[\s*',
+      \     'end':       '\s*\]',
+      \     'delimiter': '^,\s*',
+      \     'skip':      '^\s',
+      \     'brackets':  ['([''"', ')]''"']
+      \   },
+      \   {
+      \     'start':     '\k:\s*',
+      \     'end':       ';',
+      \     'delimiter': '^\s',
+      \     'skip':      '^\s',
+      \     'brackets':  ['(''"', ')''"']
+      \   },
+      \   {
+      \     'start':     '{\s*',
+      \     'end':       ';\=\s*}',
+      \     'delimiter': '^;\s*',
+      \     'skip':      '^\s',
+      \     'brackets':  ['(''"', ')''"']
+      \   },
+      \   {
+      \     'start':     '^\s*|',
+      \     'end':       '|$',
+      \     'delimiter': '^|',
+      \     'skip':      '^$',
+      \     'brackets':  ['(''"', ')''"']
+      \   },
+      \ ]
+
+command! SidewaysLeft  call sideways#Left(g:sideways_definitions)
+command! SidewaysRight call sideways#Right(g:sideways_definitions)
+
+let &cpo = s:keepcpo
+unlet s:keepcpo
 nmap <leader>a: :Tabularize /:\zs<CR>
 vmap <leader>a: :Tabularize /:\zs<CR>
 
+" sideways.vim mappings.
+nnoremap <C-h> :SidewaysLeft<CR>
+nnoremap <C-l> :SidewaysRight<CR>
+
 " Gundo settings.
 if v:version >= 703
   let g:gundo_width = 50