├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile.rb ├── doc └── blockle.txt ├── plugin └── blockle.vim └── spec ├── blockle_spec.rb ├── matchit.vim └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | *.swp 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.3.1 5 | before_install: 6 | - sudo add-apt-repository ppa:laurent-boulard/vim -y 7 | - sudo apt-get update -q 8 | - sudo apt-get install vim-gtk -y 9 | before_script: 10 | - "export DISPLAY=:99.0" 11 | - "sh -e /etc/init.d/xvfb start" 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # A sample Gemfile 3 | source 'https://rubygems.org' 4 | 5 | group :test do 6 | gem 'rake', '~> 11.2.0' 7 | gem 'guard', '~> 2.14.0' 8 | gem 'guard-rspec', '~> 4.7.0' 9 | gem 'vimrunner', '~> 0.3.0' 10 | gem 'rspec-core', '~> 3.5.0' 11 | gem 'rspec-expectations', '~> 3.5.0' 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coderay (1.1.1) 5 | diff-lcs (1.2.5) 6 | ffi (1.9.14) 7 | formatador (0.2.5) 8 | guard (2.14.0) 9 | formatador (>= 0.2.4) 10 | listen (>= 2.7, < 4.0) 11 | lumberjack (~> 1.0) 12 | nenv (~> 0.1) 13 | notiffany (~> 0.0) 14 | pry (>= 0.9.12) 15 | shellany (~> 0.0) 16 | thor (>= 0.18.1) 17 | guard-compat (1.2.1) 18 | guard-rspec (4.7.3) 19 | guard (~> 2.1) 20 | guard-compat (~> 1.1) 21 | rspec (>= 2.99.0, < 4.0) 22 | listen (3.1.5) 23 | rb-fsevent (~> 0.9, >= 0.9.4) 24 | rb-inotify (~> 0.9, >= 0.9.7) 25 | ruby_dep (~> 1.2) 26 | lumberjack (1.0.10) 27 | method_source (0.8.2) 28 | nenv (0.3.0) 29 | notiffany (0.1.1) 30 | nenv (~> 0.1) 31 | shellany (~> 0.0) 32 | pry (0.10.4) 33 | coderay (~> 1.1.0) 34 | method_source (~> 0.8.1) 35 | slop (~> 3.4) 36 | rake (11.2.2) 37 | rb-fsevent (0.9.7) 38 | rb-inotify (0.9.7) 39 | ffi (>= 0.5.0) 40 | rspec (3.5.0) 41 | rspec-core (~> 3.5.0) 42 | rspec-expectations (~> 3.5.0) 43 | rspec-mocks (~> 3.5.0) 44 | rspec-core (3.5.2) 45 | rspec-support (~> 3.5.0) 46 | rspec-expectations (3.5.0) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.5.0) 49 | rspec-mocks (3.5.0) 50 | diff-lcs (>= 1.2.0, < 2.0) 51 | rspec-support (~> 3.5.0) 52 | rspec-support (3.5.0) 53 | ruby_dep (1.4.0) 54 | shellany (0.0.1) 55 | slop (3.6.0) 56 | thor (0.19.1) 57 | vimrunner (0.3.2) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | guard (~> 2.14.0) 64 | guard-rspec (~> 4.7.0) 65 | rake (~> 11.2.0) 66 | rspec-core (~> 3.5.0) 67 | rspec-expectations (~> 3.5.0) 68 | vimrunner (~> 0.3.0) 69 | 70 | BUNDLED WITH 71 | 1.12.5 72 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Note: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separately) 25 | # * 'just' rspec: 'rspec' 26 | 27 | guard :rspec, cmd: "bundle exec rspec" do 28 | require "guard/rspec/dsl" 29 | dsl = Guard::RSpec::Dsl.new(self) 30 | 31 | # Feel free to open issues for suggestions and improvements 32 | 33 | # RSpec files 34 | rspec = dsl.rspec 35 | watch(rspec.spec_helper) { rspec.spec_dir } 36 | watch(rspec.spec_support) { rspec.spec_dir } 37 | watch(rspec.spec_files) 38 | 39 | # VimL files 40 | watch('plugin/blockle.vim') { rspec.spec_dir } 41 | end 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-blockle [![Build Status](https://travis-ci.org/Sathors/vim-blockle.svg?branch=master)](https://travis-ci.org/Sathors/vim-blockle) 2 | 3 | This plugin allows rapid toggling between the two different styles of ruby 4 | blocks, namely `do`/`end` and brackets `{}`. To use, simply move the cursor to the 5 | beginning or end of a block, and type `b`. As a mnemonic, remember 'b' 6 | for 'block'. 7 | 8 | Note: This plugin works best if you have your cursor on a `do`, `end`, `{`, or `}`. An 9 | attempt is made for it to work if you are inside a block as well, in which 10 | case the most immediate parent will be toggled. 11 | 12 | When moving from a `do`/`end` to a bracket-style block, the plugin will attempt to 13 | move to a one-liner if appropriate. 14 | 15 | For example, if you have the following ruby code (* indicates cursor position): 16 | 17 | ['one', 'two'].each d*o |number| 18 | puts number + "!" 19 | end 20 | 21 | After invoking `b`, the resulting code would be: 22 | 23 | ['one', 'two'].each *{ |number| puts number + "!" } 24 | 25 | ## Installation 26 | 27 | If you don't have a preferred installation method, I recommend 28 | installing [pathogen.vim](https://github.com/tpope/vim-pathogen), and 29 | then simply copy and paste: 30 | 31 | cd ~/.vim/bundle 32 | git clone git://github.com/jgdavey/vim-blockle.git 33 | 34 | Once help tags have been generated, you can view the manual with 35 | `:help blockle`. 36 | 37 | ## Requirements 38 | 39 | This plugin is only available if 'compatible' is not set, and will only work 40 | correctly if matchit.vim is available. 41 | Needs vim 7.4 to work. 42 | 43 | ## License 44 | 45 | Copyright (c) Joshua Davey. Distributed under the same terms as Vim itself. 46 | See `:help license`. 47 | -------------------------------------------------------------------------------- /Rakefile.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new 4 | 5 | task default: [:spec] 6 | -------------------------------------------------------------------------------- /doc/blockle.txt: -------------------------------------------------------------------------------- 1 | *blockle.txt* Plugin for toggling ruby block styles 2 | 3 | Author: Joshua Davey *blockle-author* 4 | License: Same terms as Vim itself (see |license|) 5 | 6 | This plugin is only available if 'compatible' is not set, and will only work 7 | correctly if matchit.vim is available. 8 | 9 | INTRODUCTION *blockle* 10 | 11 | This plugin allows rapid toggling between the two different styles of ruby 12 | blocks, namely do/end and brackets {}. To use, simply move the cursor to the 13 | beginning or end of a block, and type b. As a mnemonic, remember 'b' 14 | for 'block'. 15 | 16 | Note: This plugin works best if you have your cursor on a do, end, {, or }. An 17 | attempt is made for it to work if you are inside a block as well, in which 18 | case the most immediate parent will be toggled. 19 | 20 | When moving from a do/end to a bracket-style block, the plugin will attempt to 21 | move to a one-liner if appropriate: 22 | 23 | For example, if you have the following ruby code (* indicates cursor position): 24 | > 25 | ['one', 'two'].each d*o |number| 26 | puts number + "!" 27 | end 28 | > 29 | After invoking b, the resulting code would be: 30 | > 31 | ['one', 'two'].each *{ |number| puts number + "!" } 32 | > 33 | 34 | MAPPINGS *blockle-mappings* 35 | 36 | b or BlockToggle 37 | Toggle ruby block style 38 | 39 | To override default mapping, add something like the following to your vim 40 | configuration: 41 | > 42 | let g:blockle_mapping = 'bl' 43 | < 44 | You can add additional mappings by calling the plugins function directly. To 45 | do so, add something like the following to your vim configuration: 46 | > 47 | map BlockToggle 48 | < 49 | which would additionally map Super-J (Cmd-J on Mac OS) to the toggling function. 50 | 51 | 52 | vim:tw=78:et:ft=help:norl: 53 | -------------------------------------------------------------------------------- /plugin/blockle.vim: -------------------------------------------------------------------------------- 1 | " blockle.vim - Ruby Block Toggling 2 | " Author: Joshua Davey 3 | " Version: 0.4 4 | " 5 | " Licensed under the same terms as Vim itself. 6 | " ============================================================================ 7 | 8 | " Exit quickly when: 9 | " - this plugin was already loaded (or disabled) 10 | " - when 'compatible' is set 11 | if (exists('g:loaded_blockle') && g:loaded_blockle) || &cp 12 | finish 13 | endif 14 | let g:loaded_blockle = 1 15 | 16 | let s:cpo_save = &cpo 17 | set cpo&vim 18 | 19 | function! s:CharUnderCursor() 20 | return getline('.')[col('.') - 1] 21 | endfunction 22 | 23 | function! s:CharBeforeCursor() 24 | return getline('.')[col('.') - 2] 25 | endfunction 26 | 27 | function! s:WordUnderCursor() 28 | return expand('') 29 | endfunction 30 | 31 | function! s:WordUnderCursorEquals(word) 32 | return s:WordUnderCursor() ==# a:word 33 | endfunction 34 | 35 | function! s:CharUnderCursorEquals(char) 36 | return s:CharUnderCursor() ==# a:char 37 | endfunction 38 | 39 | function! s:CharBeforeCursorMatches(pattern) 40 | return s:CharBeforeCursor() =~# a:pattern 41 | endfunction 42 | 43 | function! s:ReplaceOpeningBracketWithDo() 44 | " The cursor has to be on the opening bracket. 45 | normal! sdo 46 | endfunction 47 | 48 | function! s:ReplaceClosingBracketWithEnd() 49 | " The cursor has to be on the closing bracket. 50 | normal! send 51 | endfunction 52 | 53 | function! s:SetCursorPosition(position) 54 | call setpos('.', a:position) 55 | endfunction 56 | 57 | function! s:GoToOpeningOrClosingTag() 58 | " normal! cannot be used, because else matchit would not be working. 59 | normal % 60 | endfunction 61 | 62 | function! s:InsertCharBeforeCursor(char) 63 | exe 'normal! i'.a:char."\l" 64 | endfunction 65 | 66 | function! s:InsertSpaceIfBracketsTouchesPreviousWord() 67 | if s:CharBeforeCursorMatches('[^ ;]') 68 | call s:InsertCharBeforeCursor(' ') 69 | endif 70 | endfunction 71 | 72 | function! s:ConvertOneLinerBracketsToDoEnd(start_position, end_position, start_line) 73 | let end_position = a:end_position 74 | if s:CharUnderCursorEquals(' ') 75 | normal! x 76 | else 77 | normal! l 78 | let end_position = getpos('.') 79 | endif 80 | set paste 81 | normal! send 82 | set nopaste 83 | call s:SetCursorPosition(a:start_position) 84 | 85 | " Has block parameters 86 | if search('\vdo *\|', 'c', a:start_line) 87 | let end_of_line = '2f|' 88 | else 89 | let end_of_line = 'e' 90 | endif 91 | call s:SetCursorPosition(end_position) 92 | exe "normal! i\" 93 | call s:SetCursorPosition(a:start_position) 94 | exe 'normal! '.end_of_line."a\" 95 | call s:SetCursorPosition(a:start_position) 96 | if search('do|', 'c', a:start_line) 97 | :.s/do|/do |/ 98 | call s:SetCursorPosition(a:start_position) 99 | endif 100 | endfunction 101 | 102 | function! s:ConvertBracketsToDoEnd() 103 | " Cursor should be on the opening bracket. 104 | 105 | call s:InsertSpaceIfBracketsTouchesPreviousWord() 106 | 107 | let start_position = getpos('.') 108 | let start_line = line('.') 109 | call s:GoToOpeningOrClosingTag() 110 | let end_position = getpos('.') 111 | let end_line = line('.') 112 | 113 | call s:SetCursorPosition(start_position) 114 | call s:ReplaceOpeningBracketWithDo() 115 | call s:SetCursorPosition(end_position) 116 | 117 | if start_line == end_line " Was a one-liner 118 | call s:ConvertOneLinerBracketsToDoEnd(start_position, end_position, start_line) 119 | else 120 | call s:ReplaceClosingBracketWithEnd() 121 | call s:SetCursorPosition(start_position) 122 | endif 123 | endfunction 124 | 125 | function! s:ReplaceEndWithClosingBracket() 126 | " The cursor should be on the "end". 127 | normal! ciw} 128 | endfunction 129 | 130 | function! s:ConvertDoEndToBrackets() 131 | " The cursor is positioned on the "d" of the "do" word. 132 | 133 | let do_position = getpos('.') 134 | let do_line = line('.') 135 | call s:GoToOpeningOrClosingTag() 136 | let end_line = line('.') 137 | let number_lines = end_line - do_line + 1 138 | 139 | call s:ReplaceEndWithClosingBracket() 140 | call s:SetCursorPosition(do_position) 141 | normal! de 142 | 143 | let line = getline(do_line) 144 | let before_do_str = strpart(line, 0, do_position[2] - 1) 145 | let after_do_str = strpart(line, do_position[2] - 1) 146 | 147 | call setline(do_line, before_do_str . '{' . after_do_str) 148 | 149 | if number_lines == 3 150 | normal! JJ 151 | " Remove extraneous spaces 152 | " if search(' \+', 'c', do_line) | :.s/\([^ ]\) \+/\1 /g | endif 153 | call s:SetCursorPosition(do_position) 154 | endif 155 | endfunction 156 | 157 | function s:WordStrictlyUnderCursorEquals(word) 158 | " Return if the word strictly under the cursor is equal to word. 159 | " If the cursor is positioned in a blank preceding the cursor word, then 160 | " this returns false. 161 | return s:WordUnderCursorEquals(a:word) && s:CharUnderCursor() !=# ' ' 162 | endfunction 163 | 164 | function! s:OpeningBracketIsCloserThanDo() 165 | let [do_line, do_col] = searchpos('\', 'bcWn') 166 | let [bracket_line, bracket_col] = searchpos('{', 'bcWn') 167 | return bracket_line > do_line || (bracket_line == do_line && bracket_col > do_col) 168 | endfunction 169 | 170 | function! s:GoToNearestBlockOpeningTag() 171 | if s:WordUnderCursorEquals('end') 172 | call s:GoToOpeningOrClosingTag() 173 | if s:WordUnderCursorEquals('do') 174 | return 175 | endif 176 | endif 177 | " Positions the cursor on the nearest *do or *{. 178 | if s:OpeningBracketIsCloserThanDo() 179 | call searchpos('{', 'bcW') 180 | else 181 | call searchpos('\', 'bcW') 182 | endif 183 | endfunction 184 | 185 | function! s:ToggleDoEndOrBrackets() 186 | " Save anonymous register and clipboard settings 187 | let reg = getreg('"', 1) 188 | let regtype = getregtype('"') 189 | let cb_save = &clipboard 190 | set clipboard-=unnamed 191 | let paste_mode = &paste 192 | 193 | call s:GoToNearestBlockOpeningTag() 194 | try 195 | if s:CharUnderCursorEquals('{') 196 | call s:ConvertBracketsToDoEnd() 197 | elseif s:WordUnderCursorEquals('do') 198 | call s:ConvertDoEndToBrackets() 199 | else 200 | throw 'Cannot toggle block: cursor is not on {, }, do or end' 201 | endif 202 | 203 | " Restore anonymous register and clipboard settings 204 | finally 205 | call setreg('"', reg, regtype) 206 | let &clipboard = cb_save 207 | let &paste = paste_mode 208 | 209 | silent! call repeat#set("\BlockToggle", -1) 210 | endtry 211 | endfunction 212 | 213 | nnoremap BlockToggle :call ToggleDoEndOrBrackets() 214 | 215 | if !exists('g:blockle_mapping') 216 | let g:blockle_mapping = 'b' 217 | endif 218 | 219 | augroup blockle 220 | autocmd! 221 | exec 'autocmd FileType ruby map ' . g:blockle_mapping . ' BlockToggle' 222 | augroup END 223 | 224 | let &cpo = s:cpo_save 225 | 226 | " vim:set ft=vim ff=unix ts=4 sw=2 sts=2: 227 | -------------------------------------------------------------------------------- /spec/blockle_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Blockle' do 4 | include Helpers 5 | 6 | specify 'one-liner brackets to do..end' do 7 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 8 | [1, 3].each <{> |elt| puts elt } 9 | END_INITIAL 10 | [1, 3].each o |elt| 11 | puts elt 12 | end 13 | END_FINAL 14 | end 15 | 16 | specify 'do..end to one-liner brackets' do 17 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 18 | [1, 3].each o |elt| 19 | puts elt 20 | end 21 | END_INITIAL 22 | [1, 3].each <{> |elt| puts elt } 23 | END_FINAL 24 | end 25 | 26 | specify 'inner nested do..end' do 27 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 28 | [[1, 3], [6, 7]].each do |pair| 29 | pair.each o |elt| 30 | puts elt 31 | end 32 | end 33 | END_INITIAL 34 | [[1, 3], [6, 7]].each do |pair| 35 | pair.each <{> |elt| puts elt } 36 | end 37 | END_FINAL 38 | end 39 | 40 | specify 'outer nested do..end' do 41 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 42 | [[1, 3], [6, 7]].each o |pair| 43 | pair.each do |elt| 44 | puts elt 45 | end 46 | end 47 | END_INITIAL 48 | [[1, 3], [6, 7]].each <{> |pair| 49 | pair.each do |elt| 50 | puts elt 51 | end 52 | } 53 | END_FINAL 54 | end 55 | 56 | specify 'inner nested brackets' do 57 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 58 | [[1, 3], [6, 7]].each { |pair| pair.each <{> |elt| puts elt } } 59 | END_INITIAL 60 | [[1, 3], [6, 7]].each { |pair| pair.each o |elt| 61 | puts elt 62 | end } 63 | END_FINAL 64 | end 65 | 66 | specify 'outer nested brackets' do 67 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 68 | [[1, 3], [6, 7]].each <{> |pair| pair.each { |elt| puts elt } } 69 | END_INITIAL 70 | [[1, 3], [6, 7]].each o |pair| 71 | pair.each { |elt| puts elt } 72 | end 73 | END_FINAL 74 | end 75 | 76 | specify 'with a dictionnary' do 77 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 78 | 1.times.map do |i| 79 | { 80 | foo: i, 81 | bar: 4 82 | } 83 | nd 84 | END_INITIAL 85 | 1.times.map <{> |i| 86 | { 87 | foo: i, 88 | bar: 4 89 | } 90 | } 91 | END_FINAL 92 | end 93 | 94 | specify 'with another dictionnary' do 95 | test_block_toggle <<-END_INITIAL, <<-END_FINAL 96 | 1.times.map do |i| 97 | puts 'test' 98 | { 99 | foo: i, 100 | bar: 4 101 | } 102 | puts 'test' 103 | nd 104 | END_INITIAL 105 | 1.times.map <{> |i| 106 | puts 'test' 107 | { 108 | foo: i, 109 | bar: 4 110 | } 111 | puts 'test' 112 | } 113 | END_FINAL 114 | end 115 | 116 | specify 'outside of a block' do 117 | ensure_not_working <<-EOF 118 | [1, 2].eah { |elt| puts elt } 119 | EOF 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/matchit.vim: -------------------------------------------------------------------------------- 1 | " matchit.vim: (global plugin) Extended "%" matching 2 | " Last Change: Fri Jan 25 10:00 AM 2008 EST 3 | " Maintainer: Benji Fisher PhD 4 | " Version: 1.13.2, for Vim 6.3+ 5 | " URL: http://www.vim.org/script.php?script_id=39 6 | 7 | " Documentation: 8 | " The documentation is in a separate file, matchit.txt . 9 | 10 | " Credits: 11 | " Vim editor by Bram Moolenaar (Thanks, Bram!) 12 | " Original script and design by Raul Segura Acevedo 13 | " Support for comments by Douglas Potts 14 | " Support for back references and other improvements by Benji Fisher 15 | " Support for many languages by Johannes Zellner 16 | " Suggestions for improvement, bug reports, and support for additional 17 | " languages by Jordi-Albert Batalla, Neil Bird, Servatius Brandt, Mark 18 | " Collett, Stephen Wall, Dany St-Amant, Yuheng Xie, and Johannes Zellner. 19 | 20 | " Debugging: 21 | " If you'd like to try the built-in debugging commands... 22 | " :MatchDebug to activate debugging for the current buffer 23 | " This saves the values of several key script variables as buffer-local 24 | " variables. See the MatchDebug() function, below, for details. 25 | 26 | " TODO: I should think about multi-line patterns for b:match_words. 27 | " This would require an option: how many lines to scan (default 1). 28 | " This would be useful for Python, maybe also for *ML. 29 | " TODO: Maybe I should add a menu so that people will actually use some of 30 | " the features that I have implemented. 31 | " TODO: Eliminate the MultiMatch function. Add yet another argument to 32 | " Match_wrapper() instead. 33 | " TODO: Allow :let b:match_words = '\(\(foo\)\(bar\)\):\3\2:end\1' 34 | " TODO: Make backrefs safer by using '\V' (very no-magic). 35 | " TODO: Add a level of indirection, so that custom % scripts can use my 36 | " work but extend it. 37 | 38 | " allow user to prevent loading 39 | " and prevent duplicate loading 40 | if exists("loaded_matchit") || &cp 41 | finish 42 | endif 43 | let loaded_matchit = 1 44 | let s:last_mps = "" 45 | let s:last_words = ":" 46 | 47 | let s:save_cpo = &cpo 48 | set cpo&vim 49 | 50 | nnoremap % :call Match_wrapper('',1,'n') 51 | nnoremap g% :call Match_wrapper('',0,'n') 52 | vnoremap % :call Match_wrapper('',1,'v') m'gv`` 53 | vnoremap g% :call Match_wrapper('',0,'v') m'gv`` 54 | onoremap % v:call Match_wrapper('',1,'o') 55 | onoremap g% v:call Match_wrapper('',0,'o') 56 | 57 | " Analogues of [{ and ]} using matching patterns: 58 | nnoremap [% :call MultiMatch("bW", "n") 59 | nnoremap ]% :call MultiMatch("W", "n") 60 | vmap [% [%m'gv`` 61 | vmap ]% ]%m'gv`` 62 | " vnoremap [% :call MultiMatch("bW", "v") m'gv`` 63 | " vnoremap ]% :call MultiMatch("W", "v") m'gv`` 64 | onoremap [% v:call MultiMatch("bW", "o") 65 | onoremap ]% v:call MultiMatch("W", "o") 66 | 67 | " text object: 68 | vmap a% [%v]% 69 | 70 | " Auto-complete mappings: (not yet "ready for prime time") 71 | " TODO Read :help write-plugin for the "right" way to let the user 72 | " specify a key binding. 73 | " let g:match_auto = '' 74 | " let g:match_autoCR = '' 75 | " if exists("g:match_auto") 76 | " execute "inoremap " . g:match_auto . ' x"=Autocomplete()Pls' 77 | " endif 78 | " if exists("g:match_autoCR") 79 | " execute "inoremap " . g:match_autoCR . ' =Autocomplete()' 80 | " endif 81 | " if exists("g:match_gthhoh") 82 | " execute "inoremap " . g:match_gthhoh . ' :call Gthhoh()' 83 | " endif " gthhoh = "Get the heck out of here!" 84 | 85 | let s:notslash = '\\\@" 99 | endif 100 | " In s:CleanUp(), we may need to check whether the cursor moved forward. 101 | let startline = line(".") 102 | let startcol = col(".") 103 | " Use default behavior if called with a count. 104 | if v:count 105 | exe "normal! " . v:count . "%" 106 | return s:CleanUp(restore_options, a:mode, startline, startcol) 107 | end 108 | 109 | " First step: if not already done, set the script variables 110 | " s:do_BR flag for whether there are backrefs 111 | " s:pat parsed version of b:match_words 112 | " s:all regexp based on s:pat and the default groups 113 | " 114 | if !exists("b:match_words") || b:match_words == "" 115 | let match_words = "" 116 | " Allow b:match_words = "GetVimMatchWords()" . 117 | elseif b:match_words =~ ":" 118 | let match_words = b:match_words 119 | else 120 | execute "let match_words =" b:match_words 121 | endif 122 | let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") . 123 | \ '\/\*:\*\/,#if\%(def\)\=:#else\>:#elif\>:#endif\>' 124 | let match_words = match_words . (strlen(match_words) ? "," : "") . default 125 | " Thanks to Preben "Peppe" Guldberg and Bram Moolenaar for this suggestion! 126 | if (match_words != s:last_words) || (&mps != s:last_mps) || 127 | \ exists("b:match_debug") 128 | let s:last_words = match_words 129 | let s:last_mps = &mps 130 | " The next several lines were here before 131 | " BF started messing with this script. 132 | " quote the special chars in 'matchpairs', replace [,:] with \| and then 133 | " append the builtin pairs (/*, */, #if, #ifdef, #else, #elif, #endif) 134 | " let default = substitute(escape(&mps, '[$^.*~\\/?]'), '[,:]\+', 135 | " \ '\\|', 'g').'\|\/\*\|\*\/\|#if\>\|#ifdef\>\|#else\>\|#elif\>\|#endif\>' 136 | " s:all = pattern with all the keywords 137 | if match_words !~ s:notslash . '\\\d' 138 | let s:do_BR = 0 139 | let s:pat = match_words 140 | else 141 | let s:do_BR = 1 142 | let s:pat = s:ParseWords(match_words) 143 | endif 144 | let s:all = substitute(s:pat, s:notslash . '\zs[,:]\+', '\\|', 'g') 145 | let s:all = '\%(' . s:all . '\)' 146 | " let s:all = '\%(' . substitute(s:all, '\\\ze[,:]', '', 'g') . '\)' 147 | if exists("b:match_debug") 148 | let b:match_pat = s:pat 149 | endif 150 | endif 151 | 152 | " Second step: set the following local variables: 153 | " matchline = line on which the cursor started 154 | " curcol = number of characters before match 155 | " prefix = regexp for start of line to start of match 156 | " suffix = regexp for end of match to end of line 157 | " Require match to end on or after the cursor and prefer it to 158 | " start on or before the cursor. 159 | let matchline = getline(startline) 160 | if a:word != '' 161 | " word given 162 | if a:word !~ s:all 163 | echohl WarningMsg|echo 'Missing rule for word:"'.a:word.'"'|echohl NONE 164 | return s:CleanUp(restore_options, a:mode, startline, startcol) 165 | endif 166 | let matchline = a:word 167 | let curcol = 0 168 | let prefix = '^\%(' 169 | let suffix = '\)$' 170 | " Now the case when "word" is not given 171 | else " Find the match that ends on or after the cursor and set curcol. 172 | let regexp = s:Wholematch(matchline, s:all, startcol-1) 173 | let curcol = match(matchline, regexp) 174 | " If there is no match, give up. 175 | if curcol == -1 176 | return s:CleanUp(restore_options, a:mode, startline, startcol) 177 | endif 178 | let endcol = matchend(matchline, regexp) 179 | let suf = strlen(matchline) - endcol 180 | let prefix = (curcol ? '^.*\%' . (curcol + 1) . 'c\%(' : '^\%(') 181 | let suffix = (suf ? '\)\%' . (endcol + 1) . 'c.*$' : '\)$') 182 | endif 183 | if exists("b:match_debug") 184 | let b:match_match = matchstr(matchline, regexp) 185 | let b:match_col = curcol+1 186 | endif 187 | 188 | " Third step: Find the group and single word that match, and the original 189 | " (backref) versions of these. Then, resolve the backrefs. 190 | " Set the following local variable: 191 | " group = colon-separated list of patterns, one of which matches 192 | " = ini:mid:fin or ini:fin 193 | " 194 | " Reconstruct the version with unresolved backrefs. 195 | let patBR = substitute(match_words.',', 196 | \ s:notslash.'\zs[,:]*,[,:]*', ',', 'g') 197 | let patBR = substitute(patBR, s:notslash.'\zs:\{2,}', ':', 'g') 198 | " Now, set group and groupBR to the matching group: 'if:endif' or 199 | " 'while:endwhile' or whatever. A bit of a kluge: s:Choose() returns 200 | " group . "," . groupBR, and we pick it apart. 201 | let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR) 202 | let i = matchend(group, s:notslash . ",") 203 | let groupBR = strpart(group, i) 204 | let group = strpart(group, 0, i-1) 205 | " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix 206 | if s:do_BR " Do the hard part: resolve those backrefs! 207 | let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline) 208 | endif 209 | if exists("b:match_debug") 210 | let b:match_wholeBR = groupBR 211 | let i = matchend(groupBR, s:notslash . ":") 212 | let b:match_iniBR = strpart(groupBR, 0, i-1) 213 | endif 214 | 215 | " Fourth step: Set the arguments for searchpair(). 216 | let i = matchend(group, s:notslash . ":") 217 | let j = matchend(group, '.*' . s:notslash . ":") 218 | let ini = strpart(group, 0, i-1) 219 | let mid = substitute(strpart(group, i,j-i-1), s:notslash.'\zs:', '\\|', 'g') 220 | let fin = strpart(group, j) 221 | "Un-escape the remaining , and : characters. 222 | let ini = substitute(ini, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') 223 | let mid = substitute(mid, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') 224 | let fin = substitute(fin, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') 225 | " searchpair() requires that these patterns avoid \(\) groups. 226 | let ini = substitute(ini, s:notslash . '\zs\\(', '\\%(', 'g') 227 | let mid = substitute(mid, s:notslash . '\zs\\(', '\\%(', 'g') 228 | let fin = substitute(fin, s:notslash . '\zs\\(', '\\%(', 'g') 229 | " Set mid. This is optimized for readability, not micro-efficiency! 230 | if a:forward && matchline =~ prefix . fin . suffix 231 | \ || !a:forward && matchline =~ prefix . ini . suffix 232 | let mid = "" 233 | endif 234 | " Set flag. This is optimized for readability, not micro-efficiency! 235 | if a:forward && matchline =~ prefix . fin . suffix 236 | \ || !a:forward && matchline !~ prefix . ini . suffix 237 | let flag = "bW" 238 | else 239 | let flag = "W" 240 | endif 241 | " Set skip. 242 | if exists("b:match_skip") 243 | let skip = b:match_skip 244 | elseif exists("b:match_comment") " backwards compatibility and testing! 245 | let skip = "r:" . b:match_comment 246 | else 247 | let skip = 's:comment\|string' 248 | endif 249 | let skip = s:ParseSkip(skip) 250 | if exists("b:match_debug") 251 | let b:match_ini = ini 252 | let b:match_tail = (strlen(mid) ? mid.'\|' : '') . fin 253 | endif 254 | 255 | " Fifth step: actually start moving the cursor and call searchpair(). 256 | " Later, :execute restore_cursor to get to the original screen. 257 | let restore_cursor = virtcol(".") . "|" 258 | normal! g0 259 | let restore_cursor = line(".") . "G" . virtcol(".") . "|zs" . restore_cursor 260 | normal! H 261 | let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor 262 | execute restore_cursor 263 | call cursor(0, curcol + 1) 264 | " normal! 0 265 | " if curcol 266 | " execute "normal!" . curcol . "l" 267 | " endif 268 | if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on")) 269 | let skip = "0" 270 | else 271 | execute "if " . skip . "| let skip = '0' | endif" 272 | endif 273 | let sp_return = searchpair(ini, mid, fin, flag, skip) 274 | let final_position = "call cursor(" . line(".") . "," . col(".") . ")" 275 | " Restore cursor position and original screen. 276 | execute restore_cursor 277 | normal! m' 278 | if sp_return > 0 279 | execute final_position 280 | endif 281 | return s:CleanUp(restore_options, a:mode, startline, startcol, mid.'\|'.fin) 282 | endfun 283 | 284 | " Restore options and do some special handling for Operator-pending mode. 285 | " The optional argument is the tail of the matching group. 286 | fun! s:CleanUp(options, mode, startline, startcol, ...) 287 | execute "set" a:options 288 | " Open folds, if appropriate. 289 | if a:mode != "o" 290 | if &foldopen =~ "percent" 291 | normal! zv 292 | endif 293 | " In Operator-pending mode, we want to include the whole match 294 | " (for example, d%). 295 | " This is only a problem if we end up moving in the forward direction. 296 | elseif (a:startline < line(".")) || 297 | \ (a:startline == line(".") && a:startcol < col(".")) 298 | if a:0 299 | " Check whether the match is a single character. If not, move to the 300 | " end of the match. 301 | let matchline = getline(".") 302 | let currcol = col(".") 303 | let regexp = s:Wholematch(matchline, a:1, currcol-1) 304 | let endcol = matchend(matchline, regexp) 305 | if endcol > currcol " This is NOT off by one! 306 | execute "normal!" . (endcol - currcol) . "l" 307 | endif 308 | endif " a:0 309 | endif " a:mode != "o" && etc. 310 | return 0 311 | endfun 312 | 313 | " Example (simplified HTML patterns): if 314 | " a:groupBR = '<\(\k\+\)>:' 315 | " a:prefix = '^.\{3}\(' 316 | " a:group = '<\(\k\+\)>:' 317 | " a:suffix = '\).\{2}$' 318 | " a:matchline = "12312" or "12312" 319 | " then extract "tag" from a:matchline and return ":" . 320 | fun! s:InsertRefs(groupBR, prefix, group, suffix, matchline) 321 | if a:matchline !~ a:prefix . 322 | \ substitute(a:group, s:notslash . '\zs:', '\\|', 'g') . a:suffix 323 | return a:group 324 | endif 325 | let i = matchend(a:groupBR, s:notslash . ':') 326 | let ini = strpart(a:groupBR, 0, i-1) 327 | let tailBR = strpart(a:groupBR, i) 328 | let word = s:Choose(a:group, a:matchline, ":", "", a:prefix, a:suffix, 329 | \ a:groupBR) 330 | let i = matchend(word, s:notslash . ":") 331 | let wordBR = strpart(word, i) 332 | let word = strpart(word, 0, i-1) 333 | " Now, a:matchline =~ a:prefix . word . a:suffix 334 | if wordBR != ini 335 | let table = s:Resolve(ini, wordBR, "table") 336 | else 337 | " let table = "----------" 338 | let table = "" 339 | let d = 0 340 | while d < 10 341 | if tailBR =~ s:notslash . '\\' . d 342 | " let table[d] = d 343 | let table = table . d 344 | else 345 | let table = table . "-" 346 | endif 347 | let d = d + 1 348 | endwhile 349 | endif 350 | let d = 9 351 | while d 352 | if table[d] != "-" 353 | let backref = substitute(a:matchline, a:prefix.word.a:suffix, 354 | \ '\'.table[d], "") 355 | " Are there any other characters that should be escaped? 356 | let backref = escape(backref, '*,:') 357 | execute s:Ref(ini, d, "start", "len") 358 | let ini = strpart(ini, 0, start) . backref . strpart(ini, start+len) 359 | let tailBR = substitute(tailBR, s:notslash . '\zs\\' . d, 360 | \ escape(backref, '\\'), 'g') 361 | endif 362 | let d = d-1 363 | endwhile 364 | if exists("b:match_debug") 365 | if s:do_BR 366 | let b:match_table = table 367 | let b:match_word = word 368 | else 369 | let b:match_table = "" 370 | let b:match_word = "" 371 | endif 372 | endif 373 | return ini . ":" . tailBR 374 | endfun 375 | 376 | " Input a comma-separated list of groups with backrefs, such as 377 | " a:groups = '\(foo\):end\1,\(bar\):end\1' 378 | " and return a comma-separated list of groups with backrefs replaced: 379 | " return '\(foo\):end\(foo\),\(bar\):end\(bar\)' 380 | fun! s:ParseWords(groups) 381 | let groups = substitute(a:groups.",", s:notslash.'\zs[,:]*,[,:]*', ',', 'g') 382 | let groups = substitute(groups, s:notslash . '\zs:\{2,}', ':', 'g') 383 | let parsed = "" 384 | while groups =~ '[^,:]' 385 | let i = matchend(groups, s:notslash . ':') 386 | let j = matchend(groups, s:notslash . ',') 387 | let ini = strpart(groups, 0, i-1) 388 | let tail = strpart(groups, i, j-i-1) . ":" 389 | let groups = strpart(groups, j) 390 | let parsed = parsed . ini 391 | let i = matchend(tail, s:notslash . ':') 392 | while i != -1 393 | " In 'if:else:endif', ini='if' and word='else' and then word='endif'. 394 | let word = strpart(tail, 0, i-1) 395 | let tail = strpart(tail, i) 396 | let i = matchend(tail, s:notslash . ':') 397 | let parsed = parsed . ":" . s:Resolve(ini, word, "word") 398 | endwhile " Now, tail has been used up. 399 | let parsed = parsed . "," 400 | endwhile " groups =~ '[^,:]' 401 | let parsed = substitute(parsed, ',$', '', '') 402 | return parsed 403 | endfun 404 | 405 | " TODO I think this can be simplified and/or made more efficient. 406 | " TODO What should I do if a:start is out of range? 407 | " Return a regexp that matches all of a:string, such that 408 | " matchstr(a:string, regexp) represents the match for a:pat that starts 409 | " as close to a:start as possible, before being preferred to after, and 410 | " ends after a:start . 411 | " Usage: 412 | " let regexp = s:Wholematch(getline("."), 'foo\|bar', col(".")-1) 413 | " let i = match(getline("."), regexp) 414 | " let j = matchend(getline("."), regexp) 415 | " let match = matchstr(getline("."), regexp) 416 | fun! s:Wholematch(string, pat, start) 417 | let group = '\%(' . a:pat . '\)' 418 | let prefix = (a:start ? '\(^.*\%<' . (a:start + 2) . 'c\)\zs' : '^') 419 | let len = strlen(a:string) 420 | let suffix = (a:start+1 < len ? '\(\%>'.(a:start+1).'c.*$\)\@=' : '$') 421 | if a:string !~ prefix . group . suffix 422 | let prefix = '' 423 | endif 424 | return prefix . group . suffix 425 | endfun 426 | 427 | " No extra arguments: s:Ref(string, d) will 428 | " find the d'th occurrence of '\(' and return it, along with everything up 429 | " to and including the matching '\)'. 430 | " One argument: s:Ref(string, d, "start") returns the index of the start 431 | " of the d'th '\(' and any other argument returns the length of the group. 432 | " Two arguments: s:Ref(string, d, "foo", "bar") returns a string to be 433 | " executed, having the effect of 434 | " :let foo = s:Ref(string, d, "start") 435 | " :let bar = s:Ref(string, d, "len") 436 | fun! s:Ref(string, d, ...) 437 | let len = strlen(a:string) 438 | if a:d == 0 439 | let start = 0 440 | else 441 | let cnt = a:d 442 | let match = a:string 443 | while cnt 444 | let cnt = cnt - 1 445 | let index = matchend(match, s:notslash . '\\(') 446 | if index == -1 447 | return "" 448 | endif 449 | let match = strpart(match, index) 450 | endwhile 451 | let start = len - strlen(match) 452 | if a:0 == 1 && a:1 == "start" 453 | return start - 2 454 | endif 455 | let cnt = 1 456 | while cnt 457 | let index = matchend(match, s:notslash . '\\(\|\\)') - 1 458 | if index == -2 459 | return "" 460 | endif 461 | " Increment if an open, decrement if a ')': 462 | let cnt = cnt + (match[index]=="(" ? 1 : -1) " ')' 463 | " let cnt = stridx('0(', match[index]) + cnt 464 | let match = strpart(match, index+1) 465 | endwhile 466 | let start = start - 2 467 | let len = len - start - strlen(match) 468 | endif 469 | if a:0 == 1 470 | return len 471 | elseif a:0 == 2 472 | return "let " . a:1 . "=" . start . "| let " . a:2 . "=" . len 473 | else 474 | return strpart(a:string, start, len) 475 | endif 476 | endfun 477 | 478 | " Count the number of disjoint copies of pattern in string. 479 | " If the pattern is a literal string and contains no '0' or '1' characters 480 | " then s:Count(string, pattern, '0', '1') should be faster than 481 | " s:Count(string, pattern). 482 | fun! s:Count(string, pattern, ...) 483 | let pat = escape(a:pattern, '\\') 484 | if a:0 > 1 485 | let foo = substitute(a:string, '[^'.a:pattern.']', "a:1", "g") 486 | let foo = substitute(a:string, pat, a:2, "g") 487 | let foo = substitute(foo, '[^' . a:2 . ']', "", "g") 488 | return strlen(foo) 489 | endif 490 | let result = 0 491 | let foo = a:string 492 | let index = matchend(foo, pat) 493 | while index != -1 494 | let result = result + 1 495 | let foo = strpart(foo, index) 496 | let index = matchend(foo, pat) 497 | endwhile 498 | return result 499 | endfun 500 | 501 | " s:Resolve('\(a\)\(b\)', '\(c\)\2\1\1\2') should return table.word, where 502 | " word = '\(c\)\(b\)\(a\)\3\2' and table = '-32-------'. That is, the first 503 | " '\1' in target is replaced by '\(a\)' in word, table[1] = 3, and this 504 | " indicates that all other instances of '\1' in target are to be replaced 505 | " by '\3'. The hard part is dealing with nesting... 506 | " Note that ":" is an illegal character for source and target, 507 | " unless it is preceded by "\". 508 | fun! s:Resolve(source, target, output) 509 | let word = a:target 510 | let i = matchend(word, s:notslash . '\\\d') - 1 511 | let table = "----------" 512 | while i != -2 " There are back references to be replaced. 513 | let d = word[i] 514 | let backref = s:Ref(a:source, d) 515 | " The idea is to replace '\d' with backref. Before we do this, 516 | " replace any \(\) groups in backref with :1, :2, ... if they 517 | " correspond to the first, second, ... group already inserted 518 | " into backref. Later, replace :1 with \1 and so on. The group 519 | " number w+b within backref corresponds to the group number 520 | " s within a:source. 521 | " w = number of '\(' in word before the current one 522 | let w = s:Count( 523 | \ substitute(strpart(word, 0, i-1), '\\\\', '', 'g'), '\(', '1') 524 | let b = 1 " number of the current '\(' in backref 525 | let s = d " number of the current '\(' in a:source 526 | while b <= s:Count(substitute(backref, '\\\\', '', 'g'), '\(', '1') 527 | \ && s < 10 528 | if table[s] == "-" 529 | if w + b < 10 530 | " let table[s] = w + b 531 | let table = strpart(table, 0, s) . (w+b) . strpart(table, s+1) 532 | endif 533 | let b = b + 1 534 | let s = s + 1 535 | else 536 | execute s:Ref(backref, b, "start", "len") 537 | let ref = strpart(backref, start, len) 538 | let backref = strpart(backref, 0, start) . ":". table[s] 539 | \ . strpart(backref, start+len) 540 | let s = s + s:Count(substitute(ref, '\\\\', '', 'g'), '\(', '1') 541 | endif 542 | endwhile 543 | let word = strpart(word, 0, i-1) . backref . strpart(word, i+1) 544 | let i = matchend(word, s:notslash . '\\\d') - 1 545 | endwhile 546 | let word = substitute(word, s:notslash . '\zs:', '\\', 'g') 547 | if a:output == "table" 548 | return table 549 | elseif a:output == "word" 550 | return word 551 | else 552 | return table . word 553 | endif 554 | endfun 555 | 556 | " Assume a:comma = ",". Then the format for a:patterns and a:1 is 557 | " a:patterns = ",,..." 558 | " a:1 = ",,..." 559 | " If is the first pattern that matches a:string then return 560 | " if no optional arguments are given; return , if a:1 is given. 561 | fun! s:Choose(patterns, string, comma, branch, prefix, suffix, ...) 562 | let tail = (a:patterns =~ a:comma."$" ? a:patterns : a:patterns . a:comma) 563 | let i = matchend(tail, s:notslash . a:comma) 564 | if a:0 565 | let alttail = (a:1 =~ a:comma."$" ? a:1 : a:1 . a:comma) 566 | let j = matchend(alttail, s:notslash . a:comma) 567 | endif 568 | let current = strpart(tail, 0, i-1) 569 | if a:branch == "" 570 | let currpat = current 571 | else 572 | let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g') 573 | endif 574 | while a:string !~ a:prefix . currpat . a:suffix 575 | let tail = strpart(tail, i) 576 | let i = matchend(tail, s:notslash . a:comma) 577 | if i == -1 578 | return -1 579 | endif 580 | let current = strpart(tail, 0, i-1) 581 | if a:branch == "" 582 | let currpat = current 583 | else 584 | let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g') 585 | endif 586 | if a:0 587 | let alttail = strpart(alttail, j) 588 | let j = matchend(alttail, s:notslash . a:comma) 589 | endif 590 | endwhile 591 | if a:0 592 | let current = current . a:comma . strpart(alttail, 0, j-1) 593 | endif 594 | return current 595 | endfun 596 | 597 | " Call this function to turn on debugging information. Every time the main 598 | " script is run, buffer variables will be saved. These can be used directly 599 | " or viewed using the menu items below. 600 | if !exists(":MatchDebug") 601 | command! -nargs=0 MatchDebug call s:Match_debug() 602 | endif 603 | 604 | fun! s:Match_debug() 605 | let b:match_debug = 1 " Save debugging information. 606 | " pat = all of b:match_words with backrefs parsed 607 | amenu &Matchit.&pat :echo b:match_pat 608 | " match = bit of text that is recognized as a match 609 | amenu &Matchit.&match :echo b:match_match 610 | " curcol = cursor column of the start of the matching text 611 | amenu &Matchit.&curcol :echo b:match_col 612 | " wholeBR = matching group, original version 613 | amenu &Matchit.wh&oleBR :echo b:match_wholeBR 614 | " iniBR = 'if' piece, original version 615 | amenu &Matchit.ini&BR :echo b:match_iniBR 616 | " ini = 'if' piece, with all backrefs resolved from match 617 | amenu &Matchit.&ini :echo b:match_ini 618 | " tail = 'else\|endif' piece, with all backrefs resolved from match 619 | amenu &Matchit.&tail :echo b:match_tail 620 | " fin = 'endif' piece, with all backrefs resolved from match 621 | amenu &Matchit.&word :echo b:match_word 622 | " '\'.d in ini refers to the same thing as '\'.table[d] in word. 623 | amenu &Matchit.t&able :echo '0:' . b:match_table . ':9' 624 | endfun 625 | 626 | " Jump to the nearest unmatched "(" or "if" or "" if a:spflag == "bW" 627 | " or the nearest unmatched "" or "endif" or ")" if a:spflag == "W". 628 | " Return a "mark" for the original position, so that 629 | " let m = MultiMatch("bW", "n") ... execute m 630 | " will return to the original position. If there is a problem, do not 631 | " move the cursor and return "", unless a count is given, in which case 632 | " go up or down as many levels as possible and again return "". 633 | " TODO This relies on the same patterns as % matching. It might be a good 634 | " idea to give it its own matching patterns. 635 | fun! s:MultiMatch(spflag, mode) 636 | if !exists("b:match_words") || b:match_words == "" 637 | return "" 638 | end 639 | let restore_options = (&ic ? "" : "no") . "ignorecase" 640 | if exists("b:match_ignorecase") 641 | let &ignorecase = b:match_ignorecase 642 | endif 643 | let startline = line(".") 644 | let startcol = col(".") 645 | 646 | " First step: if not already done, set the script variables 647 | " s:do_BR flag for whether there are backrefs 648 | " s:pat parsed version of b:match_words 649 | " s:all regexp based on s:pat and the default groups 650 | " This part is copied and slightly modified from s:Match_wrapper(). 651 | let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") . 652 | \ '\/\*:\*\/,#if\%(def\)\=:#else\>:#elif\>:#endif\>' 653 | " Allow b:match_words = "GetVimMatchWords()" . 654 | if b:match_words =~ ":" 655 | let match_words = b:match_words 656 | else 657 | execute "let match_words =" b:match_words 658 | endif 659 | let match_words = match_words . (strlen(match_words) ? "," : "") . default 660 | if (match_words != s:last_words) || (&mps != s:last_mps) || 661 | \ exists("b:match_debug") 662 | let s:last_words = match_words 663 | let s:last_mps = &mps 664 | if match_words !~ s:notslash . '\\\d' 665 | let s:do_BR = 0 666 | let s:pat = match_words 667 | else 668 | let s:do_BR = 1 669 | let s:pat = s:ParseWords(match_words) 670 | endif 671 | let s:all = '\%(' . substitute(s:pat . (strlen(s:pat)?",":"") . default, 672 | \ '[,:]\+','\\|','g') . '\)' 673 | if exists("b:match_debug") 674 | let b:match_pat = s:pat 675 | endif 676 | endif 677 | 678 | " Second step: figure out the patterns for searchpair() 679 | " and save the screen, cursor position, and 'ignorecase'. 680 | " - TODO: A lot of this is copied from s:Match_wrapper(). 681 | " - maybe even more functionality should be split off 682 | " - into separate functions! 683 | let cdefault = (s:pat =~ '[^,]$' ? "," : "") . default 684 | let open = substitute(s:pat . cdefault, 685 | \ s:notslash . '\zs:.\{-}' . s:notslash . ',', '\\),\\(', 'g') 686 | let open = '\(' . substitute(open, s:notslash . '\zs:.*$', '\\)', '') 687 | let close = substitute(s:pat . cdefault, 688 | \ s:notslash . '\zs,.\{-}' . s:notslash . ':', '\\),\\(', 'g') 689 | let close = substitute(close, '^.\{-}' . s:notslash . ':', '\\(', '') . '\)' 690 | if exists("b:match_skip") 691 | let skip = b:match_skip 692 | elseif exists("b:match_comment") " backwards compatibility and testing! 693 | let skip = "r:" . b:match_comment 694 | else 695 | let skip = 's:comment\|string' 696 | endif 697 | let skip = s:ParseSkip(skip) 698 | " let restore_cursor = line(".") . "G" . virtcol(".") . "|" 699 | " normal! H 700 | " let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor 701 | let restore_cursor = virtcol(".") . "|" 702 | normal! g0 703 | let restore_cursor = line(".") . "G" . virtcol(".") . "|zs" . restore_cursor 704 | normal! H 705 | let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor 706 | execute restore_cursor 707 | 708 | " Third step: call searchpair(). 709 | " Replace '\('--but not '\\('--with '\%(' and ',' with '\|'. 710 | let openpat = substitute(open, '\(\\\@" or ... 735 | " and return "endif" or "endwhile" or "" or ... . 736 | " For now, this uses b:match_words and the same script variables 737 | " as s:Match_wrapper() . Later, it may get its own patterns, 738 | " either from a buffer variable or passed as arguments. 739 | " fun! s:Autocomplete() 740 | " echo "autocomplete not yet implemented :-(" 741 | " if !exists("b:match_words") || b:match_words == "" 742 | " return "" 743 | " end 744 | " let startpos = s:MultiMatch("bW") 745 | " 746 | " if startpos == "" 747 | " return "" 748 | " endif 749 | " " - TODO: figure out whether 'if' or '' matched, and construct 750 | " " - the appropriate closing. 751 | " let matchline = getline(".") 752 | " let curcol = col(".") - 1 753 | " " - TODO: Change the s:all argument if there is a new set of match pats. 754 | " let regexp = s:Wholematch(matchline, s:all, curcol) 755 | " let suf = strlen(matchline) - matchend(matchline, regexp) 756 | " let prefix = (curcol ? '^.\{' . curcol . '}\%(' : '^\%(') 757 | " let suffix = (suf ? '\).\{' . suf . '}$' : '\)$') 758 | " " Reconstruct the version with unresolved backrefs. 759 | " let patBR = substitute(b:match_words.',', '[,:]*,[,:]*', ',', 'g') 760 | " let patBR = substitute(patBR, ':\{2,}', ':', "g") 761 | " " Now, set group and groupBR to the matching group: 'if:endif' or 762 | " " 'while:endwhile' or whatever. 763 | " let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR) 764 | " let i = matchend(group, s:notslash . ",") 765 | " let groupBR = strpart(group, i) 766 | " let group = strpart(group, 0, i-1) 767 | " " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix 768 | " if s:do_BR 769 | " let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline) 770 | " endif 771 | " " let g:group = group 772 | " 773 | " " - TODO: Construct the closing from group. 774 | " let fake = "end" . expand("") 775 | " execute startpos 776 | " return fake 777 | " endfun 778 | 779 | " Close all open structures. "Get the heck out of here!" 780 | " fun! s:Gthhoh() 781 | " let close = s:Autocomplete() 782 | " while strlen(close) 783 | " put=close 784 | " let close = s:Autocomplete() 785 | " endwhile 786 | " endfun 787 | 788 | " Parse special strings as typical skip arguments for searchpair(): 789 | " s:foo becomes (current syntax item) =~ foo 790 | " S:foo becomes (current syntax item) !~ foo 791 | " r:foo becomes (line before cursor) =~ foo 792 | " R:foo becomes (line before cursor) !~ foo 793 | fun! s:ParseSkip(str) 794 | let skip = a:str 795 | if skip[1] == ":" 796 | if skip[0] == "s" 797 | let skip = "synIDattr(synID(line('.'),col('.'),1),'name') =~? '" . 798 | \ strpart(skip,2) . "'" 799 | elseif skip[0] == "S" 800 | let skip = "synIDattr(synID(line('.'),col('.'),1),'name') !~? '" . 801 | \ strpart(skip,2) . "'" 802 | elseif skip[0] == "r" 803 | let skip = "strpart(getline('.'),0,col('.'))=~'" . strpart(skip,2). "'" 804 | elseif skip[0] == "R" 805 | let skip = "strpart(getline('.'),0,col('.'))!~'" . strpart(skip,2). "'" 806 | endif 807 | endif 808 | return skip 809 | endfun 810 | 811 | let &cpo = s:save_cpo 812 | 813 | " vim:sts=2:sw=2: 814 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'vimrunner' 2 | require 'vimrunner/rspec' 3 | 4 | Vimrunner::RSpec.configure do |config| 5 | # Use a single Vim instance for the test suite. Set to false to use an 6 | # instance per test (slower, but can be easier to manage). 7 | config.reuse_server = true 8 | 9 | # Decide how to start a Vim instance. In this block, an instance should be 10 | # spawned and set up with anything project-specific. 11 | config.start_vim do 12 | FILE_NAME = 'test.rb' 13 | 14 | vim = Vimrunner.start 15 | 16 | # Or, start a GUI instance: 17 | # vim = Vimrunner.start_gvim 18 | 19 | # Setup your plugin in the Vim instance 20 | plugin_path = File.expand_path('../..', __FILE__) 21 | vim.add_plugin(plugin_path, 'plugin/blockle.vim') 22 | vim.add_plugin(plugin_path, 'spec/matchit.vim') 23 | vim.set 'expandtab' 24 | vim.set 'tabstop', 2 25 | vim.set 'shiftwidth', 2 26 | 27 | # The returned value is the Client available in the tests. 28 | vim 29 | end 30 | end 31 | 32 | module Helpers 33 | def test_block_toggle initial_buffer, expected_final_buffer 34 | ensure_cursor_presence expected_final_buffer 35 | 36 | actual_final_buffer, command_output = toggle_block initial_buffer 37 | 38 | expect(command_output).to be_empty 39 | expect(actual_final_buffer).to eq \ 40 | normalize_string_indent(expected_final_buffer) 41 | end 42 | 43 | def ensure_not_working initial_buffer 44 | actual_final_buffer, command_output = toggle_block initial_buffer 45 | 46 | expect(command_output).to eq 'Cannot toggle block: cursor is not on {, },'\ 47 | ' do or end' 48 | expect(actual_final_buffer).to eq normalize_string_indent(initial_buffer) 49 | end 50 | 51 | private 52 | 53 | def toggle_block initial_buffer 54 | ensure_cursor_presence initial_buffer 55 | write_file FILE_NAME, initial_buffer 56 | vim.edit FILE_NAME 57 | seek_to_cursor 58 | command_output = vim.command 'execute "normal \BlockToggle"' 59 | mark_cursor_position 60 | # Necessary because the client-server API is not really robust, making 61 | # random tests fail. I guess this sleep lets time to the server to process 62 | # all the commands before writing. 63 | sleep 0.1 64 | vim.write 65 | actual_final_buffer = IO.read(FILE_NAME).chomp 66 | [actual_final_buffer, command_output] 67 | end 68 | 69 | def seek_to_cursor 70 | # Go to the cursor position. 71 | vim.search '<.>' 72 | # Remove the visual cursor. 73 | vim.normal 'xlxh' 74 | end 75 | 76 | def mark_cursor_position 77 | # Here the double quotes are necessary for the escape to be interpreted. 78 | vim.normal "i<\la>" 79 | end 80 | 81 | def ensure_cursor_presence initial_buffer 82 | number_cursors = initial_buffer.scan(/<.>/).size 83 | expect(number_cursors).to eq(1), 84 | 'Error in the test specification: the initial buffer should have exactly'\ 85 | " one cursor defined, but #{number_cursors} were found." 86 | end 87 | end 88 | --------------------------------------------------------------------------------