" Filetype plugin for editing CSV files. "{{{1 " Author: Christian Brabandt " Version: 0.31 " Script: http://www.vim.org/scripts/script.php?script_id=2830 " License: VIM License " Last Change: Thu, 15 Jan 2015 21:05:10 +0100 " Documentation: see :help ft-csv.txt " GetLatestVimScripts: 2830 30 :AutoInstall: csv.vim " " Some ideas are taken from the wiki http://vim.wikia.com/wiki/VimTip667 " though, implementation differs. " Plugin folklore "{{{1 if v:version < 700 || exists('b:did_ftplugin') finish endif let b:did_ftplugin = 1 let s:cpo_save = &cpo set cpo&vim fu! DetermineSID() let s:SID = matchstr(expand(''), '\zs\d\+\ze_DetermineSID$') endfu call s:DetermineSID() delf s:DetermineSID let s:csv_numeric_sort = v:version > 704 || v:version == 704 && has("patch341") if !s:csv_numeric_sort "{{{2 fu! CSVSortValues(i1, i2) "{{{3 return (a:i1+0) == (a:i2+0) ? 0 : (a:i1+0) > (a:i2+0) ? 1 : -1 endfu endif if !exists("##OptionSet") "{{{2 " No OptionSet autocommands fu! CSV_SetSplitOptions(window) "{{{3 if exists("s:local_stl") " local horizontal statusline for opt in items({'&nu': &l:nu, '&rnu': &l:rnu, '&fdc': &fdc}) if opt[1] != getwinvar(a:window, opt[0]) call setwinvar(a:window, opt[0], opt[1]) endif endfor " Check statusline (airline might change it) if getwinvar(a:window, '&l:stl') != s:local_stl call setwinvar(a:window, '&stl', s:local_stl) endif endif endfun endif " Function definitions: "{{{1 fu! CSVArrangeCol(first, last, bang, limit) range "{{{2 if &ft =~? 'csv' call ArrangeCol(a:first, a:last, a:bang, a:limit) else finish endif endfu " Script specific functions "{{{2 fu! Warn(mess) "{{{3 echohl WarningMsg echomsg "CSV: " . a:mess echohl Normal endfu fu! Init(startline, endline, ...) "{{{3 " if a:1 is set, keep the b:delimiter let keep = exists("a:1") && a:1 " Hilight Group for Columns if exists("g:csv_hiGroup") let s:hiGroup = g:csv_hiGroup else let s:hiGroup="WildMenu" endif if !exists("g:csv_hiHeader") let s:hiHeader = "Title" else let s:hiHeader = g:csv_hiHeader endif exe "hi link CSVHeaderLine" s:hiHeader " Determine default Delimiter if !keep if !exists("g:csv_delim") let b:delimiter=GetDelimiter(a:startline, a:endline) else let b:delimiter=g:csv_delim endif endif " Define custom commentstring if !exists("g:csv_comment") let b:csv_cmt = split(&cms, '%s') else let b:csv_cmt = split(g:csv_comment, '%s') endif if empty(b:delimiter) && !exists("b:csv_fixed_width") call Warn("No delimiter found. See :h csv-delimiter to set it manually!") " Use a sane default as delimiter: let b:delimiter = ',' endif let s:del='\%(' . b:delimiter . '\|$\)' let s:del_noend='\%(' . b:delimiter . '\)' " Pattern for matching a single column if !exists("g:csv_strict_columns") && !exists("g:csv_col") \ && !exists("b:csv_fixed_width") " - Allow double quotes as escaped quotes only insides double quotes " - Allow linebreaks only, if g:csv_nl isn't set (this is " only allowed in double quoted strings see RFC4180), though this " does not work with :WhatColumn and might mess up syntax " highlighting. " - optionally allow whitespace in front of the fields (to make it " work with :ArrangeCol (that is actually not RFC4180 valid)) " - Should work with most ugly solutions that are available let b:col='\%(\%(\%(' . (b:delimiter !~ '\s' ? '\s*' : '') . \ '"\%(' . (exists("g:csv_nl") ? '\_' : '' ) . \ '[^"]\|""\)*"\s*\)' . s:del . '\)\|\%(' . \ '[^' . b:delimiter . ']*' . s:del . '\)\)' let b:col_end='\%(\%(\%(' . (b:delimiter !~ '\s' ? '\s*' : '') . \ '"\%(' . (exists("g:csv_nl") ? '\_' : '' ) . \ '[^"]\|""\)*"\)' . s:del_noend . '\)\|\%(' . \ '[^' . b:delimiter . ']*' . s:del_noend . '\)\)' elseif !exists("g:csv_col") && exists("g:csv_strict_columns") " strict columns let b:col='\%([^' . b:delimiter . ']*' . s:del . '\)' let b:col_end='\%([^' . b:delimiter . ']*' . s:del_noend . '\)' elseif exists("b:csv_fixed_width") " Fixed width column let b:col='' " Check for sane default if b:csv_fixed_width =~? '[^0-9,]' call Warn("Please specify the list of character columns" . \ "like this: '1,3,5'. See also :h csv-fixedwidth") return endif let b:csv_fixed_width_cols=split(b:csv_fixed_width, ',') " Force evaluating as numbers call map(b:csv_fixed_width_cols, 'v:val+0') else " User given column definition let b:col = g:csv_col let b:col_noend = g:csv_col endif " set filetype specific options call LocalSettings('all') " define buffer-local commands call CommandDefinitions() " Check Header line " Defines which line is considered to be a header line call CheckHeaderLine() " CSV specific mappings call CSVMappings() " force reloading CSV Syntax Highlighting if exists("b:current_syntax") unlet b:current_syntax " Force reloading syntax file endif call DoAutoCommands() " enable CSV Menu call Menu(1) call DisableFolding() silent do Syntax unlet! b:csv_start b:csv_end " Remove configuration variables let b:undo_ftplugin .= "| unlet! b:delimiter b:col" \ . "| unlet! b:csv_fixed_width_cols b:csv_filter" \ . "| unlet! b:csv_fixed_width b:csv_list b:col_width" \ . "| unlet! b:csv_SplitWindow b:csv_headerline b:csv_cmt" \ . "| unlet! b:csv_thousands_sep b:csv_decimal_sep" \. " | unlet! b:browsefilter b:csv_cmt" \. " | unlet! b:csv_arrange_leftalign" " Delete all functions " disabled currently, because otherwise when switching ft " I think, all functions need to be read in again and this " costs time. endfu fu! LocalSettings(type) "{{{3 if a:type == 'all' " CSV local settings setl nostartofline tw=0 nowrap " undo when setting a new filetype let b:undo_ftplugin = "setlocal sol& tw< wrap<" " Set browsefilter let b:browsefilter="CSV Files (*.csv, *.dat)\t*.csv;*.dat\n". \ "All Files\t*.*\n" if has("conceal") setl cole=2 cocu=nc let b:undo_ftplugin .= '| setl cole< cocu< ' endif elseif a:type == 'fold' let s:fdt = &l:fdt let s:fcs = &l:fcs if a:type == 'fold' " Be sure to also fold away single screen lines setl fen fdm=expr setl fdl=0 fml=0 fdc=2 if !get(g:, 'csv_disable_fdt',0) let &l:foldtext=strlen(v:folddashes) . ' lines hidden' let &fcs=substitute(&fcs, 'fold:.,', '', '') if !exists("b:csv_did_foldsettings") let b:undo_ftplugin .= printf("|set fdt<|setl fcs=%s", escape(s:fcs, '\\| ')) endif endif if !exists("b:csv_did_foldsettings") let b:undo_ftplugin .= \ "| setl fen< fdm< fdl< fdc< fml< fde<" let b:csv_did_foldsettings = 1 let b:undo_ftplugin .= "| unlet! b:csv_did_foldsettings" endif endif endif endfu fu! DoAutoCommands() "{{{3 " Highlight column, on which the cursor is? if exists("g:csv_highlight_column") && g:csv_highlight_column =~? 'y' && \ !exists("#CSV_HI#CursorMoved") aug CSV_HI au! au CursorMoved HiColumn aug end " Set highlighting for column, on which the cursor is currently HiColumn elseif exists("#CSV_HI#CursorMoved") aug CSV_HI au! CursorMoved aug end aug! CSV_HI " Remove any existing highlighting HiColumn! endif " undo autocommand: let b:undo_ftplugin .= '| exe "sil! au! CSV_HI CursorMoved "' let b:undo_ftplugin .= '| exe "sil! aug! CSV_HI" |exe "sil! HiColumn!"' if has("gui_running") && !exists("#CSV_Menu#FileType") augroup CSV_Menu au! au FileType * call Menu(&ft=='csv') au BufEnter call Menu(1) " enable au BufLeave call Menu(0) " disable au BufNewFile,BufNew * call Menu(0) augroup END endif endfu fu! GetPat(colnr, maxcolnr, pat, allowmore) "{{{3 " if a:allowmmore, allows more to match after the pattern if a:colnr > 1 && a:colnr < a:maxcolnr if !exists("b:csv_fixed_width_cols") return '^' . GetColPat(a:colnr-1,0) . '\%([^' . \ b:delimiter . ']\{-}\)\?\zs' . a:pat . '\ze' . \ (a:allowmore ? ('\%([^' . b:delimiter .']\{-}\)\?' . \ b:delimiter . GetColPat(a:maxcolnr - a:colnr, 0). '$') : '') else return '\%' . b:csv_fixed_width_cols[(a:colnr - 1)] . 'c\zs' \ . a:pat . '.\{-}\ze\%' \ . (b:csv_fixed_width_cols[a:colnr]) . 'c\ze' endif elseif a:colnr == a:maxcolnr if !exists("b:csv_fixed_width_cols") return '^' . GetColPat(a:colnr - 1,0) . \ '\zs' . a:pat . '\ze$' else return '\%' . b:csv_fixed_width_cols[-1] . \ 'c\zs' . a:pat . '\ze$' endif else " colnr = 1 if !exists("b:csv_fixed_width_cols") return '^' . '\%([^' . b:delimiter . ']\{-}\)\?\zs' . a:pat . \ (a:allowmore ? ('\ze\%([^' . b:delimiter . ']*\)\?' . b:delimiter . \ GetColPat(a:maxcolnr -1 , 0) . '$') : '') else return a:pat . '\ze.\{-}\%' . b:csv_fixed_width_cols[1] . 'c' endif endif return '' endfu fu! SearchColumn(arg) "{{{3 try let arglist=split(a:arg) if len(arglist) == 1 let colnr=WColumn() let pat=substitute(arglist[0], '^\(.\)\(.*\)\1$', '\2', '') if pat == arglist[0] throw "E684" endif else " Determine whether the first word in the argument is a number " (of the column to search). let colnr = substitute( a:arg, '^\s*\(\d\+\)\s.*', '\1', '' ) " If it is _not_ a number, if colnr == a:arg " treat the whole argument as the pattern. let pat = substitute(a:arg, \ '^\s*\(\S\)\(.*\)\1\s*$', '\2', '' ) if pat == a:arg throw "E684" endif let colnr = WColumn() else " if the first word tells us the number of the column, " treat the rest of the argument as the pattern. let pat = substitute(a:arg, \ '^\s*\d\+\s*\(\S\)\(.*\)\1\s*$', '\2', '' ) if pat == a:arg throw "E684" endif endif endif "catch /^Vim\%((\a\+)\)\=:E684/ catch /E684/ " catch error index out of bounds call Warn("Error! Usage :SearchInColumn [] /pattern/") return 1 endtry let maxcolnr = MaxColumns() if colnr > maxcolnr call Warn("There exists no column " . colnr) return 1 endif let @/ = GetPat(colnr, maxcolnr, '\%('.pat. '\)', 1) try " force redraw, so that the search pattern isn't shown exe "norm! n\" catch /^Vim\%((\a\+)\)\=:E486/ " Pattern not found echohl Error echomsg "E486: Pattern not found in column " . colnr . ": " . pat if &vbs > 0 echomsg substitute(v:exception, '^[^:]*:', '','') endif echohl Normal endtry endfu fu! DeleteColumn(arg) "{{{3 let _wsv = winsaveview() if a:arg =~ '^[/]' let i = 0 let pat = a:arg[1:] call cursor(1,1) while search(pat, 'cW') " Delete matching column sil call DelColumn('') let i+=1 endw else let i = 1 sil call DelColumn(a:arg) endif if i > 1 call Warn(printf("%d columns deleted", i)) else call Warn("1 column deleted") endif call winrestview(_wsv) endfu fu! DelColumn(colnr) "{{{3 let maxcolnr = MaxColumns() let _p = getpos('.') if empty(a:colnr) let colnr=WColumn() else let colnr=a:colnr endif if colnr > maxcolnr call Warn("There exists no column " . colnr) return endif if colnr != '1' if !exists("b:csv_fixed_width_cols") let pat= '^' . GetColPat(colnr-1,1) . b:col else let pat= GetColPat(colnr,0) endif else " distinction between csv and fixed width does not matter here let pat= '^' . GetColPat(colnr,0) endif if &ro let ro = 1 setl noro else let ro = 0 endif exe ':%s/' . escape(pat, '/') . '//' call setpos('.', _p) if ro setl ro endif endfu fu! HiCol(colnr, bang) "{{{3 if a:colnr > MaxColumns() && !a:bang call Warn("There exists no column " . a:colnr) return endif if !a:bang if empty(a:colnr) let colnr=WColumn() else let colnr=a:colnr endif if colnr==1 let pat='^'. GetColPat(colnr,0) elseif !exists("b:csv_fixed_width_cols") let pat='^'. GetColPat(colnr-1,1) . b:col else let pat=GetColPat(colnr,0) endif endif if exists("*matchadd") if exists("s:matchid") " ignore errors, that come from already deleted matches sil! call matchdelete(s:matchid) endif " Additionally, filter all matches, that could have been used earlier let matchlist=getmatches() call filter(matchlist, 'v:val["group"] !~ s:hiGroup') " remove matches, that come from matchaddpos() " setmatches() can't handle them. call filter(matchlist, 'has_key(v:val, "pattern")') call setmatches(matchlist) if a:bang return endif let s:matchid=matchadd(s:hiGroup, pat, 0) elseif !a:bang exe ":2match " . s:hiGroup . ' /' . pat . '/' endif endfu fu! GetDelimiter(first, last) "{{{3 if !exists("b:csv_fixed_width_cols") let _cur = getpos('.') let _s = @/ let Delim= {0: ';', 1: ',', 2: '|', 3: ' ', 4: '\^'} let temp = {} " :silent :s does not work with lazyredraw let _lz = &lz set nolz for i in values(Delim) redir => temp[i] exe "silent! ". a:first. ",". a:last. "s/" . i . "/&/nge" redir END endfor let &lz = _lz let Delim = map(temp, 'matchstr(substitute(v:val, "\n", "", ""), "^\\d\\+")') let Delim = filter(temp, 'v:val=~''\d''') let max = max(values(temp)) let result=[] call setpos('.', _cur) let @/ = _s for [key, value] in items(Delim) if value == max return key endif endfor return '' else " There is no delimiter for fixedwidth files return '' endif endfu fu! WColumn(...) "{{{3 " Return on which column the cursor is let _cur = getpos('.') if !exists("b:csv_fixed_width_cols") if line('.') > 1 && mode('') != 'n' " in insert mode, get line from above, just in case the current " line is empty let line = getline(line('.')-1) else let line=getline('.') endif " move cursor to end of field "call search(b:col, 'ec', line('.')) call search(b:col, 'ec') let end=col('.')-1 let fields=(split(line[0:end],b:col.'\zs')) let ret=len(fields) if exists("a:1") && a:1 > 0 " bang attribute: Try to get the column name let head = split(getline(1),b:col.'\zs') " remove preceeding whitespace if len(head) < ret call Warn("Header has no field ". ret) else let ret = substitute(head[ret-1], '^\s\+', '', '') " remove delimiter let ret = substitute(ret, b:delimiter. '$', '', '') endif endif else let temp=getpos('.')[2] let j=1 let ret = 1 for i in sort(b:csv_fixed_width_cols, s:csv_numeric_sort ? 'n' : 's:CSVSortValues') if temp >= i let ret = j endif let j += 1 endfor endif call setpos('.',_cur) return ret endfu fu! MaxColumns(...) "{{{3 let this_col = exists("a:1") "return maximum number of columns in first 10 lines if !exists("b:csv_fixed_width_cols") let i = this_col ? a:1 : 1 while 1 let l = getline(i, (this_col ? i : i+10)) " Filter comments out let pat = '^\s*\V'. escape(b:csv_cmt[0], '\\') call filter(l, 'v:val !~ pat') if !empty(l) || this_col break else let i+=10 endif endw if empty(l) throw 'csv:no_col' endif let fields=[] let result=0 for item in l let temp=len(split(item, b:col.'\zs')) let result=(temp>result ? temp : result) endfor return result else return len(b:csv_fixed_width_cols) endif endfu fu! ColWidth(colnr, ...) "{{{3 " if a:1 is given, specifies the row, for which to calculate the width " " Return the width of a column " Internal function let width=20 "Fallback (wild guess) let tlist=[] if !exists("b:csv_fixed_width_cols") if !exists("b:csv_list") " only check first 10000 lines, to be faster let last = line('$') if exists("a:1") let last = a:1 endif if !get(b:, 'csv_arrange_use_all_rows', 0) if last > 10000 let last = 10000 call Warn('File too large, only checking the first 10000 rows for the width') endif endif let b:csv_list=getline(1,last) let pat = '^\s*\V'. escape(b:csv_cmt[0], '\\') call filter(b:csv_list, 'v:val !~ pat') call filter(b:csv_list, '!empty(v:val)') call map(b:csv_list, 'split(v:val, b:col.''\zs'')') endif try for item in b:csv_list call add(tlist, get(item, a:colnr-1, '')) endfor " do not strip leading whitespace call map(tlist, 'substitute(v:val, ".", "x", "g")') call map(tlist, 'strlen(v:val)') return max(tlist) catch throw "ColWidth-error" return width endtry else let cols = len(b:csv_fixed_width_cols) if a:colnr == cols return strlen(substitute(getline('$'), '.', 'x', 'g')) - \ b:csv_fixed_width_cols[cols-1] + 1 elseif a:colnr < cols && a:colnr > 0 return b:csv_fixed_width_cols[a:colnr] - \ b:csv_fixed_width_cols[(a:colnr - 1)] else throw "ColWidth-error" return 0 endif endif endfu fu! ArrangeCol(first, last, bang, limit, ...) range "{{{3 " a:1, optional width parameter of line from which to take the width " " explicitly give the range as argument to the function if exists("b:csv_fixed_width_cols") " Nothing to do call Warn("ArrangeColumn does not work with fixed width column!") return endif let cur=winsaveview() if a:bang || (exists("a:1") && !empty(a:1)) if a:bang && exists("b:col_width") " Unarrange, so that if csv_arrange_align has changed " it will be adjusted automaticaly call PrepUnArrangeCol(a:first, a:last) endif " Force recalculating the Column width unlet! b:csv_list b:col_width elseif a:limit > -1 && a:limit < getfsize(fnamemodify(bufname(''), ':p')) return endif if !exists("b:col_width") " Force recalculation of Column width let row = exists("a:1") ? a:1 : '' call CalculateColumnWidth(row) endif if &ro " Just in case, to prevent the Warning " Warning: W10: Changing read-only file let ro = 1 setl noro else let ro = 0 endif let s:count = 0 let _stl = &stl let s:max = (a:last - a:first + 1) * len(b:col_width) let s:temp = 0 try exe "sil". a:first . ',' . a:last .'s/' . (b:col) . \ '/\=Columnize(submatch(0))/' . (&gd ? '' : 'g') finally " Clean up variables, that were only needed for Columnize() function unlet! s:columnize_count s:max_cols s:prev_line s:max s:count s:temp s:val if ro setl ro unlet ro endif let &stl = _stl call winrestview(cur) endtry endfu fu! ProgressBar(cnt, max) "{{{3 if get(g:, 'csv_no_progress', 0) return endif let width = 40 " max width of progressbar if width > &columns let width = &columns endif let s:val = a:cnt * width / a:max if (s:val > s:temp || a:cnt==1) let &stl='%#DiffAdd#['.repeat('=', s:val).'>'. repeat(' ', width-s:val).']'. \ (width < &columns ? ' '.100*s:val/width. '%%' : '') redrawstatus let s:temp = s:val endif endfu fu! PrepUnArrangeCol(first, last) "{{{3 " Because of the way, Vim works with " a:firstline and a:lastline parameter, " explicitly give the range as argument to the function if exists("b:csv_fixed_width_cols") " Nothing to do call Warn("UnArrangeColumn does not work with fixed width column!") return endif let cur=winsaveview() if &ro " Just in case, to prevent the Warning " Warning: W10: Changing read-only file setl noro endif exe a:first . ',' . a:last .'s/' . (b:col) . \ '/\=UnArrangeCol(submatch(0))/' . (&gd ? '' : 'g') " Clean up variables, that were only needed for Columnize() function call winrestview(cur) endfu fu! UnArrangeCol(match) "{{{3 " Strip leading white space, also trims empty records: return substitute(a:match, '\%(^\s\+\)\|\%(\s\+\ze'.b:delimiter. '\?$\)', '', 'g') endfu fu! CalculateColumnWidth(row) "{{{3 " Internal function, not called from external, " does not work with fixed width columns let b:col_width=[] try let s:max_cols=MaxColumns(line('.')) for i in range(1,s:max_cols) if empty(a:row) call add(b:col_width, ColWidth(i)) else call add(b:col_width, ColWidth(i,a:row)) endif endfor catch /csv:no_col/ call Warn("Error: getting Column numbers, aborting!") catch /ColWidth/ call Warn("Error: getting Column Width, using default!") endtry " delete buffer content in variable b:csv_list, " this was only necessary for calculating the max width unlet! b:csv_list s:columnize_count s:decimal_column endfu fu! Columnize(field) "{{{3 " Internal function, not called from external, " does not work with fixed width columns if !exists("s:columnize_count") let s:columnize_count = 0 endif if !exists("s:max_cols") let s:max_cols = len(b:col_width) endif if exists("s:prev_line") && s:prev_line != line('.') let s:columnize_count = 0 endif let s:count+=1 let s:prev_line = line('.') " convert zero based indexed list to 1 based indexed list, " Default: 20 width, in case that column width isn't defined " Careful: Keep this fast! Using " let width=get(b:col_width,WColumn()-1,20) " is too slow, so we are using: let colnr = s:columnize_count % s:max_cols let width = get(b:col_width, colnr, 20) let align = 'r' if exists('b:csv_arrange_align') let align_list=split(get(b:, 'csv_arrange_align', " "), '\zs') try let align = align_list[colnr] catch let align = 'r' endtry endif if ((align isnot? 'r' && align isnot? 'l' && \ align isnot? 'c' && align isnot? '.') || get(b:, 'csv_arrange_leftalign', 0)) let align = 'r' endif call ProgressBar(s:count,s:max) let s:columnize_count += 1 let has_delimiter = (a:field[-1:] is? b:delimiter) if align is? 'l' " left-align content return printf("%-*S%s", width+1 , \ (has_delimiter ? a:field[:-2] : a:field), \ (has_delimiter ? b:delimiter : ' ')) elseif align is? 'c' " center the column let t = width - len(split(a:field, '\zs')) let leftwidth = t/2 " uneven width, add one let rightwidth = (t%2 ? leftwidth+1 : leftwidth) let field = (has_delimiter ? a:field[:-2] : a:field). repeat(' ', rightwidth) return printf("%*S%s", width , field, (has_delimiter ? b:delimiter : ' ')) elseif align is? '.' if !exists("s:decimal_column") let s:decimal_column = {} endif if get(s:decimal_column, colnr, 0) == 0 call CheckHeaderLine() call NumberFormat() let data = CopyCol('', colnr+1, '')[s:csv_fold_headerline : -1] let pat1 = escape(s:nr_format[1], '.').'\zs[^'.s:nr_format[1].']*\ze'. \ (has_delimiter ? b:delimiter : '').'$' let pat2 = '\d\+\ze\%(\%('.escape(s:nr_format[1], '.'). '\d\+\)\|'. \ (has_delimiter ? b:delimiter : '').'$\)' let data1 = map(copy(data), 'matchstr(v:val, pat1)') let data2 = map(data, 'matchstr(v:val, pat2)') " strlen should be okay for decimals... let data1 = map(data1, 'strlen(v:val)') let data2 = map(data2, 'strlen(v:val)') let dec = max(data1) let scal = max(data2) if dec + scal + 1 + (has_delimiter ? 1 : 0) > width let width = dec + scal + 1 + (has_delimiter ? 1 :0) let b:col_width[colnr] = width endif let s:decimal_column[colnr] = dec else let dec = get(s:decimal_column, colnr) endif let field = (has_delimiter ? a:field[:-2] : a:field) let fmt = printf("%%%d.%df", width+1, dec) try if s:nr_format[1] isnot '.' let field = substitute(field, s:nr_format[1], '.', 'g') let field = substitute(field, s:nr_format[0], '', 'g') endif if field =~? '\h' " text in the column, can't be converted to float throw "no decimal" endif let result = printf(fmt, str2float(field)). (has_delimiter ? b:delimiter : ' ') catch let result = printf("%*S", width+2, a:field) endtry return result else " right align return printf("%*S", width+1 , a:field) endif endfun fu! GetColPat(colnr, zs_flag) "{{{3 " Return Pattern for given column if a:colnr > 1 if !exists("b:csv_fixed_width_cols") let pat=b:col . '\{' . (a:colnr) . '\}' else if a:colnr >= len(b:csv_fixed_width_cols) " Get last column let pat='\%' . b:csv_fixed_width_cols[-1] . 'v.*' else let pat='\%' . b:csv_fixed_width_cols[(a:colnr - 1)] . \ 'c.\{-}\%' . b:csv_fixed_width_cols[a:colnr] . 'v' endif endif elseif !exists("b:csv_fixed_width_cols") let pat=b:col else let pat='\%' . b:csv_fixed_width_cols[0] . 'v.\{-}' . \ (len(b:csv_fixed_width_cols) > 1 ? \ '\%' . b:csv_fixed_width_cols[1] . 'v' : \ '') endif return pat . (a:zs_flag ? '\zs' : '') endfu fu! SetupAutoCmd(window,bufnr) "{{{3 " Setup QuitPre autocommand to quit cleanly aug CSV_QuitPre au! exe "au QuitPre * call CSV_CloseBuffer(".winbufnr(a:window).")" if !exists("##OptionSet") exe "au CursorHold call CSV_SetSplitOptions(".a:window.")" else exe "au OptionSet foldcolumn,number,relativenumber call CSV_SetOption(".a:bufnr. \ ", ".bufnr('%').", expand(''), v:option_new)" endif aug END endfu fu! CSV_SetOption(csvfile, header, option, value) "{{{3 " only trigger if the option is called in the correct buffer if getbufvar(a:csvfile, 'csv_SplitWindow') && bufnr('') == a:csvfile call setbufvar(a:header, '&'.a:option, a:value) endif endfu fu! SplitHeaderLine(lines, bang, hor) "{{{3 if exists("b:csv_fixed_width_cols") call Warn("Header does not work with fixed width column!") return endif " Check that there exists a header line call CheckHeaderLine() if !a:bang " A Split Header Window already exists, " first close the already existing Window if exists("b:csv_SplitWindow") call SplitHeaderLine(a:lines, 1, a:hor) endif " Split Window let _stl = &l:stl let _sbo = &sbo let a = [] let b=b:col let bufnr = bufnr('.') if a:hor setl scrollopt=hor scrollbind cursorbind let _fdc = &l:fdc let lines = empty(a:lines) ? s:csv_fold_headerline : a:lines let a = getline(1,lines) " Does it make sense to use the preview window? " sil! pedit % above sp +enew call setline(1, a) " Needed for syntax highlighting "let b:col=b "setl syntax=csv sil! doautocmd FileType csv noa 1 sil! sign unplace * exe "resize" . lines setl scrollopt=hor winfixheight nowrap cursorbind let &l:stl="%#Normal#".repeat(' ',winwidth(0)) let s:local_stl = &l:stl " set the foldcolumn to the same of the other window let &l:fdc = _fdc else setl scrollopt=ver scrollbind cursorbind noa 0 if a:lines[-1:] is? '!' let a=CopyCol('',a:lines,'') else let a=CopyCol('',1, a:lines-1) endif " Does it make sense to use the preview window? "vert sil! pedit |wincmd w | enew! above vsp +enew call append(0, a) $d _ let b:col = b sil! doautocmd FileType csv " remove leading delimiter exe "sil :%s/^". b:delimiter. "//e" " remove trailing delimiter exe "sil :%s/". b:delimiter. "\s*$//e" syn clear noa 0 let b:csv_SplitWindow = winnr() sil :call ArrangeCol(1,line('$'), 1, -1) sil! sign unplace * exe "vert res" . len(split(getline(1), '\zs')) call matchadd("CSVHeaderLine", b:col) setl scrollopt=ver winfixwidth cursorbind nonu nornu fdc=0 endif call SetupAutoCmd(winnr(),bufnr) " disable airline let w:airline_disabled = 1 let win = winnr() setl scrollbind buftype=nowrite bufhidden=wipe noswapfile nobuflisted noa wincmd p let b:csv_SplitWindow = win aug CSV_Preview au! au BufWinLeave call SplitHeaderLine(0, 1, 0) aug END else " Close split window if !exists("b:csv_SplitWindow") return endif try let winnr = winnr() if winnr == b:csv_SplitWindow || winbufnr(b:csv_SplitWindow) == bufnr('') " window already closed return endif exe b:csv_SplitWindow . "wincmd w" if exists("_stl") let &l:stl = _stl endif if exists("_sbo") let &sbo = _sbo endif setl noscrollbind nocursorbind call CSV_CloseBuffer(bufnr('%')) catch /^Vim\%((\a\+)\)\=:E444/ " cannot close last window catch /^Vim\%((\a\+)\)\=:E517/ " buffer already wiped " no-op finally unlet! b:csv_SplitWindow aug CSV_Preview au! aug END aug! CSV_Preview endtry endif endfu fu! SplitHeaderToggle(hor) "{{{3 if !exists("b:csv_SplitWindow") :call SplitHeaderLine(1,0,a:hor) else :call SplitHeaderLine(1,1,a:hor) endif endfu " TODO: from here on add logic for fixed-width csv files! fu! MoveCol(forward, line, ...) "{{{3 " Move cursor position upwards/downwards left/right " a:1 is there to have some mappings move in the same " direction but still stop at a different position " see :h csv-mapping-H let colnr=WColumn() let maxcol=MaxColumns() let cpos=getpos('.')[2] if !exists("b:csv_fixed_width_cols") call search(b:col, 'bc', line('.')) endif let spos=getpos('.')[2] " Check for valid column " a:forward == 1 : search next col " a:forward == -1: search prev col " a:forward == 0 : stay in col if colnr - v:count1 >= 1 && a:forward == -1 let colnr -= v:count1 elseif colnr - v:count1 < 1 && a:forward == -1 let colnr = 0 elseif colnr + v:count1 <= maxcol && a:forward == 1 let colnr += v:count1 elseif colnr + v:count1 > maxcol && a:forward == 1 let colnr = maxcol + 1 endif let line=a:line if line < 1 let line=1 elseif line > line('$') let line=line('$') endif if foldclosed(line) != -1 let line = line > line('.') ? foldclosedend(line) + 1 : foldclosed(line) endif " Generate search pattern if colnr == 1 let pat = '^' . GetColPat(colnr-1,0) "let pat = pat . '\%' . line . 'l' elseif (colnr == 0) || (colnr == maxcol + 1) if !exists("b:csv_fixed_width_cols") let pat=b:col else if a:forward > 0 " Move forwards let pat=GetColPat(1, 0) else " Move backwards let pat=GetColPat(maxcol, 0) endif endif else if !exists("b:csv_fixed_width_cols") let pat='^'. GetColPat(colnr-1,1) . b:col else let pat=GetColPat(colnr,0) endif "let pat = pat . '\%' . line . 'l' endif " Search " move left/right if a:forward > 0 call search(pat, 'W') elseif a:forward < 0 if colnr > 0 || cpos == spos call search('.\ze'.pat, 'bWe') let stime=localtime() while getpos('.')[2] == cpos && Timeout(stime) " make sure loop terminates " cursor didn't move, move cursor one cell to the left norm! h if colnr > 0 call MoveCol(-1, line('.')) else norm! 0 endif endw if (exists("a:1") && a:1) " H also stops at the beginning of the content " of a field. let epos = getpos('.') if getline('.')[col('.')-1] == ' ' call search('\S', 'W', line('.')) if getpos('.')[2] > spos call setpos('.', epos) endif endif endif else norm! 0 endif " Moving upwards/downwards elseif line >= line('.') call search(pat . '\%' . line . 'l', '', line) " Move to the correct screen column " This is a best effort approach, we might still " leave the column (if the next column is shorter) if !exists("b:csv_fixed_width_cols") let a = getpos('.') let a[2]+= cpos-spos else let a = getpos('.') let a[2] = cpos endif call setpos('.', a) elseif line < line('.') call search(pat . '\%' . line . 'l', 'b', line) " Move to the correct screen column if !exists("b:csv_fixed_width_cols") let a = getpos('.') let a[2]+= cpos-spos else let a = getpos('.') let a[2] = cpos endif call setpos('.', a) endif endfun fu! SortComplete(A,L,P) "{{{3 return join(range(1,MaxColumns()),"\n") endfun fu! Sort(bang, line1, line2, colnr) range "{{{3 " :Sort command let wsv = winsaveview() let flag = matchstr(a:colnr, '[nixo]') call CheckHeaderLine() let line1 = a:line1 let line2 = a:line2 if line1 <= s:csv_fold_headerline let line1 += s:csv_fold_headerline endif if line2 <= s:csv_fold_headerline let line2 += s:csv_fold_headerline endif let col = (empty(a:colnr) || a:colnr !~? '\d\+[nixo]\?') ? WColumn() : a:colnr+0 if col != 1 if !exists("b:csv_fixed_width_cols") let pat= '^' . GetColPat(col-1,1) . b:col else let pat= GetColPat(col,0) endif else let pat= '^' . GetColPat(col,0) endif exe line1. ','. line2. "sort". (a:bang ? '!' : '') . \' r'. flag. ' /' . pat . '/' call winrestview(wsv) endfun fu! CopyCol(reg, col, cnt) "{{{3 " Return Specified Column into register reg let col = a:col == "0" ? WColumn() : a:col+0 let mcol = MaxColumns() if col == '$' || col > mcol let col = mcol endif " The number of columns to return " by default (value of zero, will only return that specific column) let cnt_cols = col - 1 if !empty(a:cnt) && a:cnt > 0 && col + a:cnt <= mcol let cnt_cols = col + a:cnt - 1 endif let a = [] " Don't get lines, that are currently filtered away if !exists("b:csv_filter") || empty(b:csv_filter) let a=getline(1, '$') else for line in range(1, line('$')) if foldlevel(line) continue else call add(a, getline(line)) endif endfor endif " Filter comments out let pat = '^\s*\V'. escape(b:csv_cmt[0], '\\') call filter(a, 'v:val !~ pat') if !exists("b:csv_fixed_width_cols") call map(a, 'split(v:val, ''^'' . b:col . ''\zs'')[col-1:cnt_cols]') else call map(a, 'matchstr(v:val, GetColPat(col, 0)).*GetColPat(col+cnt_cols, 0)') endif if type(a[0]) == type([]) call map(a, 'join(v:val, "")') endif if a:reg =~ '[-"0-9a-zA-Z*+]' "exe ':let @' . a:reg . ' = "' . join(a, "\n") . '"' " set the register to blockwise mode call setreg(a:reg, join(a, "\n"), 'b') else return a endif endfu fu! MoveColumn(start, stop, ...) range "{{{3 " Move column behind dest " Explicitly give the range as argument, " cause otherwise, Vim would move the cursor let wsv = winsaveview() let col = WColumn() let max = MaxColumns() " If no argument is given, move current column after last column let source=(exists("a:1") && a:1 > 0 && a:1 <= max ? a:1 : col) let dest =(exists("a:2") && a:2 > 0 && a:2 <= max ? a:2 : max) " translate 1 based columns into zero based list index let source -= 1 let dest -= 1 if source >= dest call Warn("Destination column before source column, aborting!") return endif " Swap line by line, instead of reading the whole range into memory for i in range(a:start, a:stop) let content = getline(i) if content =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') " skip comments continue endif if !exists("b:csv_fixed_width_cols") let fields=split(content, b:col . '\zs') " Add delimiter to destination column, in case there was none, " remove delimiter from source, in case destination did not have one if matchstr(fields[dest], '.$') !~? b:delimiter let fields[dest] = fields[dest] . b:delimiter if matchstr(fields[source], '.$') =~? b:delimiter let fields[source] = substitute(fields[source], \ '^\(.*\).$', '\1', '') endif endif else let fields=[] " this is very inefficient! for j in range(1, max, 1) call add(fields, matchstr(content, GetColPat(j,0))) endfor endif let fields= (source == 0 ? [] : fields[0 : (source-1)]) \ + fields[ (source+1) : dest ] \ + [ fields[source] ] + fields[(dest+1):] call setline(i, join(fields, '')) endfor call winrestview(wsv) endfu fu! AddColumn(start, stop, ...) range "{{{3 " Add new empty column " Explicitly give the range as argument, " cause otherwise, Vim would move the cursor if exists("b:csv_fixed_width_cols") call Warn("Adding Columns only works for delimited files") return endif let wsv = winsaveview() let col = WColumn() let max = MaxColumns() " If no argument is given, add column after current column if exists("a:1") if a:1 == '$' || a:1 >= max let pos = max elseif a:1 < 0 let pos = col else let pos = a:1 endif else let pos = col endif let cnt=(exists("a:2") && a:2 > 0 ? a:2 : 1) " translate 1 based columns into zero based list index "let pos -= 1 let col -= 1 if pos == 0 let pat = '^' elseif pos == max-1 let pat = '$' else let pat = GetColPat(pos,1) endif if pat != '$' || (pat == '$' && getline(a:stop)[-1:] == b:delimiter) let subst = repeat(' '. b:delimiter, cnt) else let subst = repeat(b:delimiter. ' ', cnt) endif " if the data contains comments, substitute one line after another " skipping comment lines (we could do it with a single :s statement, " but that would fail for the first and last column. let commentpat = '\%(\%>'.(a:start-1).'l\V'. \ escape(b:csv_cmt[0], '\\').'\m\)'. '\&\%(\%<'. \ (a:stop+1). 'l\V'. escape(b:csv_cmt[0], '\\'). '\m\)' if search(commentpat) for i in range(a:start, a:stop) let content = getline(i) if content =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') " skip comments continue endif exe printf("sil %ds/%s/%s/e", i, pat, subst) endfor else " comments should by default be skipped (pattern shouldn't match) exe printf("sil %d,%ds/%s/%s/e", a:start, a:stop, pat, subst) endif call winrestview(wsv) endfu fu! SumColumn(list) "{{{3 " Sum a list of values, but only consider the digits within each value " parses the digits according to the given format (if none has been " specified, assume POSIX format (without thousand separator) If Vim has " does not support floats, simply sum up only the integer part if empty(a:list) return 0 else let sum = has("float") ? 0.0 : 0 for item in a:list if empty(item) continue endif let nr = matchstr(item, '-\?\d\(.*\d\)\?$') let format1 = '^-\?\d\+\zs\V' . s:nr_format[0] . '\m\ze\d' let format2 = '\d\+\zs\V' . s:nr_format[1] . '\m\ze\d' try let nr = substitute(nr, format1, '', '') if has("float") && s:nr_format[1] != '.' let nr = substitute(nr, format2, '.', '') endif catch let nr = 0 endtry let sum += (has("float") ? str2float(nr) : (nr + 0)) endfor if has("float") if float2nr(sum) == sum return float2nr(sum) else return printf("%.2f", sum) endif endif return sum endif endfu fu! MaxColumn(list) "{{{3 " Sum a list of values, but only consider the digits within each value " parses the digits according to the given format (if none has been " specified, assume POSIX format (without thousand separator) If Vim has " does not support floats, simply sum up only the integer part if empty(a:list) return 0 else let result = [] for item in a:list if empty(item) continue endif let nr = matchstr(item, '-\?\d\(.*\d\)\?$') let format1 = '^-\?\d\+\zs\V' . s:nr_format[0] . '\m\ze\d' let format2 = '\d\+\zs\V' . s:nr_format[1] . '\m\ze\d' try let nr = substitute(nr, format1, '', '') if has("float") && s:nr_format[1] != '.' let nr = substitute(nr, format2, '.', '') endif catch let nr = 0 endtry call add(result, has("float") ? str2float(nr) : nr+0) endfor let result = sort(result, s:csv_numeric_sort ? 'n' : 's:CSVSortValues') let ind = len(result) > 9 ? 9 : len(result) if has_key(get(s:, 'additional', {}), 'distinct') && s:additional['distinct'] if exists("*uniq") let result=uniq(result) else let l = {} for item in result let l[item] = get(l, 'item', 0) endfor let result = keys(l) endif endif return s:additional.ismax ? reverse(result)[:ind] : result[:ind] endif endfu fu! CountColumn(list) "{{{3 if empty(a:list) return 0 elseif has_key(get(s:, 'additional', {}), 'distinct') && s:additional['distinct'] if exists("*uniq") return len(uniq(sort(a:list))) else let l = {} for item in a:list let l[item] = get(l, 'item', 0) + 1 endfor return len(keys(l)) endif else return len(a:list) endif endfu fu! DoForEachColumn(start, stop, bang) range "{{{3 " Do something for each column, " e.g. generate SQL-Statements, convert to HTML, " something like this " TODO: Define the function " needs a csv_pre_convert variable " csv_post_convert variable " csv_convert variable " result contains converted buffer content let result = [] if !exists("g:csv_convert") call Warn("You need to define how to convert your data using" . \ "the g:csv_convert variable, see :h csv-convert") return endif if exists("g:csv_pre_convert") && !empty(g:csv_pre_convert) call add(result, g:csv_pre_convert) endif for item in range(a:start, a:stop, 1) if foldlevel(line) " Filter out folded lines (from dynamic filter) continue endif let t = g:csv_convert let line = getline(item) if line =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') " Filter comments out call add(result, line) continue endif let context = split(g:csv_convert, '%s') let columns = len(context) if columns > MaxColumns() let columns = MaxColumns() elseif columns == 1 call Warn("No Columns defined in your g:csv_convert variable, Aborting") return endif if !exists("b:csv_fixed_width_cols") let fields=split(line, b:col . '\zs') if a:bang call map(fields, 'substitute(v:val, b:delimiter . \ ''\?$'' , "", "")') endif else let fields=[] for j in range(1, columns, 1) call add(fields, matchstr(line, GetColPat(j,0))) endfor endif for j in range(1, columns, 1) let t=substitute(t, '%s', fields[j-1], '') endfor call add(result, t) endfor if exists("g:csv_post_convert") && !empty(g:csv_post_convert) call add(result, g:csv_post_convert) endif new call append('$', result) 1d _ endfun fu! PrepareDoForEachColumn(start, stop, bang) range"{{{3 let pre = exists("g:csv_pre_convert") ? g:csv_pre_convert : '' let g:csv_pre_convert=input('Pre convert text: ', pre) let post = exists("g:csv_post_convert") ? g:csv_post_convert : '' let g:csv_post_convert=input('Post convert text: ', post) let convert = exists("g:csv_convert") ? g:csv_convert : '' let g:csv_convert=input("Converted text, use %s for column input:\n", convert) call DoForEachColumn(a:start, a:stop, a:bang) endfun fu! EscapeValue(val) "{{{3 return '\V' . escape(a:val, '\') endfu fu! FoldValue(lnum, filter) "{{{3 call CheckHeaderLine() if (a:lnum == s:csv_fold_headerline) " Don't fold away the header line return 0 endif let result = 0 for item in values(a:filter) " always fold comments away let content = getline(a:lnum) if content =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') return 1 elseif eval('content' . (item.match ? '!~' : '=~') . 'item.pat') let result += 1 endif endfor return (result > 0) endfu fu! PrepareFolding(add, match) "{{{3 if !has("folding") return endif " Move folded-parts away? if exists("g:csv_move_folds") let s:csv_move_folds = g:csv_move_folds else let s:csv_move_folds = 0 endif if !exists("b:csv_filter") let b:csv_filter = {} endif if !exists("s:filter_count") || s:filter_count < 1 let s:filter_count = 0 endif let cpos = winsaveview() if !a:add " remove last added item from filter if !empty(b:csv_filter) call RemoveLastItem(s:filter_count) let s:filter_count -= 1 if empty(b:csv_filter) call DisableFolding() return endif else " Disable folding, if no pattern available call DisableFolding() return endif else let col = WColumn() let max = MaxColumns() let a = GetColumn(line('.'), col) let a = ProcessFieldValue(a) let pat = '\%(^\|'.b:delimiter. '\)\@<='.EscapeValue(a). \ '\m\ze\%('.b:delimiter.'\|$\)' " Make a column pattern let b= '\%(' . \ (exists("b:csv_fixed_width") ? '.*' : '') . \ GetPat(col, max, pat, 0) . '\)' let s:filter_count += 1 let b:csv_filter[s:filter_count] = { 'pat': b, 'id': s:filter_count, \ 'col': col, 'orig': a, 'match': a:match} endif " Put the pattern into the search register, so they will also " be highlighted " let @/ = '' " for val in sort(values(b:csv_filter), 'SortFilter') " let @/ .= val.pat . (val.id == s:filter_count ? '' : '\&') " endfor " Fold settings: call LocalSettings('fold') " Don't put spaces between the arguments! exe 'setl foldexpr=' . s:SID . '_FoldValue(v:lnum,b:csv_filter)' " Move folded area to the bottom, so there is only on consecutive " non-folded area if exists("s:csv_move_folds") && s:csv_move_folds \ && !&l:ro && &l:ma folddoclosed m$ let cpos.lnum = s:csv_fold_headerline + 1 endif call winrestview(cpos) endfu fu! ProcessFieldValue(field) "{{{3 let a = a:field if !exists("b:csv_fixed_width") try " strip leading whitespace if (a =~ '\s\+'. b:delimiter . '$') let b = split(a, '^\s\+\ze[^' . b:delimiter. ']\+')[0] else let b = a endif catch /^Vim\%((\a\+)\)\=:E684/ " empty pattern - should match only empty columns let b = a endtry " strip trailing delimiter try let a = split(b, b:delimiter . '$')[0] catch /^Vim\%((\a\+)\)\=:E684/ let a = b endtry if a == b:delimiter try let a=repeat(' ', ColWidth(col)) catch " no-op endtry endif endif return a endfu fu! OutputFilters(bang) "{{{3 if !a:bang call CheckHeaderLine() if s:csv_fold_headerline let title="Nr\tMatch\tCol\t Name\tValue" else let title="Nr\tMatch\tCol\tValue" endif echohl "Title" echo printf("%s", title) echo printf("%s", repeat("=",strdisplaywidth(title))) echohl "Normal" if !exists("b:csv_filter") || empty(b:csv_filter) echo printf("%s", "No active filter") else let items = values(b:csv_filter) call sort(items, "SortFilter") for item in items if s:csv_fold_headerline echo printf("%02d\t% 2s\t%02d\t%10.10s\t%s", \ item.id, (item.match ? '+' : '-'), item.col, \ substitute(GetColumn(1, item.col), \ b:col.'$', '', ''), item.orig) else echo printf("%02d\t% 2s\t%02d\t%s", \ item.id, (item.match ? '+' : '-'), \ item.col, item.orig) endif endfor endif else " Reapply filter again if !exists("b:csv_filter") || empty(b:csv_filter) call Warn("No filters defined currently!") return else exe 'setl foldexpr=' . s:SID . '_FoldValue(v:lnum,b:csv_filter)' endif endif endfu fu! SortFilter(a, b) "{{{3 return a:a.id == a:b.id ? 0 : \ a:a.id > a:b.id ? 1 : -1 endfu fu! GetColumn(line, col) "{{{3 " Return Column content at a:line, a:col let a=getline(a:line) " Filter comments out if a =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') return '' endif if !exists("b:csv_fixed_width_cols") try let a = split(a, '^' . b:col . '\zs')[a:col - 1] catch " index out of range let a = '' endtry else let a = matchstr(a, GetColPat(a:col, 0)) endif return substitute(a, '^\s\+\|\s\+$', '', 'g') endfu fu! RemoveLastItem(count) "{{{3 for [key,value] in items(b:csv_filter) if value.id == a:count call remove(b:csv_filter, key) endif endfor endfu fu! DisableFolding() "{{{3 setl nofen fdm=manual fdc=0 fdl=0 if !get(g:, 'csv_disable_fdt',0) && exists("s:fdt") && exists("s:fcs") exe printf("setl fdt=%s fcs=%s", s:fdt, escape(s:fcs, '\\|')) endif endfu fu! NumberFormat() "{{{3 let s:nr_format = [',', '.'] if exists("b:csv_thousands_sep") let s:nr_format[0] = b:csv_thousands_sep endif if exists("b:csv_decimal_sep") let s:nr_format[1] = b:csv_decimal_sep endif endfu fu! CheckHeaderLine() "{{{3 if !exists("b:csv_headerline") let s:csv_fold_headerline = 1 else let s:csv_fold_headerline = b:csv_headerline endif endfu fu! AnalyzeColumn(...) "{{{3 let maxcolnr = MaxColumns() if len(a:000) == 1 let colnr = a:1 else let colnr = WColumn() endif if colnr > maxcolnr call Warn("There exists no column " . colnr) return 1 endif " Initialize s:fold_headerline call CheckHeaderLine() let data = CopyCol('', colnr, '')[s:csv_fold_headerline : -1] let qty = len(data) let res = {} for item in data if empty(item) || item ==# b:delimiter let item = 'NULL' endif if !get(res, item) let res[item] = 0 endif let res[item]+=1 endfor let max_items = reverse(sort(values(res), s:csv_numeric_sort ? 'n' : 's:CSVSortValues')) " What about the minimum 5 items? let count_items = keys(res) if len(max_items) > 5 call remove(max_items, 5, -1) call map(max_items, 'printf(''\V%s\m'', escape(v:val, ''\\''))') call filter(res, 'v:val =~ ''^''.join(max_items, ''\|'').''$''') endif if has("float") let title="Nr\tCount\t % \tValue" else let title="Nr\tCount\tValue" endif echohl Title echo printf("%s", title) echohl Normal echo printf("%s", repeat('=', strdisplaywidth(title))) let i=1 for val in max_items for key in keys(res) if res[key] =~ val && i <= len(max_items) if !empty(b:delimiter) let k = substitute(key, b:delimiter . '\?$', '', '') else let k = key endif if has("float") echo printf("%02d\t%02d\t%2.0f%%\t%.50s", i, res[key], \ ((res[key] + 0.0)/qty)*100, k) else echo printf("%02d\t%02d\t%.50s", i, res[key], k) endif call remove(res,key) let i+=1 else continue endif endfor endfor echo printf("%s", repeat('=', strdisplaywidth(title))) echo printf("different values: %d", len(count_items)) unlet max_items endfunc fu! Vertfold(bang, col) "{{{3 if a:bang do Syntax return endif if !has("conceal") call Warn("Concealing not supported in your Vim") return endif if empty(b:delimiter) && !exists("b:csv_fixed_width_cols") call Warn("There are no columns defined, can't hide away anything!") return endif if empty(a:col) let colnr=WColumn() else let colnr=a:col endif let pat=GetPat(colnr, MaxColumns(), '.*', 1) if exists("b:csv_fixed_width_cols") && \ pat !~ '^\^\.\*' " Make the pattern implicitly start at line start, " so it will be applied by syntax highlighting (:h :syn-priority) let pat='^.*' . pat endif let pat=substitute(pat, '\\zs\(\.\*\)\@=', '', '') if !empty(pat) exe "syn match CSVFold /" . pat . "/ conceal cchar=+" endif endfu fu! InitCSVFixedWidth() "{{{3 if !exists("+cc") " TODO: make this work with a custom matchadd() command for older " Vims, that don't have 'colorcolumn' call Warn("'colorcolumn' option not available") return endif " Turn off syntax highlighting syn clear let max_len = len(split(getline(1), '\zs')) let _cc = &l:cc let &l:cc = 1 redraw! let Dict = {'1': 1} " first column is always the start of a new column let tcc = &l:cc let &l:cc = 1 echo ", , , , ..." let char=getchar() while 1 if char == "\" || char == "\" let tcc = eval('tcc'.(char=="\" ? '-' : '+').'1') if tcc < 0 let tcc=0 elseif tcc > max_len let tcc = max_len endif elseif char == "\" || char == 32 " Space let Dict[tcc] = 1 elseif char == "\" || char == 127 try call remove(Dict, reverse(sort(keys(Dict)))[0]) catch /^Vim\%((\a\+)\)\=:E\(\%(716\)\|\%(684\)\)/ " Dict or List empty break endtry elseif char == "\" || char == 27 let &l:cc=_cc redraw! return elseif char == "\" || char == "\n" || char == "\r" " Enter let Dict[tcc] = 1 break else break endif let &l:cc=tcc . (!empty(keys(Dict))? ',' . join(keys(Dict), ','):'') redraw! echo ", , , , ..." let char=getchar() endw let b:csv_fixed_width_cols=[] let tcc=0 let b:csv_fixed_width_cols = sort(keys(Dict), s:csv_numeric_sort ? 'n' : 's:CSVSortValues') let b:csv_fixed_width = join(sort(keys(Dict), s:csv_numeric_sort ? 'n' : 's:CSVSortValues'), ',') call Init(1, line('$')) let &l:cc=_cc redraw! endfu fu! NewRecord(line1, line2, count) "{{{3 if a:count =~ "\D" call Warn("Invalid count specified") return endif let cnt = (empty(a:count) ? 1 : a:count) let record = "" for item in range(1,MaxColumns()) if !exists("b:col_width") " Best guess width if exists("b:csv_fixed_width_cols") let record .= printf("%*s", ColWidth(item), \ b:delimiter) else let record .= printf("%20s", b:delimiter) endif else let record .= printf("%*s", get(b:col_width, item-1, 0)+1, b:delimiter) endif endfor if getline(1)[-1:] != b:delimiter let record = record[0:-2] . " " endif let line = [] for item in range(cnt) call add(line, record) endfor for nr in range(a:line1, a:line2) call append(nr, line) endfor endfu fu! MoveOver(outer) "{{{3 " Move over a field " a:outer means include the delimiter let last = 0 let outer_field = a:outer let cur_field = WColumn() let _wsv = winsaveview() if cur_field == MaxColumns() let last = 1 if !outer_field && getline('.')[-1:] != b:delimiter " No trailing delimiter, so inner == outer let outer_field = 1 endif endif " Move 1 column backwards, unless the cursor is in the first column " or in front of a delimiter if matchstr(getline('.'), '.\%'.virtcol('.').'v') != b:delimiter && virtcol('.') > 1 call MoveCol(-1, line('.')) endif " if cur_field != WColumn() " cursor was at the beginning of the field, and moved back to the " previous field, move back to original position " call cursor(_wsv.lnum, _wsv.col) " endif let _s = @/ if last exe "sil! norm! v$h" . (outer_field ? "" : "h") . (&sel ==# 'exclusive' ? "l" : '') else exe "sil! norm! v/." . b:col . "\h" . (outer_field ? "" : "h") . (&sel ==# 'exclusive' ? "l" : '') endif let _wsv.col = col('.')-1 call winrestview(_wsv) let @/ = _s endfu fu! CSVMappings() "{{{3 call Map('noremap', 'W', ':call MoveCol(1, line("."))') call Map('noremap', '', ':call MoveCol(1, line("."))') call Map('noremap', 'L', ':call MoveCol(1, line("."))') call Map('noremap', 'E', ':call MoveCol(-1, line("."))') call Map('noremap', '', ':call MoveCol(-1, line("."))') call Map('noremap', 'H', ':call MoveCol(-1, line("."), 1)') call Map('noremap', 'K', ':call MoveCol(0, line(".")-v:count1)') call Map('nnoremap', '', ':call MoveCol(0, line(".")-v:count1)') call Map('noremap', 'J', ':call MoveCol(0, line(".")+v:count1)') call Map('nnoremap', '', ':call MoveCol(0, line(".")+v:count1)') call Map('nnoremap', '', ':call PrepareFolding(1, 1)') call Map('nnoremap', '', ':call PrepareFolding(1, 0)') call Map('nnoremap', '', ':call PrepareFolding(0, 1)') call Map('imap', '', 'ColumnMode()', 'expr') " Text object: Field call Map('xnoremap', 'if', ':call MoveOver(0)') call Map('xnoremap', 'af', ':call MoveOver(1)') call Map('omap', 'af', ':norm vaf') call Map('omap', 'if', ':norm vif') call Map('xnoremap', 'iL', ':call SameFieldRegion()') call Map('omap', 'iL', ':call SameFieldRegion()') " Remap original values to a sane backup call Map('noremap', 'J', 'J') call Map('noremap', 'K', 'K') call Map('xnoremap', 'W', 'W') call Map('xnoremap', 'E', 'E') call Map('noremap', 'H', 'H') call Map('noremap', 'L', 'L') call Map('nnoremap', '', '') call Map('nnoremap', '', '') call Map('nnoremap', '', '') endfu fu! CommandDefinitions() "{{{3 call LocalCmd("WhatColumn", ':echo WColumn(0)', \ '-bang') call LocalCmd("NrColumns", ':call NrColumns()', '-bang') call LocalCmd("HiColumn", ':call HiCol(,0)', \ '-bang -nargs=?') call LocalCmd("SearchInColumn", \ ':call SearchColumn()', '-nargs=*') call LocalCmd("DeleteColumn", ':call DeleteColumn()', \ '-nargs=? -complete=custom,SortComplete') call LocalCmd("ArrangeColumn", \ ':call ArrangeCol(, , 0, -1, )', \ '-range -bang -nargs=?') call LocalCmd("UnArrangeColumn", \':call PrepUnArrangeCol(, )', \ '-range') call LocalCmd("InitCSV", ':call Init(,,0)', \ '-bang -range=%') call LocalCmd('Header', \ ':call SplitHeaderLine(,0,1)', \ '-nargs=? -bang') call LocalCmd('VHeader', \ ':call SplitHeaderLine(,0,0)', \ '-nargs=? -bang') call LocalCmd("HeaderToggle", \ ':call SplitHeaderToggle(1)', '') call LocalCmd("VHeaderToggle", \ ':call SplitHeaderToggle(0)', '') call LocalCmd("Sort", \ ':call Sort(0, ,,)', \ '-nargs=* -bang -range=% -complete=custom,SortComplete') call LocalCmd("Column", \ ':call CopyCol(empty()?''"'':,,)', \ '-count -register -nargs=?') call LocalCmd("MoveColumn", \ ':call MoveColumn(,,)', \ '-range=% -nargs=* -complete=custom,SortComplete') call LocalCmd("SumCol", \ ':echo csv#EvalColumn(, "SumColumn", ,)', \ '-nargs=? -range=% -complete=custom,SortComplete') call LocalCmd("MaxCol", \ ':echo csv#EvalColumn(, "MaxColumn", ,, 1)', \ '-nargs=? -range=% -complete=custom,SortComplete') call LocalCmd("MinCol", \ ':echo csv#EvalColumn(, "MaxColumn", ,, 0)', \ '-nargs=? -range=% -complete=custom,SortComplete') call LocalCmd("CountCol", \ ':echo csv#EvalColumn(, "CountColumn", ,)', \ '-nargs=? -range=% -complete=custom,SortComplete') call LocalCmd("ConvertData", \ ':call PrepareDoForEachColumn(,,0)', \ '-bang -nargs=? -range=%') call LocalCmd("Filters", ':call OutputFilters(0)', \ '-nargs=0 -bang') call LocalCmd("Analyze", ':call AnalyzeColumn()', \ '-nargs=?') call LocalCmd("VertFold", ':call Vertfold(0,)', \ '-bang -nargs=? -range=% -complete=custom,SortComplete') call LocalCmd("CSVFixed", ':call InitCSVFixedWidth()', '') call LocalCmd("NewRecord", ':call NewRecord(, \ , )', '-nargs=? -range') call LocalCmd("NewDelimiter", ':call NewDelimiter(, 1, line(''$''))', \ '-nargs=1') call LocalCmd("Duplicates", ':call CheckDuplicates()', \ '-nargs=1 -complete=custom,CompleteColumnNr') call LocalCmd('Transpose', ':call Transpose(, )', \ '-range=%') call LocalCmd('CSVTabularize', ':call Tabularize(0,,)', \ '-bang -range=%') call LocalCmd("AddColumn", \ ':call AddColumn(,,)', \ '-range=% -nargs=* -complete=custom,SortComplete') call LocalCmd('Substitute', ':call SubstituteInColumn(,,)', \ '-nargs=1 -range=%') endfu fu! Map(map, name, definition, ...) "{{{3 let keyname = substitute(a:name, '[<>]', '', 'g') let expr = (exists("a:1") && a:1 == 'expr' ? '' : '') if !get(g:, "csv_nomap_". tolower(keyname), 0) " All mappings are buffer local exe a:map " ". expr a:name a:definition " should already exists if a:map == 'nnoremap' let unmap = 'nunmap' elseif a:map == 'noremap' || a:map == 'map' let unmap = 'unmap' elseif a:map == 'vnoremap' let unmap = 'vunmap' elseif a:map == 'omap' let unmap = 'ounmap' elseif a:map == 'imap' let unmap = 'iunmap' elseif a:map == 'xnoremap' let unmap = 'xunmap' endif let b:undo_ftplugin .= "| " . unmap . " " . a:name endif endfu fu! LocalCmd(name, definition, args) "{{{3 if !exists(':'.a:name) exe "com! -buffer " a:args a:name a:definition let b:undo_ftplugin .= "| sil! delc " . a:name endif " Setup :CSV Aliases if a:name !~ '^CSV' call LocalCmd('CSV'.a:name, a:definition, a:args) endif endfu fu! Menu(enable) "{{{3 if a:enable " Make a menu for the graphical vim amenu CSV.&Init\ Plugin :InitCSV amenu CSV.SetUp\ &fixedwidth\ Cols :CSVFixed amenu CSV.-sep1- amenu &CSV.&Column.&Number :WhatColumn amenu CSV.Column.N&ame :WhatColumn! amenu CSV.Column.&Highlight\ column :HiColumn amenu CSV.Column.&Remove\ highlight :HiColumn! amenu CSV.Column.&Delete :DeleteColumn amenu CSV.Column.&Sort :%Sort amenu CSV.Column.&Copy :Column amenu CSV.Column.&Move :%MoveColumn amenu CSV.Column.S&um :%SumCol amenu CSV.Column.Analy&ze :Analyze amenu CSV.Column.&Arrange :%ArrangeCol amenu CSV.Column.&UnArrange :%UnArrangeCol amenu CSV.Column.&Add :%AddColumn amenu CSV.-sep2- amenu CSV.&Toggle\ Header :HeaderToggle amenu CSV.&ConvertData :ConvertData amenu CSV.Filters :Filters amenu CSV.Hide\ C&olumn :VertFold amenu CSV.&New\ Record :NewRecord else " just in case the Menu wasn't defined properly sil! amenu disable CSV endif endfu fu! SaveOptions(list) "{{{3 let save = {} for item in a:list exe "let save.". item. " = &l:". item endfor return save endfu fu! NewDelimiter(newdelimiter, firstl, lastl) "{{{3 let save = SaveOptions(['ro', 'ma']) if exists("b:csv_fixed_width_cols") call Warn("NewDelimiter does not work with fixed width column!") return endif if !&l:ma setl ma endif if &l:ro setl noro endif let delimiter = a:newdelimiter if a:newdelimiter is '\t' let delimiter="\t" endif let line=a:firstl while line <= a:lastl " Don't change delimiter for comments if getline(line) =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') let line+=1 continue endif let fields=split(getline(line), b:col . '\zs') " Remove field delimiter call map(fields, 'substitute(v:val, b:delimiter . \ ''\?$'' , "", "")') call setline(line, join(fields, delimiter)) let line+=1 endwhile " reset local buffer options for [key, value] in items(save) call setbufvar('', '&'. key, value) endfor "reinitialize the plugin if exists("g:csv_delim") let _delim = g:csv_delim endif let g:csv_delim = delimiter call Init(1,line('$')) if exists("_delim") let g:csv_delim = _delim else unlet g:csv_delim endif unlet! _delim endfu fu! IN(list, value) "{{{3 for item in a:list if item == a:value return 1 endif endfor return 0 endfu fu! DuplicateRows(columnlist) "{{{3 let duplicates = {} let cnt = 0 let line = 1 while line <= line('$') let key = "" let i = 1 let content = getline(line) " Skip comments if content =~ '^\s*\V'. escape(b:csv_cmt[0], '\\') continue endif let cols = split(content, b:col. '\zs') for column in cols if IN(a:columnlist, i) let key .= column endif let i += 1 endfor if has_key(duplicates, key) && cnt < 10 call Warn("Duplicate Row ". line) let cnt += 1 elseif has_key(duplicates, key) call Warn("More duplicate Rows after: ". line) call Warn("Aborting...") return else let duplicates[key] = 1 endif let line += 1 endwhile if cnt == 0 call Warn("No Duplicate Row found!") endif endfu fu! CompleteColumnNr(A,L,P) "{{{3 return join(range(1,MaxColumns()), "\n") endfu fu! CheckDuplicates(list) "{{{3 let string = a:list if string =~ '\d\s\?-\s\?\d' let string = substitute(string, '\(\d\+\)\s\?-\s\?\(\d\+\)', \ '\=join(range(submatch(1),submatch(2)), ",")', '') endif let list=split(string, ',') call DuplicateRows(list) endfu fu! Transpose(line1, line2) "{{{3 " Note: - Comments will be deleted. " - Does not work with fixed-width columns if exists("b:csv_fixed_width") call Warn("Transposing does not work with fixed-width columns!") return endif let _wsv = winsaveview() let TrailingDelim = 0 if line('$') > 1 let TrailingDelim = getline(1) =~ b:delimiter.'$' endif let pat = '^\s*\V'. escape(b:csv_cmt[0], '\\') try let columns = MaxColumns(a:line1) catch " No column, probably because of comment or empty line " so use the number of columns from the beginning of the file let columns = MaxColumns() endtry let matrix = [] for line in range(a:line1, a:line2) " Filter comments out if getline(line) =~ pat continue endif let r = [] for row in range(1,columns) let field = GetColumn(line, row) call add(r, field) endfor call add(matrix, r) endfor unlet row " create new transposed matrix let transposed = [] for row in matrix let i = 0 for val in row if get(transposed, i, []) == [] call add(transposed, []) endif if val[-1:] != b:delimiter let val .= b:delimiter endif call add(transposed[i], val) let i+=1 endfor endfor " Save memory unlet! matrix call map(transposed, 'join(v:val, '''')') if !TrailingDelim call map(transposed, 'substitute(v:val, b:delimiter.''\?$'', "", "")') endif " filter out empty records call filter(transposed, 'v:val != b:delimiter') " Insert transposed data let delete_last_line = 0 if a:line1 == 1 && a:line2 == line('$') let delete_last_line = 1 endif exe a:line1. ",". a:line2. "d _" let first = (a:line1 > 0 ? (a:line1 - 1) : 0) call append(first, transposed) if delete_last_line sil $d _ endif " save memory unlet! transposed call winrestview(_wsv) endfu fu! NrColumns(bang) "{{{3 try let cols = empty(a:bang) ? MaxColumns() : MaxColumns(line('.')) catch " No column or comment line call Warn("No valid CSV Column!") endtry echo cols endfu fu! Tabularize(bang, first, last) "{{{3 if match(split(&ft, '\.'),'csv') == -1 call Warn("No CSV filetype, aborting!") return endif let _c = winsaveview() " Table delimiter definition "{{{4 if !exists("s:td") let s:td = { \ 'hbar': (&enc =~# 'utf-8' ? '─' : '-'), \ 'vbar': (&enc =~# 'utf-8' ? '│' : '|'), \ 'scol': (&enc =~# 'utf-8' ? '├' : '|'), \ 'ecol': (&enc =~# 'utf-8' ? '┤' : '|'), \ 'ltop': (&enc =~# 'utf-8' ? '┌' : '+'), \ 'rtop': (&enc =~# 'utf-8' ? '┐' : '+'), \ 'lbot': (&enc =~# 'utf-8' ? '└' : '+'), \ 'rbot': (&enc =~# 'utf-8' ? '┘' : '+'), \ 'cros': (&enc =~# 'utf-8' ? '┼' : '+'), \ 'dhor': (&enc =~# 'utf-8' ? '┬' : '-'), \ 'uhor': (&enc =~# 'utf-8' ? '┴' : '-') \ } endif "}}}4 if match(getline(a:first), '^'.s:td.ltop) > -1 " Already tabularized, done call Warn("Looks already Tabularized, aborting!") return endif let _ma = &l:ma setl ma let colwidth = 0 let adjust_last = 0 call cursor(a:first,0) call CheckHeaderLine() let line=a:first if exists("g:csv_table_leftalign") let b:csv_arrange_leftalign = 1 endif let newlines=[] let content=[] while line <= a:last if foldclosed(line) != -1 let line = foldclosedend(line) + 1 continue endif let curline = getline(line) call add(content, curline) if empty(split(curline, b:delimiter)) " only empty delimiters, add one empty delimiter " (:NewDelimiter strips trailing delimiter let curline = repeat(b:delimiter, MaxColumns()) call add(newlines, line) call setline(line, curline) endif let line+=1 endw unlet! line let delim=b:delimiter new call setline(1,content) let b:delimiter=delim call Init(1,line('$'), 1) if exists("b:csv_fixed_width_cols") let cols=copy(b:csv_fixed_width_cols) let pat = join(map(cols, ' ''\(\%''. v:val. ''c\)'' '), '\|') let colwidth = strlen(substitute(getline('$'), '.', 'x', 'g')) let t=-1 let b:col_width = [] for item in b:csv_fixed_width_cols + [colwidth] if t > -1 call add(b:col_width, item-t) endif let t = item endfor else " don't clear column width variable, might have been set in the " plugin! sil call ArrangeCol(1, line('$'), 0, -1) if !get(b:, 'csv_arrange_leftalign',0) for line in newlines let cline = getline(line) let cline = substitute(cline, '\s$', ' ', '') call setline(line, cline) endfor unlet! line endif endif if empty(b:col_width) call Warn('An error occured, aborting!') return endif if getline(a:first)[-1:] isnot? b:delimiter let b:col_width[-1] += 1 endif let marginline = s:td.scol. join(map(copy(b:col_width), 'repeat(s:td.hbar, v:val)'), s:td.cros). s:td.ecol call NewDelimiter(s:td.vbar, 1, line('$')) "exe printf('sil %d,%ds/%s/%s/ge', a:first, (a:last+adjust_last), " \ (exists("b:csv_fixed_width_cols") ? pat : b:delimiter ), s:td.vbar) " Add vertical bar in first column, if there isn't already one exe printf('sil %d,%ds/%s/%s/e', 1, line('$'), \ '^[^'. s:td.vbar. s:td.scol. ']', s:td.vbar.'&') " And add a final vertical bar, if there isn't one already exe printf('sil %d,%ds/%s/%s/e', 1, line('$'), \ '[^'. s:td.vbar. s:td.ecol. ']$', '&'. s:td.vbar) " Make nice intersection graphs let line = split(getline(1), s:td.vbar) call map(line, 'substitute(v:val, ''[^''.s:td.vbar. '']'', s:td.hbar, ''g'')') " Set top and bottom margins call append(0, s:td.ltop. join(line, s:td.dhor). s:td.rtop) call append(line('$'), s:td.lbot. join(line, s:td.uhor). s:td.rbot) if s:csv_fold_headerline > 0 call append(1 + s:csv_fold_headerline, marginline) let adjust_last += 1 endif " Syntax will be turned off, so disable this part " " Adjust headerline to header of new table "let b:csv_headerline = (exists('b:csv_headerline')?b:csv_headerline+2:3) "call CheckHeaderLine() " Adjust syntax highlighting "unlet! b:current_syntax "ru syntax/csv.vim if a:bang exe printf('sil %d,%ds/^%s\zs\n/&%s&/e', 1 + s:csv_fold_headerline, line('$') + adjust_last, \ '[^'.s:td.scol. '][^'.s:td.hbar.'].*', marginline) endif syn clear let &l:ma = _ma call winrestview(_c) endfu fu! SubstituteInColumn(command, line1, line2) range "{{{3 " Command can be something like 1,2/foobar/foobaz/ to replace in 1 and second column " Command can be something like /foobar/foobaz/ to replace in the current column " Command can be something like 1,$/foobar/foobaz/ to replace in all columns " Command can be something like 3/foobar/foobaz/flags to replace only in the 3rd column " Save position and search register let _wsv = winsaveview() let _search = [ '/', getreg('/'), getregtype('/')] let columns = [] let maxcolnr = MaxColumns() let simple_s_command = 0 " when set to 1, we can simply use an :s command " try to split on '/' if it is not escaped or in a collection let cmd = split(a:command, '\%([\\]\|\[[^]]*\)\@WColumn()) let cmd = [columns[0]] + cmd "First item of cmd list contains address! elseif ((len(cmd) == 3 && cmd[2] !~# '^[&cgeiInp#l]\+$') \ || len(cmd) == 4) " command could be '1/foobbar/foobaz' " but also 'foobar/foobar/g' let columns = split(cmd[0], ',') if empty(columns) " No columns given? replace in current column only let columns[0] = WColumn() elseif columns[-1] == '$' let columns[-1] = maxcolnr endif else " not reached ? call add(columns, WColumn()) endif try if len(cmd) == 1 || columns[0] =~ '\D' || (len(columns) == 2 && columns[1] =~ '\D') call Warn("Error! Usage :S [columns/]pattern/replace[/flags]") return endif if len(columns) == 2 && columns[0] == 1 && columns[1] == maxcolnr let simple_s_command = 1 elseif len(columns) == 2 let columns = range(columns[0], columns[1]) endif let has_flags = len(cmd) == 4 if simple_s_command while search(cmd[1]) exe printf("%d,%ds/%s/%s%s", a:line1, a:line2, cmd[1], cmd[2], (has_flags ? '/'. cmd[3] : '')) if !has_flags || (has_flags && cmd[3] !~# 'g') break endif endw else for colnr in columns let @/ = GetPat(colnr, maxcolnr, cmd[1], 1) while search(@/) exe printf("%d,%ds//%s%s", a:line1, a:line2, cmd[2], (has_flags ? '/'. cmd[3] : '')) if !has_flags || (has_flags && cmd[3] !~# 'g') break endif endw endfor endif catch /^Vim\%((\a\+)\)\=:E486/ " Pattern not found echohl Error echomsg "E486: Pattern not found in column " . colnr . ": " . pat if &vbs > 0 echomsg substitute(v:exception, '^[^:]*:', '','') endif echohl Normal catch echohl Error "if &vbs > 0 echomsg substitute(v:exception, '^[^:]*:', '','') "endif echohl Normal finally " Restore position and search register call winrestview(_wsv) call call('setreg', _search) endtry endfu fu! ColumnMode() "{{{3 let mode = mode() if mode =~# 'R' " (virtual) Replace mode let new_line = (line('.') == line('$') || \ (synIDattr(synIDtrans(synID(line("."), col("."), 1)), "name") =~? "comment")) return "\g`[". (new_line ? "o" : "J".mode) else return "\" endif endfu fu! Timeout(start) "{{{3 return localtime()-a:start < 2 endfu fu! SameFieldRegion() "{{{3 " visually select the region, that has the same value in the cursor field let col = WColumn() let max = MaxColumns() let field = GetColumn(line('.'), col) let line = line('.') let limit = [line, line] " Search upwards and downwards from the current position and find the " limit of the current selection while line > 1 let line -= 1 if GetColumn(line, col) ==# field let limit[0] = line else break endif endw let line = line('.') while line > 1 && line < line('$') let line += 1 if GetColumn(line, col) ==# field let limit[1] = line else break endif endw exe printf(':norm! %dGV%dG',limit[0],limit[1]) endfu fu! CSV_CloseBuffer(buffer) "{{{3 " Setup by SetupAutoCmd autocommand try if bufnr((a:buffer)+0) > -1 exe a:buffer. "bw" endif catch /^Vim\%((\a\+)\)\=:E517/ " buffer already wiped " no-op finally augroup CSV_QuitPre au! augroup END augroup! CSV_QuitPre endtry endfu " Global functions "{{{2 fu! csv#EvalColumn(nr, func, first, last, ...) range "{{{3 " Make sure, the function is called for the correct filetype. if match(split(&ft, '\.'), 'csv') == -1 call Warn("File is no CSV file!") return endif let save = winsaveview() call CheckHeaderLine() let nr = matchstr(a:nr, '^\-\?\d\+') let col = (empty(nr) ? WColumn() : nr) if col == 0 let col = 1 endif " don't take the header line into consideration let start = a:first - 1 + s:csv_fold_headerline let stop = a:last - 1 + s:csv_fold_headerline let column = CopyCol('', col, '')[start : stop] " Delete delimiter call map(column, 'substitute(v:val, b:delimiter . "$", "", "g")') " Revmoe trailing whitespace call map(column, 'substitute(v:val, ''^\s\+$'', "", "g")') " Remove leading whitespace call map(column, 'substitute(v:val, ''^\s\+'', "", "g")') " Delete empty values " Leave this up to the function that does something " with each value "call filter(column, '!empty(v:val)') " parse the optional number format let format = matchstr(a:nr, '/[^/]*/') let s:additional={} call NumberFormat() if !empty(format) try let s = [] " parse the optional number format let str = matchstr(format, '/\zs[^/]*\ze/', 0, start) let s = matchlist(str, '\(.\)\?:\(.\)\?')[1:2] if empty(s) " Number format wrong call Warn("Numberformat wrong, needs to be /x:y/!") return '' endif if !empty(s[0]) let s:nr_format[0] = s[0] endif if !empty(s[1]) let s:nr_format[1] = s[1] endif endtry endif let distinct = matchstr(a:nr, '\') if !empty(distinct) let s:additional.distinct=1 endif if function(a:func) is# function("\".s:SID."_MaxColumn") let s:additional.ismax = a:1 endif try let result=call(function(a:func), [column]) return result catch " Evaluation of expression failed echohl Title echomsg "Evaluating" matchstr(a:func, '[a-zA-Z]\+$') \ "failed for column" col . "!" echohl Normal return '' finally call winrestview(save) endtry endfu " return field index (x,y) with leading/trailing whitespace and trailing " delimiter stripped (only when a:0 is not given) fu! CSVField(x, y, ...) "{{{3 if &ft != 'csv' return endif let y = a:y - 1 let x = (a:x < 0 ? 0 : a:x) let orig = !empty(a:0) let y = (y < 0 ? 0 : y) let x = (x > (MaxColumns()) ? (MaxColumns()) : x) let col = CopyCol('',x,'') if !orig " remove leading and trainling whitespace and the delimiter return matchstr(col[y], '^\s*\zs.\{-}\ze\s*'.b:delimiter.'\?$') else return col[y] endif endfu " return current column number (if a:0 is given, returns the name fu! CSVCol(...) "{{{3 return WColumn(a:0) endfu fu! CSVPat(colnr, ...) "{{{3 " Make sure, we are working in a csv file if &ft != 'csv' return '' endif " encapsulates GetPat(), that returns the search pattern for a " given column and tries to set the cursor at the specific position let pat = GetPat(a:colnr, MaxColumns(), a:0 ? a:1 : '.*', 1) "let pos = match(pat, '.*\\ze') + 1 " Try to set the cursor at the beginning of the pattern " does not work "call setcmdpos(pos) return pat endfu fu! CSVSum(col, fmt, first, last) "{{{3 let first = a:first let last = a:last if empty(first) let first = 1 endif if empty(last) let last = line('$') endif return csv#EvalColumn(a:col, 'SumColumn', first, last) endfu fu! CSVMax(col, fmt, first, last) "{{{3 let first = a:first let last = a:last if empty(first) let first = 1 endif if empty(last) let last = line('$') endif return csv#EvalColumn(a:col, 'MaxColumn', first, last, 1) endfu fu! CSVMin(col, fmt, first, last) "{{{3 let first = a:first let last = a:last if empty(first) let first = 1 endif if empty(last) let last = line('$') endif return csv#EvalColumn(a:col, 'MaxColumn', first, last, 0) endfu fu! CSVCount(col, fmt, first, last, ...) "{{{3 let first = a:first let last = a:last let distinct = 0 if empty(first) let first = 1 endif if empty(last) let last = line('$') endif if !exists('s:additional') let s:additional = {} endif if exists("a:1") && !empty(a:1) let s:additional['distinct'] = 1 endif let result=csv#EvalColumn(a:col, 'CountColumn', first, last, distinct) unlet! s:additional['distinct'] return (empty(result) ? 0 : result) endfu fu! CSV_WCol(...) "{{{3 " Needed for airline try if exists("a:1") && (a:1 == 'Name' || a:1 == 1) return printf("%s", WColumn(1)) else return printf(" %d/%d", WColumn(), MaxColumns()) endif catch return '' endtry endfun " Initialize Plugin "{{{2 let b:csv_start = exists("b:csv_start") ? b:csv_start : 1 let b:csv_end = exists("b:csv_end") ? b:csv_end : line('$') call Init(b:csv_start, b:csv_end) let &cpo = s:cpo_save unlet s:cpo_save " Vim Modeline " {{{2 " vim: set foldmethod=marker et: