aboutsummaryrefslogtreecommitdiff
path: root/plugin/subwords.vim
blob: a8a38ee44bf1323e6750fc37efaba78eb9203417 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
" subwords.vim. Motions and text objects for dealing with subwords.
"
" A subword is a word within an identifier, like a word within a camelCase
" identifer.
"

" Text object _. This references a "subword" 'a_' is around, which in the case
" of snake_case identifier will include the underscores (_). The 'i_' references
" the ci_

onoremap <silent> <Plug>(inner-sub-word) :<c-u>exec "norm " . <sid>v_subword(v:true, v:false)<cr>
vnoremap <silent> <Plug>(inner-sub-word) :<c-u>exec "norm " . <sid>v_subword(v:true, v:false)<cr>
onoremap <silent> <Plug>(around-sub-word) :<c-u>exec "norm " . <sid>v_subword(v:true, v:true)<cr>
vnoremap <silent> <Plug>(around-sub-word) :<c-u>exec "norm " . <sid>v_subword(v:true, v:true)<cr>

if ! exists('g:subwords_include_bindings')
  let g:subwords_include_bindings = 1
endif

" These mappings are the same as above, except prefer_camel is turned off, so
" snake case is used in the case of a conflict.
onoremap <silent> <Plug>(inner-sub-word-prefer-snake) :<c-u>exec "norm " . <sid>v_subword(v:false, v:false)<cr>
vnoremap <silent> <Plug>(inner-sub-word-prefer-snake) :<c-u>exec "norm " . <sid>v_subword(v:false, v:false)<cr>
onoremap <silent> <Plug>(around-sub-word-prefer-snake) :<c-u>exec "norm " . <sid>v_subword(v:false, v:true)<cr>
vnoremap <silent> <Plug>(around-sub-word-prefer-snake) :<c-u>exec "norm " . <sid>v_subword(v:false, v:true)<cr>

" Movement keys for subwords. These all have prefer_camel set to true, the idea
" being it's pretty easy to navigate underscores with f_ and t_, but more
" difficult to navigate upper case letters.
noremap   <silent> <Plug>(next-subword)   :<c-u>silent! call <SID>next_subword(v:false, v:true)<cr>
noremap   <silent> <Plug>(prev-subword)   :<c-u>silent! call <SID>next_subword(v:false, v:false)<cr>
vnoremap  <expr> <silent> <Plug>(next-subword)  visualmode() . ":\<c-u>silent! call \<SID>next_subword(visualmode(), v:true)\<cr>m'gv``"
vnoremap  <expr> <silent> <Plug>(prev-subword)  visualmode() . ":\<c-u>silent! call \<SID>next_subword(visualmode(), v:false)<cr>m'gv``"

function! s:clear_subword_mark()
  let s:subword_motion = ""
  return ''
endfunction

function! s:subword_repeat(char)
  if s:subword_motion == ''
    return a:char
  endif

  let mot = (s:subword_motion == 'next') != (a:char == ',')

  let s:subword_nosave = 1
  if mot
    return "\<Plug>(next-subword)"
  else
    return "\<Plug>(prev-subword)"
  endif
endfunction

noremap  <expr> <silent> <Plug>(subwords-replace-;) <SID>subword_repeat(';')
noremap  <expr> <silent> <Plug>(subwords-replace-,) <SID>subword_repeat(',')
vnoremap <expr> <silent> <Plug>(subwords-replace-;) <SID>subword_repeat(';')
vnoremap <expr> <silent> <Plug>(subwords-replace-,) <SID>subword_repeat(',')

noremap <expr> <silent> <Plug>(subwords-replace-t) <SID>clear_subword_mark() . "t" 
noremap <expr> <silent> <Plug>(subwords-replace-f) <SID>clear_subword_mark() . "f" 
noremap <expr> <silent> <Plug>(subwords-replace-T) <SID>clear_subword_mark() . "T"
noremap <expr> <silent> <Plug>(subwords-replace-F) <SID>clear_subword_mark() . "F" 

let s:subword_motion = ""
let s:subword_nosave = 0

" Return the type of meta-word (i.e. camelCase, snake_case). If
" a:prefer_camel is set, then a word like ThisIs_A_MixOfCamel_And_Snake will
" some_snake_cae SomeCamelCase SOME_CONSTANT_CASE
" return 'camel', otherwise it'll return 'snake'.
function! s:detect_word_type(prefer_camel, word) abort
  let is_camel = 0
  if a:word =~ '[a-z][A-Z]'
    let is_camel = 1

    if a:prefer_camel
      " The word contains a camelCase boundary.
      return 'camel'
    endif
  endif

  if a:word =~ '_'
    " The word contains a sake_case boundary.
    return 'snake'
  endif

  if is_camel
    return 'camel'
  endif

  " There is not discernible type, it's probably just a single word.
  return 'word'
endfunction

function! s:next_subword(vis, forward)
  if ! s:subword_nosave
    if a:forward
      let s:subword_motion = 'next'
    else
      let s:subword_motion = 'prev'
    endif
  endif

  let i = 0
  while i < v:count1
    call search(
          \ '\([a-z]\zs[A-Z]\ze\)\|\(\W\|_\)\zs\w\ze', (a:forward ? '' : 'b'))
    let i += 1
  endwhile

  let s:subword_nosave = 0
endfunction

" Highlight an inner subword.
function! s:v_subword(prefer_camel, around)

  " Detect the type of the word.
  let word = expand("<cword>")
  let t = s:detect_word_type(a:prefer_camel, word)

  let expr = ''

  if t == 'camel'
    let expr = 'v'

    let line = getline('.')
    let c = col('.') - 1
    let i = 0

    while line[c] =~ '[a-z]' && c >= 0
      let c -= 1
      let i += 1
    endwhile

    " camelCase

    if c >= 0 && ! (line[c] =~ '[A-Z_]')
      " If we are at the beginning of the meta word, the don't go back too far.
      let i -= 1
    endif

    if i > 0
      let expr .= printf("%dh", i)
    endif

    let expr .= 'o'

    let c = col('.') - 1
    let i = 0

    if line[c] =~ '[A-Z_]'
      " Actually on the starting capital letter, include it, but start counting
      " from the next character.
      let i += 1
      let c += 1
    endif

    while  c < len(line) && line[c] =~ '[a-z]'
      let c += 1
      let i += 1
    endwhile

    if i > 1
      let expr .= printf("%dl", i - 1)
    endif

  elseif t == "snake"
    let expr = 'v'

    let line = getline('.')
    let c = col('.') - 1
    let i = 0

    while c >= 0 && !( line[c] =~ '\W' ) && line[c] != '_'
      let c -= 1
      let i += 1
    endwhile

    let lhs_under = c >= 0 && line[c] == '_'

    let i -= 1
    let c += 1

    if i > 0
      let expr .= printf('%dho', i)
    endif

    let c = col('.') - 1
    let i = 0

    while c < len(line) && !(line[c] =~ '\W') && line[c] != '_'
      let c += 1
      let i += 1
    endwhile

    let rhs_under = c < len(line) && line[c] == '_'

    let i -= 1
    let c -= 1

    if i > 0
      let expr .= printf('%dl', i)
    endif

    if a:around
      if rhs_under
        let expr .= 'l'
      elseif lhs_under
        let expr .= 'oho'
      endif
    endif


  elseif t == "word"
    " Just a word? Easy peasy.
    let expr = 'viw'
  endif

  return expr
endfunction!

if g:subwords_include_bindings
  onoremap <silent> i_ <Plug>(inner-sub-word-prefer-snake)
  vnoremap <silent> i_ <Plug>(inner-sub-word-prefer-snake)
  onoremap <silent> a_ <Plug>(around-sub-word-prefer-snaked)
  vnoremap <silent> a_ <Plug>(around-sub-word-prefer-snaked)

  onoremap <silent> i- <Plug>(inner-sub-word)
  vnoremap <silent> i- <Plug>(inner-sub-word)
  onoremap <silent> a- <Plug>(around-sub-word)
  vnoremap <silent> a- <Plug>(around-sub-word)

  noremap  + <Plug>(next-subword)
  vnoremap + <Plug>(next-subword)
  noremap  - <Plug>(prev-subword)
  vnoremap - <Plug>(prev-subword)

  noremap  <silent> ; <Plug>(subwords-replace-;)
  noremap  <silent> , <Plug>(subwords-replace-,)
  vnoremap <silent> ; <Plug>(subwords-replace-;)
  vnoremap <silent> , <Plug>(subwords-replace-,)

  noremap <silent> t <Plug>(subwords-replace-t)
  noremap <silent> T <Plug>(subwords-replace-T)
  noremap <silent> f <Plug>(subwords-replace-f)
  noremap <silent> F <Plug>(subwords-replace-F)
endif