Test page for lang-vim.js

Download: lang-vim.js

" jptemplate.vim:
"
" A simple yet powerful interactive templating system for VIM.
"
" Version 1.5 (released 2008-07-08).
"
" Copyright (c) 2008 Jannis Pohlmann <jannis@xfce.org>.
"
" This program is free software; you can redistribute it and/or modify
" it under the terms of the GNU General Public License as published by
" the Free Software Foundation; either version 2 of the License, or (at
" your option) any later version.
"
" This program is distributed in the hope that it will be useful, but
" WITHOUT ANY WARRANTY; without even the implied warranty of
" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
" General Public License for more details.
"
" You should have received a copy of the GNU General Public License
" along with this program; if not, write to the Free Software
" Foundation, Inc., 59 Temple Place, Suite 330, Boston,
" MA  02111-1307  USA


" Reserved variable names
let s:reservedVariables = ['date','shell','interactive_shell']

" Variable value history
let s:rememberedValues = {}

" Characters to be escaped before the substitute() call
let s:escapeCharacters = '&~\'


function! jp:Initialize()
  " List for default configuration
  let defaults = []

  " Default configuration values
  call add(defaults, ['g:jpTemplateDir', $HOME . '/.vim/jptemplate'])
  call add(defaults, ['g:jpTemplateKey', '<C-Tab>'])
  call add(defaults, ['g:jpTemplateDateFormat', '%Y-%m-%d'])
  call add(defaults, ['g:jpTemplateDefaults', {}])
  call add(defaults, ['g:jpTemplateVerbose', 0])

  " Set default configuration for non-existent variables
  for var in filter(defaults, '!exists(v:val[0])')
    exec 'let ' . var[0] . ' = ' . string(var[1])
  endfor

endfunction


function! jp:GetTemplateInfo()

  " Prepare info dictionary
  let info = {}

  " Get part of the line before the cursor
  let part = getline('.')[0 : getpos('.')[2]-1]

  " Get start and end position of the template name
  let info['start'] = match(part, '\(\w*\)$')
  let info['end']   = matchend(part, '\(\w*\)$')

  " Get template name
  let info['name']  = part[info['start'] : info['end']]

  " Throw exception if no template name could be found
  if empty(info['name'])
    throw 'No template name found at cursor'
  endif

  " Determine directory to load the template from (skip empty directories;
  " each directory may override the ones before)
  for dir in filter([ 'general', &ft ], '!empty(v:val)')
    let filename = g:jpTemplateDir .'/'. dir .'/'. info['name']
    if filereadable(filename)
      let info['filename'] = filename
    endif
  endfor

  " Throw exception if the template file does not exist in any of these
  " directories (or is not readable)
  if !has_key(info, 'filename')
    throw 'Template file not found'
  endif

  " Determine indentation
  let info['indent'] = matchstr(part, '^\s\+')

  " Return template name information
  return info

endfunction


function! jp:ReadTemplate(name, filename)

  " Try to read the template file and throw exception if that fails
  try
    return readfile(a:filename)
  catch
    throw 'Template "' . a:name . '" could not be found.'
  endtry

endfunction


function! jp:UpdateCursorPosition(lines, endColumn)

  " Define cursorPosition as the column to which the cursor is moved
  let cursorPosition = -1

  for cnt in range(0, a:lines)
    " Search for ${cursor} in the current line
    let str    = getline(line('.') + cnt)
    let start  = match(str, '${cursor}')
    let end    = matchend(str, '${cursor}')
    let before = strpart(str, 0, start)
    let after  = strpart(str, end)

    if start >= 0
      " Remove ${cursor} and move the cursor to the desired position
      call setline(line('.') + cnt, before . after)
      call cursor(line('.') + cnt, start+1)

      let cursorPosition = start

      " We're done
      break
    endif
  endfor

  " Update cursor position in case no ${cursor} was found in the template
  " and the resulting template was not empty
  if cursorPosition == -1 && a:lines >= 0
    if a:lines == 0
      call cursor(line('.'), a:endColumn + 1)
    else
      call cursor(line('.') + a:lines, a:endColumn + 1)
    endif
  endif

  " Return to insert mode (distinguish between 'in the middle of the line' and
  " 'at the end of the line')
  if col('.') == len(getline('.')) 
    startinsert!
  else
    startinsert
  endif

endfunction


function! jp:ParseExpression(expr)

  " Determine position of the separator between name and value
  let valuepos = match(a:expr, ':')

  " Extract name and value strings
  let name  = valuepos >= 0 ? strpart(a:expr, 0, valuepos) : a:expr
  let value = valuepos >= 0 ? strpart(a:expr, valuepos + 1) : ''

  " Return list with both strings
  return [name, value]

endfunction


function! jp:EvaluateReservedVariable(name, value, variables)

  let result = ''

  if a:name == 'date'
    let result = strftime(empty(a:value) ? g:jpTemplateDateFormat : a:value)
  elseif a:name == 'shell'
    if !empty(a:value)
      let result = system(a:value)
    endif
  elseif a:name == 'interactive_shell'
    let command = input('interactive_shell: ', a:value)
    if !empty(command)
      let result = system(command)
    endif
  endif

  return result

endfunction


function! jp:ExpandTemplate(info, template)

  " Backup content before and after the template name
  let before = strpart(getline('.'), 0, a:info['start'])
  let after  = strpart(getline('.'), a:info['end'])

  " Merge lines of the template and then split them up again.
  " This makes multi-line variable values possible
  let mergedTemplate = split(join(a:template, "\n"), "\n")

  " Define cnt as the number of inserted lines
  let cnt = 0

  " Remove template string if the resulting template is empty
  if empty(mergedTemplate)
    call setline(line('.'), before . after)
    call cursor(line('.'), len(before) + 1)
    let cnt = -1
  else
    " Insert template between before and after
    for cnt in range(0, len(mergedTemplate) - 1)
      if cnt == 0
        call setline(line('.'), before . mergedTemplate[cnt])
      else
        call append(line('.') + cnt - 1, a:info['indent'] . mergedTemplate[cnt])
      endif
      if cnt == len(mergedTemplate) - 1
        call setline(line('.') + cnt, getline(line('.') + cnt) . after)
      endif
    endfor
  endif

  " Define start and end columns of the template
  let startColumn = len(before)
  if empty(mergedTemplate)
    let endColumn = startColumn
  else
    if cnt == 0
      let endColumn = startColumn + len(mergedTemplate[0])
    else
      let endColumn = len(a:info['indent'] . mergedTemplate[len(mergedTemplate) - 1])
    endif
  endif

  " Return number of inserted lines, start and end columns
  return [cnt, startColumn, endColumn]

endfunction


function! jp:ProcessTemplate(info, template)

  let matchpos    = 0
  let expressions = []
  let variables   = {}
  let reserved    = {}

  " Make a string out of the template lines
  let s:str = join(a:template, ' ')

  " Detect all variable names of the template
  while 1
    " Find next variable start and end position
    let start = match(s:str, '${[^{}]\+}', matchpos)
    let end   = matchend(s:str, '${[^{}]\+}', matchpos)

    if start < 0
      " Stop search if there is no variable left
      break
    else
      " Extract variable expression (remove '${' and '}')
      let expr = s:str[start+2 : end-2]

      " Extract variable name and default value */
      let [name, value] = jp:ParseExpression(expr)

      if name == 'cursor'
        " Skip the ${cursor} variable
      elseif count(s:reservedVariables, name) > 0
        let reserved[expr] = ''
      else
        " Only insert variables on their first appearance
        if !has_key(variables, name)
          " Add expression to the expression list
          call add(expressions, expr)

          " Set variable value to ''
          let variables[name] = ''
        endif

        " Check whether local default value is defined or not
        if empty(value)
          " If not, check if variable value is empty
          if empty(variables[name])
            " If so, either set it to the last remembered value or the global
            " default if it exists
            if has_key(s:rememberedValues, name)
              let variables[name] = s:rememberedValues[name]
            elseif has_key(g:jpTemplateDefaults, name)
              let variables[name] = g:jpTemplateDefaults[name]
            endif
          endif
        else
          " Use local default(first occurence in the template only)
          let variables[name] = value
        endif
      endif

      " Start next search at the end position of this expression
      let matchpos = end
    endif
  endwhile

  " Ask the user to enter values for all variables
  for expr in expressions
    let [name, value] = jp:ParseExpression(expr)
    let variables[name] = input(name . ': ', variables[name])
    let s:rememberedValues[name] = variables[name]
  endfor

  " Evaluate reserved variables
  for expr in keys(reserved)
    let [name, value] = jp:ParseExpression(expr)
    let replacement = jp:EvaluateReservedVariable(name, value, variables)
    let reserved[expr] = replacement
  endfor

  " Expand all variables (custom and reserved)
  for index in range(len(a:template))
    for expr in expressions
      let [name, value] = jp:ParseExpression(expr)
      let expr = '${' . name . '\(:[^{}]\+\)\?}'
      let value = escape(variables[name], s:escapeCharacters)
      let a:template[index] = substitute(a:template[index], expr, value, 'g')
    endfor

    for [expr, value] in items(reserved)
      let expr = '${' . expr . '}'
      let value = escape(value, s:escapeCharacters)
      let a:template[index] = substitute(a:template[index], expr, value, 'g')
    endfor
  endfor

  " Insert template into the code line by line
  let [insertedLines, startColumn, endColumn] = jp:ExpandTemplate(a:info, a:template)

  " Update the cursor position and return to insert mode 
  call jp:UpdateCursorPosition(insertedLines, endColumn)

endfunction


function! jp:InsertTemplate()

  try
    " Detect bounds of the template name as well as the name itself
    let info = jp:GetTemplateInfo()

    " Load the template file
    let template = jp:ReadTemplate(info['name'], info['filename'])

    " Do the hard work: Process the template
    call jp:ProcessTemplate(info, template)
  catch
    " Inform the user about errors
    echo g:jpTemplateVerbose ? v:exception . " (in " . v:throwpoint . ")" : v:exception
  endtry

endfunction


" Initialize jptemplate configuration
call jp:Initialize()

" Map keyboard shortcut to the template system
exec 'imap ' . g:jpTemplateKey . ' <Esc>:call jp:InsertTemplate()<CR>'

This test Vim script code is jptemplate.