Indent a block of code on the basis of a single character - vim

The question's title might sound a little vague, so I'll explain the situation here more clearly.
I have these lines of code in a file which I want to align with respect to the character =.
const service = require('./service');
const baseService = require('./baseService');
const config = require('../config');
const Promise = require('bluebird');
const errors = require('../errors');
I want the above lines to somehow look like this
const service = require('./service');
const baseService = require('./baseService');
const config = require('../config');
const Promise = require('bluebird');
const errors = require('../errors');
I want all the = characters to lie in the same column and shift the after-coming code accordingly. What can I do to achieve this task?
A plugin capable of doing this would be nice, but it'd be great if I could do this without the aid of any plugin. That way I'd also learn something.

Using GNU tools
:%!column -t -s= -o=
% ............. current file
! ............. use external command
-t ............. use tabs
-s ............. input separator
-o ............. output separator
Instead of whole file it can be a range of lines 1,5 or you can select a paragraph with vip
I ended up creating a function called AlignText that uses column command to solve this issue:
" Align text by a chosen char
" https://stackoverflow.com/questions/57093175/
" https://vi.stackexchange.com/a/2412/7339
if !exists('*AlignText')
function! AlignText(param) range
execute a:firstline . ',' . a:lastline . '!column -t -s' . a:param . ' -o' . a:param
endfunction
endif
command! -range=% -nargs=1 Align <line1>,<line2>call AlignText(<q-args>)
" :Align =
" :8,$ Align =
If you wnat to test this function before put it into your vimrc, copy the code to your clipboard and then try this:
:#+
Now you can use something like this:
:15,22Align /
:Align ,
I by any change you want to use the function call instead of the command one, don't forget to pass the range and put the argument between quotes.
A slitly different version that accepts nor arguments and uses just "column -t"
" https://stackoverflow.com/questions/57093175/
" https://vi.stackexchange.com/a/2412/7339
function! AlignText(...) range
if a:0 < 1
execute a:firstline . ',' . a:lastline . '!column -t'
else
execute a:firstline . ',' . a:lastline . '!column -t -s' . a:1 . ' -o' . a:1
endif
endfunction
command! -range=% -nargs=? Align <line1>,<line2>call AlignText(<f-args>)

there are plugins can do this kind of alignment. E.g.
Align or easy-align. I have been using Align for a long time, for your requirement, I just select those lines and do <leader>t=
https://github.com/vim-scripts/Align
https://github.com/junegunn/vim-easy-align
You can of course code by yourself, you find out the max length before the =, then you know how many spaces you should insert between partBefore and =. (the diff) on each line.

(NOTE: Originally answered at the Vi and Vim Stack Exchange.)
If you're in a pinch and want to get the expressions aligned, without having to install and learn any plug-ins, here is a quick way to do it.
Select the lines on a visual selection. For example, if this is your whole buffer, you could use ggVG, but if these lines are in the middle of a file, just select appropriately. Perhaps V4j?
Insert enough whitespace before the =, which you can do with :normal f=9i . (Note the "space" at the end.) This will add 9 spaces before each = in the lines of the visual selection. If 9 is not enough, add more (like 99 or 999, as much as you want.) Note that when you type : with the visual selection, Vim will automatically insert the range, so the actual command is :'<,'>normal f=9i , but you don't need to type those characters.
Move to the column where you want the =s to be flushed to. In this case, line 2 has the longest variable name, so move to two spaces after the end of that name, which is where the =s should be at the end. If this is the whole buffer, you could use 2G2e2l to get there.
Staying on that same column, move to the first line of the block. In this case, you're moving from line 2 to line 1, so k is enough.
Start visual-block selection, pressing Ctrl-V.
Move to the last line of the block. If this is the whole buffer, you could use G, if this is the middle of a file, you could use 4j to go four lines down, etc.
(Optional) Use $ to select until the end of the line, on every line of the visual block selection. UPDATE: This step is actually not necessary here, since < will work correctly even if you don't select lines to the end. Using $ is important when using e.g. A to append to the end of all lines though.
Now you can use the < command to shift the lines left, but until they hit the left of the visual block. Each < will shift them by one 'shiftwidth' only, so you're likely to need more than one. So, to be sure, use 9< (or 99<, or 999<, if you added tons of spaces in step 2.)
VoilĂ !
This is a pretty cool technique and it can be helpful when you need more flexibility than plug-ins can afford you. It's a good one to learn and keep on your Vim toolbox.

Related

Vimscript remove control characters

I have a vimscript function shown below that performs a search and replace on the currently yanked/copied text buffer and pastes them into a file.
function Repaste(s, ...)
for i in a:000
let sub = substitute(getreg('"'), a:s, i, 'ge')
let sane = substitute(sub, '[^[:print:]]', '\n', 'ge')
call append(line('.'), sane)
endfor
endfunction
command -nargs=* RP call Repaste(<f-args>)
When I call this function I get ^# characters in place of new lines.
Here is an example of the yanked/copied text
set cindent
Here is an example of the command executed
:RP c d e f
Here is the output
set findent ^#
set eindent ^#
set dindent ^#
How do i remove these characters and why do they appear? Thanks.
The append() function is a low-level one. :help append() shows that the {expr} as a String type is inserted as one text line, regardless of newlines in its content. The ^# is the representation of \n inside a line; cp :help <Nul>.
If you really want to keep using append(), you have to provide a List type; to obtain this, you can split() your String:
call append(line('.'), split(sane, '\n'))
However, I think you're better off by using a higher-level function to insert the created lines, using :put with the expression register to insert the contents of your variable:
put =sane
This will:
automatically handle embedded newlines
set the change marks '[ and '] to the inserted text
print a message 4 more lines (if the inserted number exceeds the 'report' threshold).
Replacing the call to append with put=sub produces the required result.

Yanking all marked lines in vim

Often times when reviewing log files in vim, I'll highlight interesting lines using marks. At some point, I'd like to be able to copy all of the interesting lines (either all marked lines, or a list of marks) to either a register or another file (it doesn't really matter which; the goal is to facilitate writing a summary). I haven't been able to find any built in way to do this; is it possible in vim?
I suppose it's probably a fairly straightforward function; probably looking something like this, but my vimscript abilities are very weak:
for cur_mark in list_of_marks
goto mark
yank current line and append to register
Has anyone ever written anything similar that they can point me to?
Thanks
EDIT: I posted the accepted solution at https://github.com/mikeage/vim-yankmarks
As always, there are few things that are more motivating than asking for help. Here's what I came up with; feedback welcome.
function! Yankmark()
let save_cursor = getpos(".")
let n = 0
" I should really make this a parameter...
let marks_to_yank="abcdefghijklmnopqrstuvwxyz"
let nummarks = strlen(marks_to_yank)
" Clear the a register
let #a=''
while n < nummarks
let c = strpart(marks_to_yank, n, 1)
" Is the mark defined
if getpos("'".c)[2] != 0
" using g' instead of ' doesn't mess with the jumplist
exec "normal g'".c
normal "Ayy
endif
let n = n + 1
endwhile
call setpos('.', save_cursor)
endfunction
Mikeage had a great idea; here's a more refined version of his function turned into a command:
":YankMarks [{marks}] [{register}]
" Yank all marked (with [a-z] / {marks} marks) lines into
" the default register / {register} (in the order of the
" marks).
function! s:YankMarks( ... )
let l:marks = 'abcdefghijklmnopqrstuvwxyz'
let l:register = '"'
if a:0 > 2
echohl ErrorMsg
echomsg 'Too many arguments'
echohl None
return
elseif a:0 == 2
let l:marks = a:1
let l:register = a:2
elseif a:0 == 1
if len(a:1) == 1
let l:register = a:1
else
let l:marks = a:1
endif
endif
let l:lines = ''
let l:yankedMarks = ''
for l:mark in split(l:marks, '\zs')
let l:lnum = line("'" . l:mark)
if l:lnum > 0
let l:yankedMarks .= l:mark
let l:lines .= getline(l:lnum) . "\n"
endif
endfor
call setreg(l:register, l:lines, 'V')
echomsg printf('Yanked %d line%s from mark%s %s',
\ len(l:yankedMarks),
\ len(l:yankedMarks) == 1 ? '' : 's',
\ len(l:yankedMarks) == 1 ? '' : 's',
\ l:yankedMarks
\) . (l:register ==# '"' ? '' : ' into register ' . l:register)
endfunction
command! -bar -nargs=* YankMarks call <SID>YankMarks(<f-args>)
A different way of accomplishing this might be using the :global command. The global command takes the form :g/{pattern}/{cmd}. The command, {cmd}, will be executed on all lines matching {pattern}.
Append lines matching a pattern to a register:
:g/pattern/yank A
Append matching line to a log file:
:g/pattern/w >> file.log
Of course if you want to find line matching a mark you can match it in your pattern. The following pattern matches a line with mark m.
:g/\%'m/w >> file.log
To do something like this. (Note: I am using \v to turn on very magic)
:g/\v(%'a|%'b|%'m)/yank A
Of course if a pattern won't work you can do this by hand. Instead of marking the lines just build up the lines as you go. Just yank a line to an uppercase register to append.
"Ayy
Or do a write append with a range of a single line
:.w >> file.log
For more help see
:h :g
:h :w_a
:h /\%'m
:h /\v
You can do something like:
:redir #a
:silent marks XYZN
:redir END
"ap
That way the output of the :marks command will be redirected to the a register. Note, that it will only lists (in the above case) the X, Y, Z and N marks (as the arguments), and if there was an a register, it will be deleted/overwritten.
Also note, that it might not give the desired output, but gives you a starting point...
I like the solution from Mikeage, though I would probably solve this with the multiselect - Create multiple selections and operate plugin. This also has the benefit that you don't run out of marks.
With the plugin, you can select lines with <Leader>msa or :MSAdd. Finally, yank all lines with:
:let #a=''
:MSExecCmd yank A
If you use an upper-case register name when yanking into a specific register, Vim will append the yanked content instead of overwriting the register's value.
So, for example:
"ayy - yank current line to register a, overwriting
[move]
"Ayy - append this line to register a
[move]
"ap - paste all yanked material
See :help quotea for more details.

How to make Vim's global command :g/ work on per occurrence basis

Typically Vim's global command :g// works on per line basis. Is it possible to make it work on per occurrence basis as there could be more than one occurrence on a line.
Not a direct answer, but you could use something like :rubydo, which will run some ruby scriptlet per line of code. Combining that with gsub in ruby should get you the ability to do just about anything per occurrence of a match. Of course, you will need to do it with ruby code, which may not give you access to everything that you might need without hassle (register appending would be annoying for instance)
:[range]rubyd[o] {cmd} Evaluate Ruby command {cmd} for each line in the
[range], with $_ being set to the text of each line in
turn, without a trailing <EOL>. Setting $_ will change
the text, but note that it is not possible to add or
delete lines using this command.
The default for [range] is the whole file: "1,$".
You can try:
command! -bang -nargs=1 -range OGlobal
\ <line1>,<line2>call s:Global("<bang>", <f-args>)
function! s:Global(bang, param) range
let inverse = a:bang == '!'
" obtain the separator character
let sep = a:param[0]
" obtain all fields in the initial command
let fields = split(a:param, sep)
" todo: handle inverse
let l = a:firstline
while 1
let l = search(fields[0], 'W')
if l == -1 || l > a:lastline
break
endif
exe fields[1]
endwhile
endfunction
Which you can use as :global, except that the command name is different (and that the bang option as not been implemented yet)

Escape characters during paste in vim

I copy stuff from output buffers into C++ code I'm working on in vim.
Often this output gets stuck into strings. And it'd be nice to be able to escape all the control characters automatically rather than going back and hand editing the pasted fragment.
As an example I might copy something like this:
error in file "foo.dat"
And need to put it into something like this
std::string expected_error = "error in file \"foo.dat\""
I'm thinking it might be possible to apply a replace function to the body of the last paste using the start and end marks of the last paste, but I'm not sure how to make it fly.
UPDATE:
Joey Mazzarelli sugested using
`[v`]h:%s/\%V"/\\"/g
after a paste.
Since no explaination was given for what that was going and I initially found it a bit terse, but hard to explain in the comments I thought I'd put an explaination of what I think that does here:
`[ : Move to start of last paste
v : Start visual mode
`] : Move to end of last paste
h : adjust cursor one position left
:% : apply on the lines of the selection
s/ : replace
\%V : within the visual area
" : "
/ : with
\\" : \"
/g : all occurrences
This seems like a good approach, but only handles the one character, ", I'd like it to handle newlines, tabs, and other things that might be expected to fall in text. (Probably not general unicode though) I understand that may not have been clear in the problem definition.
Here are a couple of vimscript functions that should do what you want.
EscapeText() transforms arbitrary text to the C-escaped equivalent. It converts newline to \n, tab to \t, Control+G to \a, etc., and generates octal escapes (like \o032) for special characters that don't have a friendly name.
PasteEscapedRegister() escapes the contents of the register named by v:register, then inserts it into the current buffer. (The register is restored when the function returns, so the function can be called repeatedly without escaping the register contents multiple times.)
There are also a couple of key mappings included to make PasteEscapedRegister() easy to use interactively. <Leader>P pastes escaped register contents before the cursor position, and <Leader>p pastes after. Both can be prefixed with a register specification, like "a\P to paste the escaped contents of register a.
Here's the code:
function! EscapeText(text)
let l:escaped_text = a:text
" Map characters to named C backslash escapes. Normally, single-quoted
" strings don't require double-backslashing, but these are necessary
" to make the substitute() call below work properly.
"
let l:charmap = {
\ '"' : '\\"',
\ "'" : '\\''',
\ "\n" : '\\n',
\ "\r" : '\\r',
\ "\b" : '\\b',
\ "\t" : '\\t',
\ "\x07" : '\\a',
\ "\x0B" : '\\v',
\ "\f" : '\\f',
\ }
" Escape any existing backslashes in the text first, before
" generating new ones. (Vim dictionaries iterate in arbitrary order,
" so this step can't be combined with the items() loop below.)
"
let l:escaped_text = substitute(l:escaped_text, "\\", '\\\', 'g')
" Replace actual returns, newlines, tabs, etc., with their escaped
" representations.
"
for [original, escaped] in items(charmap)
let l:escaped_text = substitute(l:escaped_text, original, escaped, 'g')
endfor
" Replace any other character that isn't a letter, number,
" punctuation, or space with a 3-digit octal escape sequence. (Octal
" is used instead of hex, since octal escapes terminate after 3
" digits. C allows hex escapes of any length, so it's possible for
" them to run up against subsequent characters that might be valid
" hex digits.)
"
let l:escaped_text = substitute(l:escaped_text,
\ '\([^[:alnum:][:punct:] ]\)',
\ '\="\\o" . printf("%03o",char2nr(submatch(1)))',
\ 'g')
return l:escaped_text
endfunction
function! PasteEscapedRegister(where)
" Remember current register name, contents, and type,
" so they can be restored once we're done.
"
let l:save_reg_name = v:register
let l:save_reg_contents = getreg(l:save_reg_name, 1)
let l:save_reg_type = getregtype(l:save_reg_name)
echo "register: [" . l:save_reg_name . "] type: [" . l:save_reg_type . "]"
" Replace the contents of the register with the escaped text, and set the
" type to characterwise (so pasting into an existing double-quoted string,
" for example, will work as expected).
"
call setreg(l:save_reg_name, EscapeText(getreg(l:save_reg_name)), "c")
" Build the appropriate normal-mode paste command.
"
let l:cmd = 'normal "' . l:save_reg_name . (a:where == "before" ? "P" : "p")
" Insert the escaped register contents.
"
exec l:cmd
" Restore the register to its original value and type.
"
call setreg(l:save_reg_name, l:save_reg_contents, l:save_reg_type)
endfunction
" Define keymaps to paste escaped text before or after the cursor.
"
nmap <Leader>P :call PasteEscapedRegister("before")<cr>
nmap <Leader>p :call PasteEscapedRegister("after")<cr>
This might at least get you started...
After pasting it in:
`[v`]h:%s/\%V"/\\"/g
You can obviously map that to something easier to type.
While Joeys solution looks like it might be extensible to cover all the cases that I need, I thought I'd share my partial solution using vims python integration (Since I'm more familiar at python than vim script)
# FILE : tocstring.py
import vim
def setRegister(reg, value):
vim.command( "let #%s='%s'" % (reg, value.replace("'","''") ) )
def getRegister(reg):
return vim.eval("#%s" % reg )
def transformChar( map, c):
if c in map:
return map[c]
return c
def transformText( map, text ):
return ''.join( [ transformChar(map,c) for c in text ] )
cmap={}
cmap["\\"]="\\\\"
cmap["\n"]="\\n"
cmap["\t"]=r"\\t"
cmap['"']="\\\""
def convertToCString( inregister, outregister ):
setRegister(outregister, transformText( cmap, getRegister(inregister) ) )
Then in my .vimrc or other conf file I can put
# FILE cpp.vim
python import tocstring
# C-Escape and paste the currently yanked content
nmap <Leader>P :python tocstring.convertToCString("#","z")<CR>"zP
# C-Escape and paste the current visual selection
vmap <Leader>P "zd:python tocstring.convertToCString("z","z")<CR>"zP
It would be nice if I could the first function to work so that "a\P pasted the transformed contents of the "a" register, and I assume this is doable using v:register somehow, but it escapes me.
A version of this that works in the same way as Joeys solution could be crafted as
nmap <Leader>P `[v`]"zd:python tocstring.convertToCString("z","z")<CR>"zP
Acknowledgement : This uses code from Can you access registers from python functions in vim for interacting with registers from vims python
for Java/JavaScript type of escaping one can use json_encode
nmap <leader>jp :call setreg('e', json_encode(#+))\| normal "ep<CR>
json_encode(#+) - json encode content of register + (mapped to clipboard)
setreg('e',...) - write it to register e
normal "ep - paste content of register e

How to "apply" backspace characters within a text file (ideally in vim)

I have a log file with backspace characters in it (^H). I'm looking through the file in Vim and it can be quite hard to see what's going on.
Ideally I'd like to be able to "apply" all the ^H on a given line/range so that I can see the final result.
I'd much rather do this within Vim on a line-by-line basis, but a solution which converts the whole file is better than nothing.
Turn on the 'paste' option (using :set paste), and then press dd i <CTRL-R> 1 <ESC> on each line that you want to apply the backspaces to. This also works if you delete multiple lines, or even the whole file.
The key here is that you are using <CTRL-R> 1 in insert mode to 'type out' the contents of register 1 (where your deleted lines just got put), and 'paste' option prevents Vim from using any mappings or abbreviations.
I googled this while trying to remember the command I had used before to `apply' backspaces, and then I remembered it: col -b - here is the manpage. (It does a little more and comes from BSD or more exactly AT&T UNIX as the manpage says, so if you are on Linux you may need to install an additional package, on debian its in bsdmainutils.)
Simplistic answer:
:%s/[^^H]^H//g
where ^^H is:
Literal ^ character
Ctrl-V Ctrl-H
and repeat it couple of times (until vim will tell you that no substitutions have been made
If you want without repetition, and you don't mind using %!perl:
%!perl -0pe 's{([^\x08]+)(\x08+)}{substr$1,0,-length$2}eg'
All characters are literal - i.e. you don't have to do ctrl-v ... anywhere in above line.
Should work in most cases.
All right, here is a bare-metal solution.
Copy this code into a file named crush.c:
#include <stdio.h>
// crush out x^H sequences
// there was a program that did this, once
// cja, 16 nov 09
main()
{
int c, lc = 0;
while ((c = getchar()) != EOF) {
if (c == '\x08')
lc = '\0';
else {
if (lc)
putchar(lc);
lc = c;
}
}
if (lc)
putchar(lc);
}
Compile this code with your favorite compiler:
gcc crush.c -o crush
Then use it like this to crush out those bothersome sequences:
./crush <infilename >outfilename
Or use it in a pipeline ("say" is a speech-to-text app on the Mac)
man date | ./crush | say
You can copy crush to your favorite executable directory (/usr/local/bin, or some such) and then reference it as follows
man date | crush | say
Just delete all occurrences of .^H (where . is the regex interpretation of .):
:s/.^H//g
(insert ^H literally by entering Ctrl-V Ctrl-H)
That will apply to the current line. Use whatever range you want if you want to apply it to other lines.
Once you done one :s... command, you can repeat on another line by just typing :sg (you need to g on the end to re-apply to all occurrences on the current line).
How about the following function? I've used \%x08 instead of ^H as it's easier to copy and paste the resulting code. You could type it in and use Ctrl-V Ctrl-H if you prefer, but I thought \%x08 might be easier. This also attempts to handle backspaces at the start of the line (it just deletes them).
" Define a command to make it easier to use (default range is whole file)
command! -range=% ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Function that does the work
function! ApplyBackspaces() range
" For each line in the selected lines
for index in range(a:firstline, a:lastline)
" Get the line as a string
let thisline = getline(index)
" Remove backspaces at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Repeatedly apply backspaces until there are none left
while thisline =~ '.\%x08'
" Substitute any character followed by backspace with nothing
let thisline = substitute(thisline, '.\%x08', '', 'g')
endwhile
" Remove any backspaces left at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Write the line back
call setline(index, thisline)
endfor
endfunction
Use with:
" Whole file:
:ApplyBackspaces
" Whole file (explicitly requested):
:%ApplyBackspaces
" Visual range:
:'<,'>ApplyBackspaces
For more information, see:
:help command
:help command-range
:help function
:help function-range-example
:help substitute()
:help =~
:help \%x
Edit
Note that if you want to work on a single line, you could do something like this:
" Define the command to default to the current line rather than the whole file
command! -range ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Create a mapping so that pressing ,b in normal mode deals with the current line
nmap ,b :ApplyBackspaces<CR>
or you could just do:
nmap ,b :.ApplyBackspaces<CR>
Here's a much faster Awk filter that does the same:
#!/usr/bin/awk -f
function crushify(data) {
while (data ~ /[^^H]^H/) {
gsub(/[^^H]^H/, "", data)
}
print data
}
crushify($0)
Note that where ^^H appears, the first caret in ^^H is a caret (shift-6) and the second caret with H is entered (into vim) by typing CTRL-v CTRL-H
Here's a Bash-based filter you can use to process the whole file:
#!/bin/bash
while read LINE; do
while [[ "$LINE" =~ '^H' ]]; do
LINE="${LINE/[^^H]^H/}"
done
echo "$LINE"
done
Note that where ^H appears, it is entered into vim using CTRL-v CTRL-h, and the ^^H is entered as SHIFT-6 CTRL-v CTRL-h.

Resources