aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2022-08-24 01:26:08 -0600
committerJosh Rahm <joshuarahm@gmail.com>2022-08-24 01:26:08 -0600
commit0d83f507cef5f59d4b8afeed9633978394f2331a (patch)
treedd586ecd98dcfc16327d95ed1efc41b3f0796126
parenteabc175e8d7649f0d3308e8bec57f876107b8335 (diff)
downloadfieldmarshal.vim-0d83f507cef5f59d4b8afeed9633978394f2331a.tar.gz
fieldmarshal.vim-0d83f507cef5f59d4b8afeed9633978394f2331a.tar.bz2
fieldmarshal.vim-0d83f507cef5f59d4b8afeed9633978394f2331a.zip
Implement subwords.vim
This plugin adds support for motions and text object based around subwords. A "subword" is a constituent word of a larger "metaword" in some case format. For examle "camel" is a "subword" of the "metaword" "camelCase" or "camel_case". The text objects added are * `i-` the inner subword. For this would match something like "[camel]_case" or "[camel]Case" * `a-` around the subword. This distinction only matters for snake_case, where the match would be "[camel_]case". For camelCase, this is the same as `i-`. * `i_`/`a_` like the above, but interpret ambiguous metawords as snake_case. For example, the metaword ThisIs_AnExample is ambiguous because it's both snake_case and camelCase. `i-`/`a-` interpet it as camelCase, but `i_`/`a_` interpet it as snake_case. ThisIs_AnExample ^ i- === [This]Is_AnExample a- === [This]Is_AnExample i_ === [ThisIs]_AnExample a_ === [ThisIs_]AnExample The motions added are * `M--` (Meta + hyphen) Move forward one subword * `M-_` (Meta + underscore) Move backward one subword.
-rw-r--r--plugin/subwords.vim174
1 files changed, 174 insertions, 0 deletions
diff --git a/plugin/subwords.vim b/plugin/subwords.vim
new file mode 100644
index 0000000..e31796c
--- /dev/null
+++ b/plugin/subwords.vim
@@ -0,0 +1,174 @@
+" 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> i- :<c-u>exec "norm " . <sid>v_subword(v:true, v:false)<cr>
+vnoremap <silent> i- :<c-u>exec "norm " . <sid>v_subword(v:true, v:false)<cr>
+onoremap <silent> a- :<c-u>exec "norm " . <sid>v_subword(v:true, v:true)<cr>
+vnoremap <silent> a- :<c-u>exec "norm " . <sid>v_subword(v:true, v:true)<cr>
+
+" 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> i_ :<c-u>exec "norm " . <sid>v_subword(v:false, v:false)<cr>
+vnoremap <silent> i_ :<c-u>exec "norm " . <sid>v_subword(v:false, v:false)<cr>
+onoremap <silent> a_ :<c-u>exec "norm " . <sid>v_subword(v:false, v:true)<cr>
+vnoremap <silent> a_ :<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> <M--> :<c-u>silent! call <SID>next_subword(v:false, v:true)<cr>
+noremap <silent> <M-_> :<c-u>silent! call <SID>next_subword(v:false, v:false)<cr>
+vnoremap <silent> <M--> v:<c-u>silent! call <SID>next_subword(v:true, v:true)<cr>m'gv``
+vnoremap <silent> <M-_> v:<c-u>silent! call <SID>next_subword(v:true, v:false)<cr>m'gv``
+
+" 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_case 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)
+ 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
+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!