Stwierdziłem niedawno, że brakuje mi w Vimie takiego drobiazgu jak wyświetlanie listy funkcji z edytowanego właśnie pliku. Można z jednej strony wykorzystać ctags, ale (pewien nie jestem, nie jestem znawcą tagsów ani ich obsługi w Vimie) po dodaniu funkcji/metody/czegokolwiek innego co można by nazwać tagiem trzeba by od nowa generować listę tagów. Nie jest to problemem takim bardzo dużym, do momentu gdy ciągle pracuje się nad różnymi projektami, z czego sporej części nie mam na dysku (tylko wykorzystując plugin netrw pracuję via ftp). W tym momencie moja znajomość systemu ctags podpowiada mi że robi się ciężko…

Postanowiłem więc napisać sobie prosty skrypt do tego, a że język wewnętrzny Vima mnie nieco odrzuca, postawiłem na Pythona ;) Żeby jakikolwiek skrypt pythona (jak ktoś lubi, można użyć też np perla) mógł zadziałać w Vimie, ten ostatni musi być skompilowany z jego obsługą. W Ubuntu wystarczy zainstalować pakiet vim-full. Łatwo sprawdzić czy Vim ma wkompilowaną obsługę pythona:

vim --version|gi '+python'

Jeśli wyświetliła Ci się podobna linijka:

+python +quickfix +reltime +rightleft +ruby +scrollbind +signs +smartindent

to znaczy że możesz śmiało używać pythona do oskryptowania Vima :)

Skrypty Pythona w Vimie przekazywane są do funkcji Vimowej o zaskakującej, nietypowej nazwie python. Tradycyjne Hello world wyglądałoby tak:

:python print "Hello world"

Interakcję z samym Vimem umożliwia nam moduł o kolejnej zaskakującej nazwie: vim, który powinniśmy zaimportować na początku funkcji/skryptu. Szczegółowa jego dokumentacja jest na stronach Vima: vim.org/htmldoc/if_pyth.html.

Pierwszym krokiem jest wczytanie zawartości całego bufora. Do jego zawartości dostajemy się poprzez obiekt vim.current.buffer:

fc = "\n".join(line for line in vim.current.buffer)

Samo wyszukanie deklaracji funkcji w plikach pythona można by zrobić mniej więcej tak:

re.findall(r'^\s*def\s+(\w+)', fc, re.M)

Jednak nam najwygodniej pobrać będzie jednocześnie z nazwą funkcji, także całą linijkę w której ta została zdefiniowana. Przyda to się przy przechodzeniu do miejsca w kodzie w którym funkcja jest zdefiniowana. Ja końcowo zrobiłem to tak:

re.findall(r'^(\s*def\s+(\w+\s*\(.*?\))\s*:)', fc, re.M)

Teraz, gdy mamy listę funkcji, trzeba je wyświetlić userowi, a następnie pobrać informację do deklaracji jakiej funkcji w kodzie chcemy się przenieść. Najpierw chciałem skorzystać ze standardowego pythonowego raw_input(), ale ta funkcja w Vimie nie działa. Użyłem wobec tego Vimowego input(). Aby skorzystać z funkcji Vimowych, moduł vim udostępnia nam dwie funkcje: vim.eval() i vim.command(). Tutaj odpowiedni będzie vim.eval():

choice = vim.eval('input("Which one: ")')

W tej chwili mamy już praktycznie napisany cały skrypt. Pozostało tylko znalezienie offsetu w jakim się zaczyna deklaracja wybranej przez usera funkcji w buforze, a następnie za pomocą vim.command() przenieść kursor w wybrane miejsce:

vim.command('goto '+str(pos+1))

Całość w moim wykonaniu jest nieco bardziej rozbudowana (minimalnie), jako że staram się zrobić sobie mały framework do skryptowania Vima. Wycinek realizujący odpowiednią funkcjonalnośc w moim przypadku wygląda tak:

" importy ogólne, przydatne funkcje, etc
python << EOF
import re
import sys

import vim

def MyDevPlugins_GetFileType(default=None):
	ft = vim.eval('synIDattr(synID(line("."), col("."), 1), "name")')
	if ft:
		m = re.findall(r'^([a-z]+)', ft)
		if m:
			return m[0]
	if not ft:
		ft = default

	return ft

EOF

" FindSubs - wyszukiwanie funkcji
python << EOF
FindSubs_ftypes = {
	'perl':		r'^(\s*sub\s+(\w+(?:\s*\(.*?\))?))',
	'python':	r'^(\s*def\s+(\w+\s*\(.*?\))\s*:)',
	'php':		r'^(\s*function\s+(\w+\s*\(.*?\)))',
}
FindSubs_ftypes['java'] = FindSubs_ftypes['html'] = FindSubs_ftypes['php']

def FindSubs_Find(ft='perl'):
	try:
		rxp = FindSubs_ftypes[ft]
	except KeyError:
		print 'Unknown filetype: "%s"' % ft
		return

	# read buffer content
	fc = "\n".join(line for line in vim.current.buffer)

	# find all functions names
	m = re.findall(rxp, fc, re.M)
	if not m:
		print 'Not found'
		return

	# print them for user
	for i, sub in enumerate(m):
		print '%d. %s' % (i+1, sub[1])

	# ask for function he want to go
	choice = vim.eval('input("Which one: ")')
	try:
		choice = int(choice) - 1
	except:
		print 'Incorrect value'
		return

	# find file offset
	pos = fc.find(m[choice][0].strip())
	if pos < 0:
		print >>sys.stderr, "Some error occured."
		return

	# and go there
	vim.command('goto '+str(pos+1))

def FindSubs_Main():
	FindSubs_Find(MyDevPlugins_GetFileType())

EOF

function! FindSubs()
	python FindSubs_Main()
endfunction
map <F5> :call FindSubs()<CR>

Ostatnia linijka odpowiada za podmapowanie wywołania funkcji pod klawisz F5.

Natomiast problemy sprawia opcja rozpoznawania typu pliku. Nie doszukałem się nigdzie w dokumentacji lepszego sposobu, a ten czasem zawodzi :( Nic na to chwilowo nie poradzę. Oczywiście przydałoby się rozpoznawanie większej ilości języków, ale to co jest obecnie wystarcza mi w zupełności, w razie czego można sobie dorobić odpowiednie regexpy do słownika FindSubs_ftypes.