" File:         cocoacomplete.vim (part of the cocoa.vim plugin)
" Author:       Michael Sanders (msanders42 [at] gmail [dot] com)
" Last Updated: June 30, 2009
" Description:  An omni-completion plugin for Cocoa/Objective-C.

let s:lib_dir = fnameescape($HOME.'/.vim/lib/')
let s:cocoa_indexes = s:lib_dir.'cocoa_indexes/'

if !isdirectory(s:cocoa_indexes)
	echom 'Error in cocoacomplete.vim: could not find ~/.vim/lib/cocoa_indexes directory'
endif

fun! objc#cocoacomplete#Complete(findstart, base)
	if a:findstart
		" Column where completion starts:
		return match(getline('.'), '\k\+\%'.col('.').'c')
	else
		let matches = []
		let complete_type = s:GetCompleteType(line('.'), col('.') - 1)

		if complete_type == 'methods'
			call s:Complete(a:base, ['alloc', 'init', 'retain', 'release',
			                       \ 'autorelease', 'retainCount',
			                       \ 'description', 'class', 'superclass',
			                       \ 'self', 'zone', 'isProxy', 'hash'])
			let obj_pos = s:GetObjPos(line('.'), col('.'))
			call extend(matches, s:CompleteMethod(line('.'), obj_pos, a:base))
		elseif complete_type == 'types' || complete_type == 'returntypes'
			let opt_types = complete_type == 'returntypes' ? ['IBAction'] : []
			call s:Complete(a:base, opt_types + ['void', 'id', 'BOOL', 'int',
			                                   \ 'double', 'float', 'char'])
			call extend(matches, s:CompleteCocoa(a:base, 'classes', 'types',
			                                           \ 'notifications'))
		elseif complete_type != ''
			if complete_type =~ 'function_params$'
				let complete_type = substitute(complete_type, 'function_params$', '', '')
				let functions = s:CompleteFunction(a:base)
			endif

			" Mimic vim's dot syntax for other complete types (see :h ft).
			let word = a:base == '' ? 'NS' : a:base
			let args = [word] + split(complete_type, '\.')
			call extend(matches, call('s:CompleteCocoa', args))

			" List functions after the other items in the menu.
			if exists('functions') | call extend(matches, functions) | endif
		endif
		return matches
	endif
endf

fun s:GetCompleteType(lnum, col)
	let scopelist = map(synstack(a:lnum, a:col), 'synIDattr(v:val, "name")')
	if empty(scopelist) | return 'types' | endif

	let current_scope = scopelist[-1]
	let beforeCursor = strpart(getline(a:lnum), 0, a:col)

	" Completing a function name:
	if getline(a:lnum) =~ '\%'.(a:col + 1).'c\s*('
		return 'functions'
	elseif current_scope == 'objcSuperclass'
		return 'classes'
	" Inside brackets "[ ... ]":
	elseif index(scopelist, 'objcMessage') != -1
		return beforeCursor =~ '\[\k*$' ? 'classes' : 'methods'
	" Inside parentheses "( ... )":
	elseif current_scope == 'cParen'
		" Inside parentheses for method definition:
		if beforeCursor =~ '^\s*[-+]\s*([^{;]*'
			return beforeCursor =~ '^\s*[-+]\s*([^)]*$' ? 'returntypes' : 'types'
		" Inside function, loop, or conditional:
		else
			return 'classes.types.constants.function_params'
		endif
	" Inside braces "{ ... }" or after equals "=":
	elseif current_scope == 'cBlock' || current_scope == 'objcAssign' || current_scope == ''
		let type = current_scope == 'cBlock' ? 'types.constants.' : ''
		let type = 'classes.'.type.'function_params'

		if beforeCursor =~ 'IBOutlet' | return 'classes' | endif
		return beforeCursor =~ '\v(^|[{};=\])]|return)\s*\k*$'? type : 'methods'
	" Directly inside "@implementation ... @end" or "@interface ... @end"
	elseif current_scope == 'objcImp' || current_scope  == 'objcHeader'
		" TODO: Complete delegate/subclass methods
	endif
	return ''
endf

" Adds item to the completion menu if they match the base.
fun s:Complete(base, items)
	for item in a:items
		if item =~ '^'.a:base | call complete_add(item) | endif
	endfor
endf

" Returns position of "foo" in "[foo bar]" or "[baz bar: [foo bar]]".
fun s:GetObjPos(lnum, col)
	let beforeCursor = strpart(getline(a:lnum), 0, a:col)
	return match(beforeCursor, '\v.*(^|[\[=;])\s*\[*\zs[A-Za-z0-9_@]+') + 1
endf

" Completes a method given the position of the object and the method
" being completed.
fun s:CompleteMethod(lnum, col, method)
	let class = s:GetCocoaClass(a:lnum, a:col)
	if class == ''
		let object = matchstr(getline(a:lnum), '\%'.a:col.'c\k\+')
		let class = s:GetDeclWord(object)
		if class == '' | return [] | endif
	endif
	let method = s:GetMethodName(a:lnum, a:col, a:method)
	let matches = split(system(s:lib_dir.'get_methods.sh '.class.
	                                   \ '|grep "^'.method.'"'), "\n")
	if exists('g:loaded_snips') " Use snipMate if it's installed
		call objc#pum_snippet#Map()
	else " Otherwise, only complete the method name.
		call map(matches, 'substitute(v:val, ''\v:\zs.{-}\ze(\w+:|$)'', " ", "g")')
	endif

	" If dealing with a partial method name, only complete past it. E.g., in
	" "[NSString stringWithCharacters:baz l|]" (where | is the cursor),
	" only return "length", not "stringWithCharacters:length:".
	if stridx(method, ':') != -1
		let method = substitute(method, a:method.'$', '\\\\zs&', '')
		call map(matches, 'matchstr(v:val, "'.method.'.*")')
	endif
	return matches
endf

" Returns the Cocoa class at a given position if it exists, or
" an empty string "" if it doesn't.
fun s:GetCocoaClass(lnum, col)
	let class = matchstr(getline(a:lnum), '\%'.a:col.'c[A-Za-z0-9_"@]\+')
	if class =~ '^@"' | return 'NSString' | endif " Treat @"..." as an NSString
	let v:errmsg = ''
	sil! hi cocoaClass
	if v:errmsg == '' && synIDattr(synID(a:lnum, a:col, 0), 'name') == 'cocoaClass'
		return class " If cocoaClass is defined, try using that.
	endif
	return system('grep ^'.class.' '.s:cocoa_indexes.'classes.txt') != ''
	              \ ? class : '' " Use grep as a fallback.
endf

" Returns the word before a variable declaration.
fun s:GetDeclWord(var)
	let startpos = [line('.'), col('.')]
	let line_found = searchdecl(a:var) != 0 ? 0 : line('.')
	call cursor(startpos)
	let matchstr = '\v(IBOutlet\s+)=\zs\k+\s*\ze\**\s*'

	" If the declaration was not found in the implementation file, check
	" the header.
	if !line_found && expand('%:e') == 'm'
		let header_path = expand('%:p:r').'.h'
		if filereadable(header_path)
			for line in readfile(header_path)
				if line =~ '^\s*\(IBOutlet\)\=\s*\k*\s*\ze\**\s*'.a:var.'\s*'
					return matchstr(line, matchstr)
				endif
			endfor
			return ''
		endif
	endif

	return matchstr(getline(line_found), matchstr.a:var)
endf

fun s:SearchList(list, regex)
	for line in a:list
		if line =~ a:regex
			return line
		endif
	endfor
	return ''
endf

" Returns the method name, ready to be searched by grep.
" The "base" word needs to be passed in separately, because
" Vim apparently removes it from the line during completions.
fun s:GetMethodName(lnum, col, base)
	let line = getline(a:lnum)
	let col = matchend(line, '\%'.a:col.'c\S\+\s\+') + 1 " Skip past class name.
	if line =~ '\%'.col.'c\k\+:'
		let base = a:base == '' ? '' : ' '.a:base
		let method = matchstr(line, '\%'.col.'c.\{-}\ze]').base
		return substitute(method, '\v\k+:\zs.{-}\ze(\s*\k+:|'.base.'$)', '[^:]*', 'g')
	else
		return a:base
	endif
endf

" Completes Cocoa functions, using snippets for the parameters if possible.
fun s:CompleteFunction(word)
	let files = s:cocoa_indexes.'functions.txt' " TODO: Add C functions.
	let matches = split(system('zgrep -h "^'.a:word.'" '.files), "\n")
	if exists('g:loaded_snips') " Use snipMate if it's installed
		call objc#pum_snippet#Map()
	else " Otherwise, just complete the function name
		call map(matches, "{'word':matchstr(v:val, '^\\k\\+'), 'abbr':v:val}")
	endif
	return matches
endf

" Completes word for Cocoa "classes", "types", "notifications", or "constants".
" (supplied as the optional parameters).
fun s:CompleteCocoa(word, file, ...)
	let files = ''
	for file in [a:file] + a:000
		let files .= ' '.s:cocoa_indexes.file.'.txt'
	endfor

	return split(system('grep -ho "^'.a:word.'[A-Za-z0-9_]*" '.files), "\n")
endf
" vim:noet:sw=4:ts=4:ft=vim
