samer@3: " File: imaps.vim samer@3: " Authors: Srinath Avadhanula samer@3: " Benji Fisher samer@3: " samer@3: " WWW: http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/vim-latex/vimfiles/plugin/imaps.vim?only_with_tag=MAIN samer@3: " samer@3: " Description: insert mode template expander with cursor placement samer@3: " while preserving filetype indentation. samer@3: " samer@3: " $Id: imaps.vim 997 2006-03-20 09:45:45Z srinathava $ samer@3: " samer@3: " Documentation: {{{ samer@3: " samer@3: " Motivation: samer@3: " this script provides a way to generate insert mode mappings which do not samer@3: " suffer from some of the problem of mappings and abbreviations while allowing samer@3: " cursor placement after the expansion. It can alternatively be thought of as samer@3: " a template expander. samer@3: " samer@3: " Consider an example. If you do samer@3: " samer@3: " imap lhs something samer@3: " samer@3: " then a mapping is set up. However, there will be the following problems: samer@3: " 1. the 'ttimeout' option will generally limit how easily you can type the samer@3: " lhs. if you type the left hand side too slowly, then the mapping will not samer@3: " be activated. samer@3: " 2. if you mistype one of the letters of the lhs, then the mapping is samer@3: " deactivated as soon as you backspace to correct the mistake. samer@3: " samer@3: " If, in order to take care of the above problems, you do instead samer@3: " samer@3: " iab lhs something samer@3: " samer@3: " then the timeout problem is solved and so is the problem of mistyping. samer@3: " however, abbreviations are only expanded after typing a non-word character. samer@3: " which causes problems of cursor placement after the expansion and invariably samer@3: " spurious spaces are inserted. samer@3: " samer@3: " Usage Example: samer@3: " this script attempts to solve all these problems by providing an emulation samer@3: " of imaps wchich does not suffer from its attendant problems. Because maps samer@3: " are activated without having to press additional characters, therefore samer@3: " cursor placement is possible. furthermore, file-type specific indentation is samer@3: " preserved, because the rhs is expanded as if the rhs is typed in literally samer@3: " by the user. samer@3: " samer@3: " The script already provides some default mappings. each "mapping" is of the samer@3: " form: samer@3: " samer@3: " call IMAP (lhs, rhs, ft) samer@3: " samer@3: " Some characters in the RHS have special meaning which help in cursor samer@3: " placement. samer@3: " samer@3: " Example One: samer@3: " samer@3: " call IMAP ("bit`", "\\begin{itemize}\\\item <++>\\\end{itemize}<++>", "tex") samer@3: " samer@3: " This effectively sets up the map for "bit`" whenever you edit a latex file. samer@3: " When you type in this sequence of letters, the following text is inserted: samer@3: " samer@3: " \begin{itemize} samer@3: " \item * samer@3: " \end{itemize}<++> samer@3: " samer@3: " where * shows the cursor position. The cursor position after inserting the samer@3: " text is decided by the position of the first "place-holder". Place holders samer@3: " are special characters which decide cursor placement and movement. In the samer@3: " example above, the place holder characters are <+ and +>. After you have typed samer@3: " in the item, press and you will be taken to the next set of <++>'s. samer@3: " Therefore by placing the <++> characters appropriately, you can minimize the samer@3: " use of movement keys. samer@3: " samer@3: " NOTE: Set g:Imap_UsePlaceHolders to 0 to disable placeholders altogether. samer@3: " Set samer@3: " g:Imap_PlaceHolderStart and g:Imap_PlaceHolderEnd samer@3: " to something else if you want different place holder characters. samer@3: " Also, b:Imap_PlaceHolderStart and b:Imap_PlaceHolderEnd override the values samer@3: " of g:Imap_PlaceHolderStart and g:Imap_PlaceHolderEnd respectively. This is samer@3: " useful for setting buffer specific place hoders. samer@3: " samer@3: " Example Two: samer@3: " You can use the command to insert dynamic elements such as dates. samer@3: " call IMAP ('date`', "\=strftime('%b %d %Y')\", '') samer@3: " samer@3: " sets up the map for date` to insert the current date. samer@3: " samer@3: "--------------------------------------%<-------------------------------------- samer@3: " Bonus: This script also provides a command Snip which puts tearoff strings, samer@3: " '----%<----' above and below the visually selected range of lines. The samer@3: " length of the string is chosen to be equal to the longest line in the range. samer@3: " Recommended Usage: samer@3: " '<,'>Snip samer@3: "--------------------------------------%<-------------------------------------- samer@3: " }}} samer@3: samer@3: " line continuation used here. samer@3: let s:save_cpo = &cpo samer@3: set cpo&vim samer@3: samer@3: " ============================================================================== samer@3: " Script Options / Variables samer@3: " ============================================================================== samer@3: " Options {{{ samer@3: if !exists('g:Imap_StickyPlaceHolders') samer@3: let g:Imap_StickyPlaceHolders = 1 samer@3: endif samer@3: if !exists('g:Imap_DeleteEmptyPlaceHolders') samer@3: let g:Imap_DeleteEmptyPlaceHolders = 1 samer@3: endif samer@3: " }}} samer@3: " Variables {{{ samer@3: " s:LHS_{ft}_{char} will be generated automatically. It will look like samer@3: " s:LHS_tex_o = 'fo\|foo\|boo' and contain all mapped sequences ending in "o". samer@3: " s:Map_{ft}_{lhs} will be generated automatically. It will look like samer@3: " s:Map_c_foo = 'for(<++>; <++>; <++>)', the mapping for "foo". samer@3: " samer@3: " }}} samer@3: samer@3: " ============================================================================== samer@3: " functions for easy insert mode mappings. samer@3: " ============================================================================== samer@3: " IMAP: Adds a "fake" insert mode mapping. {{{ samer@3: " For example, doing samer@3: " IMAP('abc', 'def' ft) samer@3: " will mean that if the letters abc are pressed in insert mode, then samer@3: " they will be replaced by def. If ft != '', then the "mapping" will be samer@3: " specific to the files of type ft. samer@3: " samer@3: " Using IMAP has a few advantages over simply doing: samer@3: " imap abc def samer@3: " 1. with imap, if you begin typing abc, the cursor will not advance and samer@3: " long as there is a possible completion, the letters a, b, c will be samer@3: " displayed on on top of the other. using this function avoids that. samer@3: " 2. with imap, if a backspace or arrow key is pressed before completing samer@3: " the word, then the mapping is lost. this function allows movement. samer@3: " (this ofcourse means that this function is only limited to samer@3: " left-hand-sides which do not have movement keys or unprintable samer@3: " characters) samer@3: " It works by only mapping the last character of the left-hand side. samer@3: " when this character is typed in, then a reverse lookup is done and if samer@3: " the previous characters consititute the left hand side of the mapping, samer@3: " the previously typed characters and erased and the right hand side is samer@3: " inserted samer@3: samer@3: " IMAP: set up a filetype specific mapping. samer@3: " Description: samer@3: " "maps" the lhs to rhs in files of type 'ft'. If supplied with 2 samer@3: " additional arguments, then those are assumed to be the placeholder samer@3: " characters in rhs. If unspecified, then the placeholder characters samer@3: " are assumed to be '<+' and '+>' These placeholder characters in samer@3: " a:rhs are replaced with the users setting of samer@3: " [bg]:Imap_PlaceHolderStart and [bg]:Imap_PlaceHolderEnd settings. samer@3: " samer@3: function! IMAP(lhs, rhs, ft, ...) samer@3: samer@3: " Find the place holders to save for IMAP_PutTextWithMovement() . samer@3: if a:0 < 2 samer@3: let phs = '<+' samer@3: let phe = '+>' samer@3: else samer@3: let phs = a:1 samer@3: let phe = a:2 samer@3: endif samer@3: samer@3: let hash = s:Hash(a:lhs) samer@3: let s:Map_{a:ft}_{hash} = a:rhs samer@3: let s:phs_{a:ft}_{hash} = phs samer@3: let s:phe_{a:ft}_{hash} = phe samer@3: samer@3: " Add a:lhs to the list of left-hand sides that end with lastLHSChar: samer@3: let lastLHSChar = a:lhs[strlen(a:lhs)-1] samer@3: let hash = s:Hash(lastLHSChar) samer@3: if !exists("s:LHS_" . a:ft . "_" . hash) samer@3: let s:LHS_{a:ft}_{hash} = escape(a:lhs, '\') samer@3: else samer@3: let s:LHS_{a:ft}_{hash} = escape(a:lhs, '\') .'\|'. s:LHS_{a:ft}_{hash} samer@3: endif samer@3: samer@3: " map only the last character of the left-hand side. samer@3: if lastLHSChar == ' ' samer@3: let lastLHSChar = '' samer@3: end samer@3: exe 'inoremap ' samer@3: \ escape(lastLHSChar, '|') samer@3: \ '=LookupCharacter("' . samer@3: \ escape(lastLHSChar, '\|"') . samer@3: \ '")' samer@3: endfunction samer@3: samer@3: " }}} samer@3: " IMAP_list: list the rhs and place holders corresponding to a:lhs {{{ samer@3: " samer@3: " Added mainly for debugging purposes, but maybe worth keeping. samer@3: function! IMAP_list(lhs) samer@3: let char = a:lhs[strlen(a:lhs)-1] samer@3: let charHash = s:Hash(char) samer@3: if exists("s:LHS_" . &ft ."_". charHash) && a:lhs =~ s:LHS_{&ft}_{charHash} samer@3: let ft = &ft samer@3: elseif exists("s:LHS__" . charHash) && a:lhs =~ s:LHS__{charHash} samer@3: let ft = "" samer@3: else samer@3: return "" samer@3: endif samer@3: let hash = s:Hash(a:lhs) samer@3: return "rhs = " . s:Map_{ft}_{hash} . " place holders = " . samer@3: \ s:phs_{ft}_{hash} . " and " . s:phe_{ft}_{hash} samer@3: endfunction samer@3: " }}} samer@3: " LookupCharacter: inserts mapping corresponding to this character {{{ samer@3: " samer@3: " This function extracts from s:LHS_{&ft}_{a:char} or s:LHS__{a:char} samer@3: " the longest lhs matching the current text. Then it replaces lhs with the samer@3: " corresponding rhs saved in s:Map_{ft}_{lhs} . samer@3: " The place-holder variables are passed to IMAP_PutTextWithMovement() . samer@3: function! s:LookupCharacter(char) samer@3: if IMAP_GetVal('Imap_FreezeImap', 0) == 1 samer@3: return a:char samer@3: endif samer@3: let charHash = s:Hash(a:char) samer@3: samer@3: " The line so far, including the character that triggered this function: samer@3: let text = strpart(getline("."), 0, col(".")-1) . a:char samer@3: " Prefer a local map to a global one, even if the local map is shorter. samer@3: " Is this what we want? Do we care? samer@3: " Use '\V' (very no-magic) so that only '\' is special, and it was already samer@3: " escaped when building up s:LHS_{&ft}_{charHash} . samer@3: if exists("s:LHS_" . &ft . "_" . charHash) samer@3: \ && text =~ "\\C\\V\\(" . s:LHS_{&ft}_{charHash} . "\\)\\$" samer@3: let ft = &ft samer@3: elseif exists("s:LHS__" . charHash) samer@3: \ && text =~ "\\C\\V\\(" . s:LHS__{charHash} . "\\)\\$" samer@3: let ft = "" samer@3: else samer@3: " If this is a character which could have been used to trigger an samer@3: " abbreviation, check if an abbreviation exists. samer@3: if a:char !~ '\k' samer@3: let lastword = matchstr(getline('.'), '\k\+$', '') samer@3: call IMAP_Debug('getting lastword = ['.lastword.']', 'imap') samer@3: if lastword != '' samer@3: " An extremeley wierd way to get around the fact that vim samer@3: " doesn't have the equivalent of the :mapcheck() function for samer@3: " abbreviations. samer@3: let _a = @a samer@3: exec "redir @a | silent! iab ".lastword." | redir END" samer@3: let abbreviationRHS = matchstr(@a."\n", "\n".'i\s\+'.lastword.'\s\+@\?\zs.*\ze'."\n") samer@3: samer@3: call IMAP_Debug('getting abbreviationRHS = ['.abbreviationRHS.']', 'imap') samer@3: samer@3: if @a =~ "No abbreviation found" || abbreviationRHS == "" samer@3: let @a = _a samer@3: return a:char samer@3: endif samer@3: samer@3: let @a = _a samer@3: let abbreviationRHS = escape(abbreviationRHS, '\<"') samer@3: exec 'let abbreviationRHS = "'.abbreviationRHS.'"' samer@3: samer@3: let lhs = lastword.a:char samer@3: let rhs = abbreviationRHS.a:char samer@3: let phs = IMAP_GetPlaceHolderStart() samer@3: let phe = IMAP_GetPlaceHolderEnd() samer@3: else samer@3: return a:char samer@3: endif samer@3: else samer@3: return a:char samer@3: endif samer@3: endif samer@3: " Find the longest left-hand side that matches the line so far. samer@3: " matchstr() returns the match that starts first. This automatically samer@3: " ensures that the longest LHS is used for the mapping. samer@3: if !exists('lhs') || !exists('rhs') samer@3: let lhs = matchstr(text, "\\C\\V\\(" . s:LHS_{ft}_{charHash} . "\\)\\$") samer@3: let hash = s:Hash(lhs) samer@3: let rhs = s:Map_{ft}_{hash} samer@3: let phs = s:phs_{ft}_{hash} samer@3: let phe = s:phe_{ft}_{hash} samer@3: endif samer@3: samer@3: if strlen(lhs) == 0 samer@3: return a:char samer@3: endif samer@3: " enough back-spaces to erase the left-hand side; -1 for the last samer@3: " character typed: samer@3: let bs = substitute(strpart(lhs, 1), ".", "\", "g") samer@3: return bs . IMAP_PutTextWithMovement(rhs, phs, phe) samer@3: endfunction samer@3: samer@3: " }}} samer@3: " IMAP_PutTextWithMovement: returns the string with movement appended {{{ samer@3: " Description: samer@3: " If a:str contains "placeholders", then appends movement commands to samer@3: " str in a way that the user moves to the first placeholder and enters samer@3: " insert or select mode. If supplied with 2 additional arguments, then samer@3: " they are assumed to be the placeholder specs. Otherwise, they are samer@3: " assumed to be '<+' and '+>'. These placeholder chars are replaced samer@3: " with the users settings of [bg]:Imap_PlaceHolderStart and samer@3: " [bg]:Imap_PlaceHolderEnd. samer@3: function! IMAP_PutTextWithMovement(str, ...) samer@3: samer@3: " The placeholders used in the particular input string. These can be samer@3: " different from what the user wants to use. samer@3: if a:0 < 2 samer@3: let phs = '<+' samer@3: let phe = '+>' samer@3: else samer@3: let phs = escape(a:1, '\') samer@3: let phe = escape(a:2, '\') samer@3: endif samer@3: samer@3: let text = a:str samer@3: samer@3: " The user's placeholder settings. samer@3: let phsUser = IMAP_GetPlaceHolderStart() samer@3: let pheUser = IMAP_GetPlaceHolderEnd() samer@3: samer@3: " Problem: depending on the setting of the 'encoding' option, a character samer@3: " such as "\xab" may not match itself. We try to get around this by samer@3: " changing the encoding of all our strings. At the end, we have to samer@3: " convert text back. samer@3: let phsEnc = s:Iconv(phs, "encode") samer@3: let pheEnc = s:Iconv(phe, "encode") samer@3: let phsUserEnc = s:Iconv(phsUser, "encode") samer@3: let pheUserEnc = s:Iconv(pheUser, "encode") samer@3: let textEnc = s:Iconv(text, "encode") samer@3: if textEnc != text samer@3: let textEncoded = 1 samer@3: else samer@3: let textEncoded = 0 samer@3: endif samer@3: samer@3: let pattern = '\V\(\.\{-}\)' .phs. '\(\.\{-}\)' .phe. '\(\.\*\)' samer@3: " If there are no placeholders, just return the text. samer@3: if textEnc !~ pattern samer@3: call IMAP_Debug('Not getting '.phs.' and '.phe.' in '.textEnc, 'imap') samer@3: return text samer@3: endif samer@3: " Break text up into "initial <+template+> final"; any piece may be empty. samer@3: let initialEnc = substitute(textEnc, pattern, '\1', '') samer@3: let templateEnc = substitute(textEnc, pattern, '\2', '') samer@3: let finalEnc = substitute(textEnc, pattern, '\3', '') samer@3: samer@3: " If the user does not want to use placeholders, then remove all but the samer@3: " first placeholder. samer@3: " Otherwise, replace all occurences of the placeholders here with the samer@3: " user's choice of placeholder settings. samer@3: if exists('g:Imap_UsePlaceHolders') && !g:Imap_UsePlaceHolders samer@3: let finalEnc = substitute(finalEnc, '\V'.phs.'\.\{-}'.phe, '', 'g') samer@3: else samer@3: let finalEnc = substitute(finalEnc, '\V'.phs.'\(\.\{-}\)'.phe, samer@3: \ phsUserEnc.'\1'.pheUserEnc, 'g') samer@3: endif samer@3: samer@3: " The substitutions are done, so convert back, if necessary. samer@3: if textEncoded samer@3: let initial = s:Iconv(initialEnc, "decode") samer@3: let template = s:Iconv(templateEnc, "decode") samer@3: let final = s:Iconv(finalEnc, "decode") samer@3: else samer@3: let initial = initialEnc samer@3: let template = templateEnc samer@3: let final = finalEnc samer@3: endif samer@3: samer@3: " Build up the text to insert: samer@3: " 1. the initial text plus an extra character; samer@3: " 2. go to Normal mode with , so it works even if 'insertmode' samer@3: " is set, and mark the position; samer@3: " 3. replace the extra character with tamplate and final; samer@3: " 4. back to Normal mode and restore the cursor position; samer@3: " 5. call IMAP_Jumpfunc(). samer@3: let template = phsUser . template . pheUser samer@3: " Old trick: insert and delete a character to get the same behavior at samer@3: " start, middle, or end of line and on empty lines. samer@3: let text = initial . "X\\:call IMAP_Mark('set')\\"_s" samer@3: let text = text . template . final samer@3: let text = text . "\\:call IMAP_Mark('go')\" samer@3: let text = text . "i\=IMAP_Jumpfunc('', 1)\" samer@3: samer@3: call IMAP_Debug('IMAP_PutTextWithMovement: text = ['.text.']', 'imap') samer@3: return text samer@3: endfunction samer@3: samer@3: " }}} samer@3: " IMAP_Jumpfunc: takes user to next <+place-holder+> {{{ samer@3: " Author: Luc Hermitte samer@3: " Arguments: samer@3: " direction: flag for the search() function. If set to '', search forwards, samer@3: " if 'b', then search backwards. See the {flags} argument of the samer@3: " |search()| function for valid values. samer@3: " inclusive: In vim, the search() function is 'exclusive', i.e we always goto samer@3: " next cursor match even if there is a match starting from the samer@3: " current cursor position. Setting this argument to 1 makes samer@3: " IMAP_Jumpfunc() also respect a match at the current cursor samer@3: " position. 'inclusive'ness is necessary for IMAP() because a samer@3: " placeholder string can occur at the very beginning of a map which samer@3: " we want to select. samer@3: " We use a non-zero value only in special conditions. Most mappings samer@3: " should use a zero value. samer@3: function! IMAP_Jumpfunc(direction, inclusive) samer@3: samer@3: " The user's placeholder settings. samer@3: let phsUser = IMAP_GetPlaceHolderStart() samer@3: let pheUser = IMAP_GetPlaceHolderEnd() samer@3: samer@3: let searchString = '' samer@3: " If this is not an inclusive search or if it is inclusive, but the samer@3: " current cursor position does not contain a placeholder character, then samer@3: " search for the placeholder characters. samer@3: if !a:inclusive || strpart(getline('.'), col('.')-1) !~ '\V\^'.phsUser samer@3: let searchString = '\V'.phsUser.'\_.\{-}'.pheUser samer@3: endif samer@3: samer@3: " If we didn't find any placeholders return quietly. samer@3: if searchString != '' && !search(searchString, a:direction) samer@3: return '' samer@3: endif samer@3: samer@3: " Open any closed folds and make this part of the text visible. samer@3: silent! foldopen! samer@3: samer@3: " Calculate if we have an empty placeholder or if it contains some samer@3: " description. samer@3: let template = samer@3: \ matchstr(strpart(getline('.'), col('.')-1), samer@3: \ '\V\^'.phsUser.'\zs\.\{-}\ze\('.pheUser.'\|\$\)') samer@3: let placeHolderEmpty = !strlen(template) samer@3: samer@3: " If we are selecting in exclusive mode, then we need to move one step to samer@3: " the right samer@3: let extramove = '' samer@3: if &selection == 'exclusive' samer@3: let extramove = 'l' samer@3: endif samer@3: samer@3: " Select till the end placeholder character. samer@3: let movement = "\v/\\V".pheUser."/e\".extramove samer@3: samer@3: " First remember what the search pattern was. s:RemoveLastHistoryItem will samer@3: " reset @/ to this pattern so we do not create new highlighting. samer@3: let g:Tex_LastSearchPattern = @/ samer@3: samer@3: " Now either goto insert mode or select mode. samer@3: if placeHolderEmpty && g:Imap_DeleteEmptyPlaceHolders samer@3: " delete the empty placeholder into the blackhole. samer@3: return movement."\"_c\:".s:RemoveLastHistoryItem."\" samer@3: else samer@3: return movement."\\:".s:RemoveLastHistoryItem."\gv\" samer@3: endif samer@3: samer@3: endfunction samer@3: samer@3: " }}} samer@3: " Maps for IMAP_Jumpfunc {{{ samer@3: " samer@3: " These mappings use and thus provide for easy user customization. When samer@3: " the user wants to map some other key to jump forward, he can do for samer@3: " instance: samer@3: " nmap ,f IMAP_JumpForward samer@3: " etc. samer@3: samer@3: " jumping forward and back in insert mode. samer@3: imap IMAP_JumpForward =IMAP_Jumpfunc('', 0) samer@3: imap IMAP_JumpBack =IMAP_Jumpfunc('b', 0) samer@3: samer@3: " jumping in normal mode samer@3: nmap IMAP_JumpForward i=IMAP_Jumpfunc('', 0) samer@3: nmap IMAP_JumpBack i=IMAP_Jumpfunc('b', 0) samer@3: samer@3: " deleting the present selection and then jumping forward. samer@3: vmap IMAP_DeleteAndJumpForward "_i=IMAP_Jumpfunc('', 0) samer@3: vmap IMAP_DeleteAndJumpBack "_i=IMAP_Jumpfunc('b', 0) samer@3: samer@3: " jumping forward without deleting present selection. samer@3: vmap IMAP_JumpForward i=IMAP_Jumpfunc('', 0) samer@3: vmap IMAP_JumpBack `=IMAP_Jumpfunc('b', 0) samer@3: samer@3: " }}} samer@3: " Default maps for IMAP_Jumpfunc {{{ samer@3: " map only if there is no mapping already. allows for user customization. samer@3: " NOTE: Default mappings for jumping to the previous placeholder are not samer@3: " provided. It is assumed that if the user will create such mappings samer@3: " hself if e so desires. samer@3: if !hasmapto('IMAP_JumpForward', 'i') samer@3: imap IMAP_JumpForward samer@3: endif samer@3: if !hasmapto('IMAP_JumpForward', 'n') samer@3: nmap IMAP_JumpForward samer@3: endif samer@3: if exists('g:Imap_StickyPlaceHolders') && g:Imap_StickyPlaceHolders samer@3: if !hasmapto('IMAP_JumpForward', 'v') samer@3: vmap IMAP_JumpForward samer@3: endif samer@3: else samer@3: if !hasmapto('IMAP_DeleteAndJumpForward', 'v') samer@3: vmap IMAP_DeleteAndJumpForward samer@3: endif samer@3: endif samer@3: " }}} samer@3: samer@3: nmap