" Vim plugin -- Vi-style editing for the cmdline " General: {{{1 " File: conomode.vim " Created: 2008 Sep 28 " Last Change: 2012 Jan 28 " Rev Days: 36 " Author: Andy Wokula " Version: 0.6 (macro, undo) " Credits: " inspired from a vim_use thread on vi-style editing in the bash (there " enabled with 'set -o vi'). " Subject: command line " Date: 25-09-2008 " CAUTION: This script may crash Vim now and then! (buggy " getcmdline()?) -- almost fixed since Vim7.3f BETA " Description: {{{1 " Implements a kind of Normal mode ( "Cmdline-Normal mode" ) for the " Command line. Great fun if :h cmdline-window gets boring ;-) " Usage: {{{1 " - when in Cmdline-mode, press to enter (was ) " "Commandline-Normal mode" " - mode indicator: a colon ":" at the cursor, hiding the char under it " (side effect of incomplete mapping) " - quit to Cmdline-mode with "i", ":", or any unmapped key (which then " executes or inserts itself), or wait 60 seconds. " Features So Far: {{{1 " - Motions: h l w b e W B E 0 ^ $ f{char} F{char} t{char} T{char} ; , % " also in Operator pending mode " - Operators: d y c " these write to the unnamed register; c prompts for input() " - Simple Changes: r{char} ~ " - Putting: P p " puts the unnamed register " - Mode Switching: " I i a A - back to Cmdline (with positioned cursor), - back to " Normal mode, - execute Cmdline, : - back to Cmdline (remove all " text) " - Insert: o " input() version of i (versus i: accepts a count, recordable) " - Repeating: . " repeatable commands: d r c ~ o " also: I i a A " - Undo: u U " redo with "U" (to keep c_CTRL-R working); undo information survives mode " switching; undo is unlimited " - Count: can be given for most commands " - Macros: q @ " q starts[/stops] recording, @ executes, no register involved " - Shortcuts: yy Y dd D x X cc C s S " yy -> y_, Y -> y$, dd -> d_, D -> d$, x -> dl, X -> dh, cc -> c_, " C -> c$, s -> cl, S -> 0d$i " - Misc: - redraw the Cmdline, gX - cut undo (forget older entries) " Incompatibilities: (some ...) {{{1 " - redo with "U" (instead of "") " - "q" and "@" don't ask for a register, "@" while recording a macro " immediately executes " - "o" is alternate version of "i", no "O" " - no "Beep" situation (yet) to interrupt macro execution " Small Differences: " - "e" jumps after the word, "$" jumps to EOL (after last char in the " line), "e" and "$" are exclusive " - at EOL, "x", "dl", "dw" etc. do not go left to delete at least one " character " - typing "dx" does "x", ignoring "d"; same for similar situations " - "c", "r", "~": no undo step if old and new text are equal; "i": no undo " step if nothing inserted " - "yy" yanks characterwise " - "q" does not record into a register, data is stored in g:CONOMODE_RECBUF " Notes: {{{1 " - strange: utf-8 characters are garbled by the mode indicator; press " Ctrl-L to redraw " - how to find out which keys are mapped in the mode? " :ConomodemacroLocal cmap : " - mapping : ( = a key code expanding to several bytes) " doesn't work; probably this is related to a known Vim bug: " :h todo|/These two abbreviations don't give the same result: " - manipulation of cmdline and cursor position uses getcmdline(), " c_CTRL-\_e, c_CTRL-R_=, getcmdpos(), setcmdpos() " - ok: "3fx;;" -- do not remember the count for ";" " - ok: "cw{text}5." -- "5." does "5cw{text}" " TODO: {{{1 " - M recording of ^R* (or remove ^R*) " - M we need a beep: when executing, if one of the recorded commands fails, " the rest of the commands should not be executed " - M beep: or just do :normal plus feedkeys( : ) ? " ? refactor s:count1? " ? while recording, use input() for "i", "I", "a", "A" " ? recursive " - (non-vi) "c", "i": if the last inserted char is a parenthesis (and it is " the only one), then "." will insert the corresponding paren " - support more registers, make '"adw' work " - last-position jump, `` " ? (non-vi) somehow enable Smartput?? " ? (from vile) "q" in Operator-pending mode records a motion " - i{text} (or just {text}): starting with empty cmdline can't " be repeated " - search commands "/", "?", "n", "N" for the cmd-history " - make ":" work like in ctmaps.vim " - zap to multi-byte char " " + count: [1-9][0-9]* enable zero after one non-zero " + count with multiplication: 2d3w = 6dw " + count: f{char}, F{char}; r{char}; undo; put " + "c" is repeatable " + BF compare old and new text case sensitive " + BF for now, disable recursive " + BF opend(), allow "c" on zero characters " + qcfx^UFoo^M@ works!! (with somes "x"es in the line) " + BF qc$q recorded ocondollar instead of ocon$ " + doop_c: no default text, instead add old text to input history " + BF doop_c: escape chars in input string for use in mapping (?) - yes! " + implement "i", "I" and "A" with input(), like "c" " + no need longer need to type in "c{motion}{text}" " + BF undo/redo is now recorded " + BF doop_c, opend: c{motion} should leave the cursor after the change " + after playing a macro, undo the recorded commands at once. " ! KISS: let "@" remember the undo-index (mac_begin); when finished with " playing, remove the []s back to that index " + command "a": move right first " + continuous undo (don't break undo when switching to Cmdline-mode) " + multi-byte support (!): some commands moved bytewise, not characterwise " (Mbyte); noch was übersehen? " + BF -recursion prevention did = within e (not allowed) " + BF need two kinds of escaping, s:MapEscape() " + remove the [count] limits (e.g. don't expand "3h" to "") " what about "3h" -> "2h", "50@" -> "@49@"; simple motions only " ! do "dorep", while count > 0 " + NF "%" motion, motions can become inclusive (added s:incloff) " + NF motion "|" " + BF "f" now inclusive " + NF added "t" and "T" (always move cursor, as in newer Vims) " + NF each cmdtype (':', '/?') gets separate undo data (hmm, Ctrl-C wipes " undo data) " + whole-line text object for "cc", "dd", etc. (repeat used c$, d$) " + s:getpos_* functions now return 0-based positions (1-based sux) " + BF: cmdl "infiles", inserting "filou" before "f" made try_continue_undo " detect "oufil" as inserted part; now use cursor position to decide " + NF: "gX" - cut older undo states " + BF: :* now recorded " }}} " Checks: {{{1 if exists("loaded_conomode") finish endif let loaded_conomode = 1 if v:version < 700 echomsg "conomode: you need at least Vim 7.0" finish endif let s:cpo_sav = &cpo set cpo&vim if &cedit == "\" echomsg "Conomode: Please do :set cedit& (only a warning)." " the user's new key for 'cedit' may come in the way endif " Config: {{{1 " if non-zero, add a few keys for Cmdline-mode if !exists("g:conomode_emacs_keys") let g:conomode_emacs_keys = 0 endif " Some Local Variables: {{{1 let s:zaprev = {"f": "F", "F": "f", "t": "T", "T": "t"} if !exists("s:undo") let s:undo = {} endif if !exists("s:quitnormal") let s:quitnormal = 1 endif if !exists("s:undostore") let s:undostore = {} endif " DEBUG: let g:conomode_dbg_undo = s:undo " word forward patterns: let s:wfpat = { \ "w": ['\k*\s*\zs', '\s*\zs', '\%(\k\@!\S\)*\s*\zs'] \, "W": ['\S*\s*\zs', '\s*\zs', '\S*\s*\zs'] \, "e": ['\k\+\zs', '\s*\%(\k\+\|\%(\k\@!\S\)*\)\zs', '\%(\k\@!\S\)*\zs'] \, "E": ['\S\+\zs', '\s*\S*\zs', '\S*\zs'] \} let s:wbpat = { \ "b": ['\k*$', '\%(\k\+\|\%(\k\@!\S\)*\)\s*$', '\%(\k\@!\S\)*$'] \, "B": ['\S*$', '\S*\s*$', '\S*$'] \} let s:cmdrev = { \ "caret": "^", "scolon": ";", "comma": ",", "dollar": "$" \, "percent": "%", "bar": "|" \, "put0-1": "P", "put1-1": "p", "put00": "*" } "}}}1 " Functions: " Getpos: {{{1 func! s:forward_word(wm, count1) " wm - word motion: w, W or e let pat = s:wfpat[a:wm] let cnt = a:count1 let gcp = getcmdpos()-1 let cmdl = getcmdline()[gcp :] while 1 let cpchar = matchstr(cmdl, '^.') if cpchar =~ '\k' let matpos = match(cmdl, pat[0]) elseif cpchar =~ '\s' let matpos = match(cmdl, pat[1]) else let matpos = match(cmdl, pat[2]) endif let cnt -= 1 if cnt <= 0 || matpos <= 0 break endif let gcp += matpos let cmdl = cmdl[matpos :] endwhile let newcp = gcp + matpos return newcp endfunc func! s:getpos_w() return s:forward_word("w", s:count1) endfunc func! s:getpos_W() return s:forward_word("W", s:count1) endfunc func! s:getpos_e() return s:forward_word("e", s:count1) endfunc func! s:getpos_E() return s:forward_word("E", s:count1) endfunc func! s:backward_word(wm, count1) let pat = s:wbpat[a:wm] let cnt = a:count1 let gcp = getcmdpos()-1 let cmdl = strpart(getcmdline(), 0, gcp) while gcp >= 1 let cpchar = matchstr(cmdl, '.$') if cpchar =~ '\k' let gcp = match(cmdl, pat[0]) elseif cpchar =~ '\s' let gcp = match(cmdl, pat[1]) else let gcp = match(cmdl, pat[2]) endif let cnt -= 1 if cnt <= 0 || gcp <= 0 break endif let cmdl = strpart(cmdl, 0, gcp) endwhile return gcp endfunc func! s:getpos_b() return s:backward_word("b", s:count1) endfunc func! s:getpos_B() return s:backward_word("B", s:count1) endfunc func! s:getpos_h() " Omap mode only let gcp = getcmdpos()-1 if s:count1 > gcp return 0 elseif s:count1 == 1 if gcp >= 8 return gcp-8+match(strpart(getcmdline(), gcp-8, 8), '.$') else return match(strpart(getcmdline(), 0, gcp), '.$') endif endif let pos = match(strpart(getcmdline(), 0, gcp), '.\{'.s:count1.'}$') return pos >= 0 ? pos : 0 endfunc func! s:getpos_l() let gcp = getcmdpos()-1 if s:count1 == 1 return matchend(getcmdline(), '.\|$', gcp) endif let cmdlsuf = strpart(getcmdline(), gcp) let lensuf = strlen(cmdlsuf) if s:count1 >= lensuf return gcp+lensuf else return gcp+matchend(cmdlsuf, '.\{'.s:count1.'}\|$') endif endfunc func! s:getpos_dollar() return strlen(getcmdline()) endfunc func! s:getpos_0() return 0 endfunc func! s:getpos_caret() return match(getcmdline(), '\S') endfunc " jump to matching paren func! s:getpos_percent() let gcp = getcmdpos()-1 let cmdl = getcmdline() if cmdl[gcp] !~ '[()[\]{}]' let ppos = match(cmdl, '[()[\]{}]', gcp) if ppos == -1 return gcp endif else let ppos = gcp endif " balance counter, paren position, opening/closing paren character, " first opening/closing (paren) position let pairs = '()[]{}' let bc = 1 if cmdl[ppos] =~ '[([{]' let opc = cmdl[ppos] let cpc = pairs[stridx(pairs, opc)+1] let fop = stridx(cmdl, opc, ppos+1) let fcp = stridx(cmdl, cpc, ppos+1) while 1 if fcp == -1 return gcp elseif bc==1 && (fop == -1 || fcp < fop) let s:incloff = 1 return fcp endif if fop >= 0 && fop < fcp let bc += 1 let fop = stridx(cmdl, opc, fop+1) else let bc -= 1 let fcp = stridx(cmdl, cpc, fcp+1) endif endwhile else let cpc = cmdl[ppos] let opc = pairs[stridx(pairs, cpc)-1] let fcp = strridx(cmdl, cpc, ppos-1) let fop = strridx(cmdl, opc, ppos-1) while 1 if fop == -1 return gcp elseif bc==1 && (fcp == -1 || fop > fcp) let s:incloff = 1 return fop endif if fcp > fop let bc += 1 let fcp = strridx(cmdl, cpc, fcp-1) else let bc -= 1 let fop = strridx(cmdl, opc, fop-1) endif endwhile endif return gcp endfunc func! s:getpos_bar() let cmdl = getcmdline() let pos = byteidx(cmdl, s:count1-1) if pos == -1 return strlen(cmdl) else return pos endif endfunc " Getzappos: {{{1 func! s:getzappos(zapcmd, ...) let cnt = s:count1 if a:0 == 0 if !s:from_mapping call inputsave() let aimchar = nr2char(getchar()) call inputrestore() else let aimchar = nr2char(getchar()) endif let s:lastzap = [a:zapcmd, aimchar] if s:recording " call s:rec_chars(cnt, a:zapcmd.aimchar) if s:zapmode == "n" let reczap = "&cono". a:zapcmd else let reczap = "&ocon". a:zapcmd endif if s:zapmode == "o" && s:operator == "c" let s:rec_op_c = reczap."". s:MapEscape(aimchar) else call s:rec_chars(cnt, reczap."". s:MapEscape(aimchar).":") endif endif else let aimchar = a:1 endif let gcp = getcmdpos()-1 let newcp = gcp let cmdl = getcmdline() if a:zapcmd ==# "f" || a:zapcmd ==# "t" if a:zapcmd ==# "t" let newcp += 1 endif while cnt >= 1 && newcp >= 0 let newcp = stridx(cmdl, aimchar, newcp+1) let cnt -= 1 endwhile if newcp < 0 let newcp = gcp else if a:zapcmd ==# "t" " FIXME multibyte? let newcp -= 1 endif let s:incloff = 1 endif else " F if a:zapcmd ==# "T" let newcp -= 1 endif while cnt >= 1 && newcp >= 0 let newcp = strridx(cmdl, aimchar, newcp-1) let cnt -= 1 endwhile if newcp < 0 let newcp = gcp elseif a:zapcmd ==# "T" " multibyte? let newcp += 1 endif endif let s:beep = newcp == gcp return newcp endfunc func! s:getpos_f() return s:getzappos("f") endfunc func! s:getpos_F() return s:getzappos("F") endfunc func! s:getpos_t() return s:getzappos("t") endfunc func! s:getpos_T() return s:getzappos("T") endfunc func! s:getpos_scolon() if exists("s:lastzap") return s:getzappos(s:lastzap[0], s:lastzap[1]) else return getcmdpos()-1 endif endfunc func! s:getpos_comma() if exists("s:lastzap") return s:getzappos(s:zaprev[s:lastzap[0]], s:lastzap[1]) else return getcmdpos()-1 endif endfunc " Move: {{{1 func! move(motion) let s:count1 = s:getcount1() call setcmdpos(1 + s:getpos_{a:motion}()) call s:rec_chars(s:count1, a:motion) return "" endfunc func! move_zap(zapcmd) let s:count1 = s:getcount1() let s:zapmode = "n" call setcmdpos(1 + s:getzappos(a:zapcmd)) return "" endfunc " Put: {{{1 func! edit_put(mode, reg, gcpoff, endoff) let coff = a:gcpoff if a:mode == 1 " limit count to 500 let cnt = min([s:getcount1(),500]) let s:lastedit = ["edit_put", 0, a:reg, coff, a:endoff] let s:lastcount = cnt call s:rec_chars(cnt, "put". a:gcpoff. a:endoff) else let cnt = s:lastcount endif let gcp = getcmdpos()-1 let cmdl = getcmdline() if coff == 1 && cmdl[gcp] == "" let coff = 0 endif let boff = coff==0 ? 0 : matchend(strpart(cmdl, gcp, 8), '.') let ins = repeat(getreg(a:reg), cnt) if ins != "" " after undoing "p", move the cursor one left from the start of the " change call s:undo.add(0, "m", gcp, "") call s:undo.add(1, "i", gcp+boff, ins) call setcmdpos(gcp+1+strlen(ins)+boff+a:endoff) endif return strpart(cmdl, 0, gcp+boff). ins. strpart(cmdl, gcp+boff) endfunc " Edit: {{{1 func! edit_r(mode, ...) if a:mode == 1 let cnt = s:getcount1() if !s:from_mapping call inputsave() let replchar = nr2char(getchar()) call inputrestore() else let replchar = nr2char(getchar()) endif let s:lastedit = ["edit_r", 0, replchar] let s:lastcount = cnt " we must have that damn replchar BEFORE the next : call s:rec_chars(cnt, "&conor".s:MapEscape(replchar).":") else let replchar = a:1 let cnt = s:lastcount endif let gcp = getcmdpos()-1 let cmdl = getcmdline() let ripos = matchend(cmdl, '.\{'.cnt.'}', gcp) if ripos >= 1 let mid = cmdl[gcp : ripos-1] let newmid = repeat(replchar, cnt) if mid !=# newmid call s:undo.add(0, "d", gcp, mid) call s:undo.add(1, "i", gcp, newmid) endif return strpart(cmdl, 0, gcp). newmid. strpart(cmdl, ripos) else return cmdl endif endfunc func! edit_tilde(mode, ...) if a:mode == 1 let cnt = s:getcount1() let s:lastedit = ["edit_tilde", 0] let s:lastcount = cnt call s:rec_chars(cnt, "~") else let cnt = s:lastcount endif let gcp = getcmdpos()-1 let cmdl = getcmdline() let ripos = matchend(cmdl, '.\{1,'.cnt.'}', gcp) if ripos >= 1 let mid = cmdl[gcp : ripos-1] " let newmid = substitute(mid, '\(\u\)\|\(\l\)', '\l\1\u\2', 'g') let newmid = substitute(mid, '\k', '\=toupper(submatch(0))==#submatch(0) ? tolower(submatch(0)) : toupper(submatch(0))', 'g') if mid !=# newmid call s:undo.add(0, "d", gcp, mid) call s:undo.add(1, "i", gcp, newmid) endif call setcmdpos(gcp+1 + strlen(newmid)) return strpart(cmdl, 0, gcp). newmid. strpart(cmdl, ripos) else return cmdl endif endfunc func! setop(op) let s:operator = a:op let s:beep = 0 call s:rec_chars("", a:op) return "" endfunc func! s:doop_d(str, pos, rep) let @@ = a:str call s:undo.add(1, "d", a:pos, a:str) call setcmdpos(a:pos + 1) return "" endfunc func! s:doop_y(str, pos, ...) let @@ = a:str call setcmdpos(a:pos + 1) return a:str endfunc " Insert: {{{1 func! s:doop_c(str, pos, rep) if s:beep && !s:from_mapping return a:str endif let @@ = a:str if !a:rep if !s:from_mapping call histadd("@", a:str) call inputsave() let newtext = input("Change into:") call inputrestore() else let newtext = input("", a:str) endif let s:lastitext = newtext if s:recording call s:rec_chars(s:count1, s:rec_op_c."".s:MapEscape(newtext,"v").":") endif else let newtext = s:lastitext endif if s:beep return a:str endif if a:str !=# newtext call s:undo.add(0, "d", a:pos, a:str) call s:undo.add(1, "i", a:pos, newtext) endif call setcmdpos(a:pos+1 + strlen(newtext)) return newtext endfunc func! insert(mode, cmd) if a:mode == 1 let cnt = s:getcount1() let s:lastedit = ["insert", 0, a:cmd] let s:lastcount = cnt if !s:from_mapping call inputsave() let newtext = input(a:cmd==?"a" ? "Append:" : "Insert:") call inputrestore() else let newtext = input("") endif let s:lastitext = newtext if s:recording call s:rec_chars(cnt, a:cmd. "&". s:MapEscape(newtext,"v"). ":") " faced a crash without (eat) (and mapesc) endif else let cnt = s:lastcount let newtext = s:lastitext endif let cmdl = getcmdline() if newtext != "" || a:cmd ==# "I" if a:cmd ==# "I" let iwhite = matchstr(cmdl, '^[ \t:]*') if iwhite == "" && newtext == "" return cmdl endif let gcp = 0 call s:undo.add(0, "d", gcp, iwhite) let cmdl = strpart(cmdl, strlen(iwhite)) elseif a:cmd ==# "a" let gcp = matchend(cmdl, '^.\=', getcmdpos()-1) elseif a:cmd ==# "A" let gcp = strlen(cmdl) else let gcp = getcmdpos()-1 endif let resulttext = repeat(newtext, cnt) call s:undo.add(1, "i", gcp, resulttext) call setcmdpos(gcp+1 + strlen(resulttext)) return strpart(cmdl, 0, gcp). resulttext. strpart(cmdl, gcp) else return cmdl endif endfunc " Opend: {{{1 func! opend(motion, ...) let motion = a:motion if a:0 == 0 let s:count1 = s:getcount1() let s:lastedit = ["opend", motion, 0] let s:lastcount = s:count1 let isrep = 0 if s:recording if s:operator == "c" " just without trailing ":" let mot = get(s:cmdrev, a:motion, a:motion) let s:rec_op_c = "&ocon".mot."" else call s:rec_chars(s:count1, a:motion) endif endif elseif a:1 == 1 " zap motion, a:0 == 2 let s:count1 = s:getcount1() let s:lastedit = ["opend", a:2, 0] let s:lastcount = s:count1 let s:zapmode = "o" let isrep = 0 else " e.g. a:1 == 0 let s:count1 = s:lastcount let isrep = 1 endif let s:incloff = 0 let gcp = getcmdpos()-1 " cw,cW -> ce,cE (not on white space) if s:operator == "c" && motion ==? "w" \ && getcmdline()[gcp] =~ '\S' let motion = tr(motion, "wW", "eE") elseif motion == '_' " special case, text object for a line let gcp = 0 let tarpos = s:getpos_dollar() else let tarpos = s:getpos_{motion}() endif " only exclusive "motions" let cmdl = getcmdline() if gcp < tarpos let [pos1, pos2] = [gcp, tarpos+s:incloff] elseif tarpos < gcp let [pos1, pos2] = [tarpos, gcp+s:incloff] elseif s:operator == "c" " op c must accept everything to always eat ^U and ^M from rec let [pos1, pos2] = [gcp, gcp+s:incloff] else return cmdl endif let cmdlpart = strpart(cmdl, pos1, pos2-pos1) let newpart = s:doop_{s:operator}(cmdlpart, pos1, isrep) return strpart(cmdl,0,pos1). newpart. cmdl[pos2 :] endfunc " Repeat: {{{1 func! edit_dot() let cnt = s:getcount() call s:rec_chars(cnt, ".") if exists("s:lastedit") if cnt > 0 let s:lastcount = cnt endif return call("s:".s:lastedit[0], s:lastedit[1:]) else return getcmdline() endif endfunc func! macro_rec() let s:counta = "" let s:countb = "" cmap :0 zero if !s:recording let s:recbuf = "" let s:recording = 1 " call s:undo.mac_begin() call s:Warn("START recording") else " call s:undo.mac_end() let s:recording = 0 let g:CONOMODE_RECBUF = s:recbuf call s:Warn("STOP recording") endif return "" endfunc " execute macro: duplicate macro count times, size limit=1000 func! macro_exec() if s:recording call s:undo.mac_end() let s:recording = 0 let g:CONOMODE_RECBUF = s:recbuf endif let cnt = s:getcount1() if s:recbuf != "" let reclen = strlen(s:recbuf) if reclen * cnt > 1000 let cnt = max([1000 / reclen, 1]) endif exec "cnoremap