What is the easiest way to swap occurrences of two strings in Vim? - vim

What is the easiest way to replace all occurrences of string_a with string_b while at the same time changing anything that was already string_b into string_a? My current method is as follows:
:s/string_a/string_c/g
:s/string_b/string_a/g
:s/string_c/string_b/g
Although this works, it requires extra typing and seems inefficient. Does anybody know of a better way to do this?

I'd do it like this:
:%s/\v(foo|bar)/\={'foo':'bar','bar':'foo'}[submatch(0)]/g
But that's too much typing, so I'd do this:
function! Mirror(dict)
for [key, value] in items(a:dict)
let a:dict[value] = key
endfor
return a:dict
endfunction
function! S(number)
return submatch(a:number)
endfunction
:%s/\v(foo|bar)/\=Mirror({'foo':'bar'})[S(0)]/g
But that still requires typing foo and bar twice, so I'd do something like this:
function! SwapWords(dict, ...)
let words = keys(a:dict) + values(a:dict)
let words = map(words, 'escape(v:val, "|")')
if(a:0 == 1)
let delimiter = a:1
else
let delimiter = '/'
endif
let pattern = '\v(' . join(words, '|') . ')'
exe '%s' . delimiter . pattern . delimiter
\ . '\=' . string(Mirror(a:dict)) . '[S(0)]'
\ . delimiter . 'g'
endfunction
:call SwapWords({'foo':'bar'})
If one of your words contains a /, you have to pass in a delimiter which you know none of your words contains, .e.g
:call SwapWords({'foo/bar':'foo/baz'}, '#')
This also has the benefit of being able to swap multiple pairs of words at once.
:call SwapWords({'foo':'bar', 'baz':'quux'})

You can do this easily with Tim Pope's Abolish plugin
:%S/{transmit,receive}/{receive,transmit}

Here is how I swap two words skip & limit:
%s/skip/xxxxx/g | %s/limit/skip/g | %s/xxxxx/limit/g
Pretty sure someone could turn it into a shorter command which accepts two arguments.

The swapstrings plugin provides a handy command for this:
:SwapStrings string_a string_b

You can do it with a single command as shown in my code below:
:%s/\<\(string_a\|string_b\)\>/\=strpart("string_bstring_a", 8 * ("string_b" == submatch(0)), 8)/g

Related

Vim: Archiving text selections into other file

I'd like to create a command and bindings for appending a selected piece of text into another file located next to it with a timestamp.
I've put some scraps together from other posts, yet I barely know what I am doing and I am not getting what I expect.
fun! MoveSelectedLinesToFile(filename)
exec "'<,'>w! >>" . a:filename
norm gvd
endfunc
fun! ArchiveSelectedLinesToFile(filename)
call writefile( ["[" . strftime("%Y-%m-%d %H:%M:%S") . "]"], a:filename, "a" )
call MoveSelectedLinesToFile(a:filename)
call writefile( ["",""], a:filename, "a" )
endfunc
vnoremap a :call ArchiveSelectedLinesToFile(expand('%:p') . '.arc.md')<CR>
using this on a sequence of lines 4-6 of this content:
1
2
3
4
5
6
7
8
9
The archive file shows:
[2022-09-19 14:52:10]
[2022-09-19 14:52:10]
while it should show
[2022-09-19 14:52:10]
4
5
6
and the source file was altered to
1
2
3
8
9
which is one line to much as 7 was wrongfully taken.
I am on Windows, if that means anything.
Q:
I am getting E16 invalid range errors for all 3 lines of ArchiveSelectedLinesToFile. Where from there exactly? And why?
Is there a way to maybe just construct a block of text and append that instead of adding the different bits, like the timestamp and the whitespace one by one? Make it one coherent operation? Maybe only bother the piping-mechanism once?
Why is this so inconsistent? Sometimes for one triggering I get 2 timestamps with no payload into the other file and once it even worked fine.
Is there some easier way of doing archiving in this or a similar way?
I am aware that none of this is probably as clean as it could be, yet I do not know better as of now. Improvement suggestions of form are appreciated.
Background:
I recently started using VimWiki for taking notes, including my TODOs, my call-stack if you will. I have one main TODO file, however large tasks warrant their own file.
Now whenever I am done with a TODO I might have put more notes under the heading of that TODO since its inception, holding valuable information for future me.
Without going to the length of creating an extra wiki entry for the topic, traditionally I just deleted the lines, I've come to think it would be neat to archive most of them away instead. I think using Version Control for this is overkill.
By putting those contents into another file with a timestamp this acts as a sort of history. Further I could still choose between archiving and deleting to decide what might be relevant further and what's not.
:help writefile() takes a list as first argument so a better strategy would be to build the list first, with timestamp and all, and then use a single writefile().
For that, you need to start by handling ranges properly. When you call a function defined like this over a range, the function is called for each line:
function! Foo()
echo line('.')
endfunction
:4,6call Foo()
4
5
6
which is totally fine if that's your goal, but this is not the case here. You want your function to be called once, so you need to define it with the range argument:
function! Foo() range
echo line('.')
endfunction
:4,6call Foo()
4
which allows you to handle the range yourself, within the function:
function! Foo() range
echo a:firstline
echo a:lastline
endfunction
:4,6call Foo()
4
6
See :help :func-range.
First, generate a timestamp:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
endfunction
Second, store the selected lines in a variable:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
endfunction
See :help getline().
Third, put them together in a list:
function! ArchiveSelectedLinesToFile() range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
endfunction
See :help list-concatenation.
Fourth, write the list to the given file:
function! ArchiveSelectedLinesToFile(filename) range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
call writefile(archival_data, a:filename, 'a')
endfunction
Fifth, delete the selected lines:
function! ArchiveSelectedLinesToFile(filename) range
let timestamp = '[' . strftime('%Y-%m-%d %H:%M:%S') . ']'
let lines = getline(a:firstline, a:lastline)
let archival_data = [timestamp] + lines
call writefile(archival_data, a:filename, 'a')
execute a:firstline . ',' . a:lastline . 'd _'
endfunction
See :help :range, :help :d and :help "_.
You are not done yet, though, because of all the text objects that start with a. This will cause timing issues so you will have to map your function call to a different key that is not the start of some command, motion, or mapping. Additionally, you might want to restrict your mapping to visual mode. Here is an example with <F5>, YMMV:
xnoremap <F5> :call ArchiveSelectedLinesToFile(expand('%:p') . '.arc.md')<CR>

In vim, how to split a word and flip the halves? FooBar => BarFoo

I sometimes write a multi-word identifier in one order, then decide the other order makes more sense. Sometimes there is a separator character, sometimes there is case boundary, and sometimes the separation is positional. For example:
$foobar becomes $barfoo
$FooBar becomes $BarFoo
$foo_bar becomes $bar_foo
How would I accomplish this in vim? I want to put my cursor on the word, hit a key combo that cuts the first half, then appends it to the end of the current word. Something like cw, but also yanking into the cut buffer and then appending to the current word (eg ea).
Nothing general and obvious comes to mind. This is more a novelty question than one of daily practical use, but preference is given to shortest answer with fewest plugins. (Hmm, like code golf for vim.)
You can use this function, it swaps any word of the form FooBar, foo_bar, or fooBar:
function! SwapWord()
" Swap the word under the cursor, ex:
" 'foo_bar' --> 'bar_foo',
" 'FooBar' --> 'BarFoo',
" 'fooBar' --> 'barFoo' (keeps case style)
let save_cursor = getcurpos()
let word = expand("<cword>")
let match_ = match(word, '_')
if match_ != -1
let repl = strpart(word, match_ + 1) . '_' . strpart(word, 0, match_)
else
let matchU = match(word, '\u', 1)
if matchU != -1
let was_lower = (match(word, '^\l') != -1)
if was_lower
let word = substitute(word, '^.', '\U\0', '')
endif
let repl = strpart(word, matchU) . strpart(word, 0, matchU)
if was_lower
let repl = substitute(repl, '^.', '\L\0', '')
endif
else
return
endif
endif
silent exe "normal ciw\<c-r>=repl\<cr>"
call setpos('.', save_cursor)
endf
Mapping example:
noremap <silent> gs :call SwapWord()<cr>
Are you talking about a single instance, globally across a file, or generically?
I would tend to just do a global search and replace, e.g.:
:1,$:s/$foobar/$barfoo/g
(for all lines, change $foobar to $barfoo, every instance on each line)
EDIT (single occurrence with cursor on the 'f'):
3xep
3xep (had some ~ in there before the re-edit of the question)
4xea_[ESC]px
Best I got for now. :)
nnoremap <Leader>s dwbP
Using Leader, s should now work.
dw : cut until the end of the word from cursor position
b : move cursor at the beginning of the word
P : paste the previously cut part at the front
It won't work for you last example though, you have to add another mapping to deal with _ .
(If you don't know what Leader is, see :help mapleader)

Case insensitive f key in vim?

Does anyone know of any way to get the 'f' key in vim normal command mode to operate case insensitive
Example
function! OpenCurrentFileInMarked()
In the above line, if I am at the start of the line I want to be able to type 'fi'and get the first 'i' and then ';' to toggle to the capital 'I' in the middle of the function name. I would prefer that the 'f' key is always bound to case insensitivity. Would work much better for me as a default.
The easy answer here is: use a plugin. Others have had the idea before.
Fanf,ingTastic;: Find a char across lines
The main purpose of this plugin is to make f and t cross the line boundary.
To make them ignore case you need to let g:fanfingtastic_ignorecase = 1 in your vimrc.
ft_improved: improved f/t command
Same here.
To make it ignore case you again need to set a variable in your vimrc, this time let g:ft_improved_ignorecase = 1.
The first part of this (case insensitive f) is actually in the reference manual as an example of how to use the getchar() function:
This example redefines "f" to ignore case:
:nmap f :call FindChar()<CR>
:function FindChar()
: let c = nr2char(getchar())
: while col('.') < col('$') - 1
: normal l
: if getline('.')[col('.') - 1] ==? c
: break
: endif
: endwhile
:endfunction
See :help getchar().
You'll need to save the character returned and write a similar map for ; if you want that to work too.
You can't do this with regular vim commands. However, you can write your own function and bind the f key to it.

Mapping/macro to 'smartly' auto-create pairs of apostrophes in vim (and ignore contractions)

I'm currently using closepairs for my auto-closing needs, and it works pretty well. However, there is one caveat -- apostrophes. Don't get me wrong, I need apostrophes closed all the time. I don't want to just disable them. But whenever I type in plain text, whenever there are any contractions (I'm, Don't, Can't)...these apostrophes get made.
Now I could just type to delete them as soon as they can, but doing it every time is a bit impractical.
Does anyone know how I can possibly modify the closepairs script to only autoclose single quotes/apostrophes if they are the start of a word? That is, they are preceded by a whitespace character?
Here is the current code:
inoremap <expr> " <SID>pairquotes('"')
inoremap <expr> ' <SID>pairquotes("'")
function! s:pairquotes(pair)
let l:col = col('.')
let l:line = getline('.')
let l:chr = l:line[l:col-1]
if a:pair == l:chr
return "\<right>"
else
return a:pair.a:pair."\<left>"
endf
I don't know closepairs, but the AutoClose - Inserts matching bracket, paren, brace or quote plugin handles this well. You'll find a list of plugin alternatives on the Vim Tips Wiki.
Are you sure you want to autocomplete only after whitespace? In that case, something like function('string') would not autocomplete after the parenthesis.
Regardless, you can check the previous character against some regex. For example, to avoid autocompletion after letters:
function! s:pairquotes(pair)
let l:line = getline('.')
let l:col = col('.')
let l:chr = l:line[l:col - 1]
let l:prev = l:line[l:col - 2]
if l:chr == a:pair
return "\<right>"
elseif l:prev !~ "[A-Za-z]"
return a:pair . a:pair . "\<left>"
else
return a:pair
endif
endfunction
Note that there are exceptions even with this conservative example, like typing r'regex' in Python, so it might also make sense to define filetype-specific behavior.

CamelCase Expansion in Vim like Intellij Idea?

In Intellij Idea, there's a feature. Let's say I have used a variable myCamelCase somewhere in my code. Then if I type mCC and press Ctrl-Enter or some such key combination, it expands to myCamelCase. Is there something similar in Vim?
Okay, forgive me for answering twice, but since my first attempt missed the point, I'll have another go. This is more complicated than I thought, but possibly not as complicated as I have made it (!).
This is now modified to suggest all matching variable names.
First of all, here's a function to generate the 'mCC' abbreviation from the 'myCamelCase' string:
function! Camel_Initials(camel)
let first_char = matchstr(a:camel,"^.")
let other_char = substitute(a:camel,"\\U","","g")
return first_char . other_char
endfunction
Now, here's a function that takes an abbreviation ('mCC') and scans the current buffer (backwards from the current line) for "words" that have this abbreviation. A list of all matches is returned:
function! Expand_Camel_Initials(abbrev)
let winview=winsaveview()
let candidate=a:abbrev
let matches=[]
try
let resline = line(".")
while resline >= 1
let sstr = '\<' . matchstr(a:abbrev,"^.") . '[a-zA-Z]*\>'
keepjumps let resline=search(sstr,"bW")
let candidate=expand("<cword>")
if candidate != a:abbrev && Camel_Initials(candidate) == a:abbrev
call add( matches, candidate )
endif
endwhile
finally
call winrestview(winview)
if len(matches) == 0
echo "No expansion found"
endif
return sort(candidate)
endtry
endfunction
Next, here's a custom-completion function that reads the word under the cursor and suggests the matches returned by the above functions:
function! Camel_Complete( findstart, base )
if a:findstart
let line = getline('.')
let start = col('.') - 1
while start > 0 && line[start - 1] =~ '[A-Za-z_]'
let start -= 1
endwhile
return start
else
return Expand_Camel_Initials( a:base )
endif
endfunction
To make use of this, you must define the "completefunc":
setlocal completefunc=Camel_Complete
To use insert-mode completion, type CTRL-X CTRL-U, but I usually map this to CTRL-L:
inoremap <c-l> <c-x><c-u>
With this code in your vimrc you should find that typing mCC followed by CTRL-L will make the expected replacement. If no matching expansion is found, the abbreviation is unchanged.
The code isn't water-tight, but it works in all the simple cases I tested. Hope it helps. Let me know if anything needs elucidating.
There is a plugin for this in Vim called vim-abolish. Use the map crc to expand to camel case.

Resources