Vim jako środowisko pythonowe po roku

Rok temu opisywałem, jak przygotowałem Vima do pracy w charakterze środowiska programistycznego. Po roku intensywnego używania, nauczyłem się nieco odmiennego sposobu posługiwania się tym narzędziem. Odmiennego, ponieważ Vim jest edytorem, a nie IDE i robienie na siłę kalki z np. Eclipse nie jest wg mnie najlepszym pomysłem.

Od początku

W poprzednim wpisie skupiłem się głównie na krótkim opisie, z jakich elementów mam zamiar opierać się przy używaniu tego edytora na produkcji, zupełnie pomijając podstawową konfigurację, bez której najprawdopodobniej żaden z tych skryptów działać nie będzie.

Bez jakiejkolwiek konfiguracji, zależnie od dystrybucji, Vim prawdopodobnie będzie wyglądał tak:

Goły Gvim

Goły Gvim

Zależnie od dystrybucji Linuksa, Vim może być rozprowadzany z domyślną konfiguracją lub ze specyficzną dla danej dystrybucji. Na szczęście można ją (konfigurację) całkowicie pominąć, i stworzyć swoją własną. Głównym plikiem konfiguracyjnym (dla użytkownika) jest .vimrc, umieszczany zwykle w katalogu domowym użytkownika. Pod Windowsem podobnie - z tym, że plik będzie się nazywał _vimrc i będzie umieszczony w Documents and settings\nazwa_użytkownika (XP) lub Users\nazwa_użytkownika (Windows 7).

Podstawowe ustawienia edytora obejmują, m.in. zachowanie oraz wygląd. Pozostałe, specyficzne dla poszczególnych typów plików ustawienia, przechowuję w osobnych plikach w katalogu .vim/ftplugin (o tym później). Podstawowymi, według mnie, ustawieniami, jakie powinny się znaleźć w .vimrc są:

"Ustawia tryb rozszerzony Vima, zamiast domyślnego, kompatybilnego z Vi
set nocompatible
"Włącza autoładowanie wcięć i wtyczek dla poszczególnych typów plików
filetype plugin indent on
"Włącza kolorowanie składni
syntax on
"Zezwala na kasowanie wszystkiego w trybie wprowadzania
set backspace=indent,eol,start
"Powiadamia Vima o użytym ciemnym motywie
set background=dark

Porcja kilku kolejnych ustawień to rzecz czysto subiektywna, mająca głównie wpływ na wygląd i wygodę użytkowania edytora:

"Pytaj o potwierdzenie, zamiast odmawiać wykonania operacji
set confirm
"Podświetl aktualną linię
set cursorline
"Ukryj, wszystkie 'nieaktywne' bufory, zamiast usuwać z pamięci
set hidden
"pamiętaj 1000 linii historii (komendy, wyszukiwanie, itp)
set history=1000
"Ignoruj wielkość znaków
set ignorecase
"Zawsze pokazuj pasek statusu
set laststatus=2
"Nie przerysowuj ekranu podczas wykonywania makr, rejestrów itp
set lazyredraw

"Ustaw znaki zastępujące znak tabulatora i białe znaki na końcach linii.
"Włączane tylko dla niektórych plików, lub poprzez wywołanie komendy
":set list
set listchars=tab:▸-,trail:·
"Pokaż numery linii
set number
"Ustaw zawartość linii statusu
set rulerformat=%l,%c%V%=#%n\ %3p%%
"Minimalna liczba wierszy zawsze widocznych nad i pod kursorem
set scrolloff=5
"Zaznaczenie bez kursora w trybie wizualnym i zaznaczania
set selection=exclusive

Ustawienia dotyczące wcięć:

"Domyślna długość znaku tabulacji
set tabstop=4
"Domyślna długość wcięcia/przesunięcia
set shiftwidth=4
"Automatycznie użyj spacji zamiast znaku tabulacji
set expandtab

Warto zauważyć, że Vim pozwala na swobodne zdefiniowanie w jaki sposób znaki tabulacji mają się układać w tekście, jaką mają mieć długość, oraz używać "wirtualnych" wcięć krótszych (np na 4 znaki) niż domyślna długość taba, wciąż używając klawisza Tab i Backspace. Więcej informacji odnośnie konfiguracji tego aspektu można znaleźć w pomocy: :help tabstop, :help softtabstop, :help expandtab i :help shiftwidth.

Kilka dodatkowych ustawień:

"Skróć niektóre informacje (np. użyj '[New]' zamiast '[New File])
set shortmess=atToOI
"Łańcuch znaków, które pokazywane są by oznaczyć zwinięte linie
set showbreak=>
"Pokazuj ostatnią wykonaną komendę w ostatniej linii edytora
set showcmd
"Gdy zamykający nawias zostanie wpisany, skocz na moment do otwierającego
set showmatch

"Ustawienia sprawdzania pisowni
set spelllang=pl,en

"Zapisuj w widoku tylko pozycję kursora
set viewoptions=cursor
"Konfiguracja informacji zapisywanych w pliku .viminfo
set viminfo='20,<1000,h,f0
"Dodaj klawisze kursora do przechodzenia pomiędzy liniami
set whichwrap+=<,>,[,]
"Pokazuje listę możliwych dopełnień na pasku statusów
set wildmenu

"Nie zapisuj plików backupu/writeback/swapfile
set nobackup
set nowb
set noswapfile

"Opcje komendy TOhtml :let html_number_lines = 1
:let html_use_css = 1
:let html_ignore_folding = 1
:let html_use_encoding = "utf-8"

Komentarze przed komendami powinny być samo opisujące. Warto pamiętać, że każda komenda, ustawienie czy opcja jest dostępna w obszernej pomocy edytora:

:h set
:h spell
:h 'spell'
:h CTRL-W_+
:h holy-grail

Opcji do wyboru jest ogromna ilość, ja wybrałem tylko część z nich, która wg mnie jest istotna, niemniej jednak nic nie stoi na przeszkodzie, by otworzyć sobie pomoc :h options i przestudiować zawartość.

Wtyczki

Kolejną rzeczą, która silnie wpływa na sposób użytkowania edytora to skrypty napisane z wykorzystaniem dialektu języka Vima - ale nie tylko. Vim dystrybuowany jest z ogromną ilością takich skryptów, które składają się na sposób jego działania, wyglądu i zachowania. Oprócz oficjalnych skryptów, jest cała masa skryptów pisanych przez społeczność i dostępnych (zwykle) na http://www.vim.org/scripts.

Standardowo dodawaną wtyczką, pozwalającą na zarządzanie rozrastającą się z czasem bazą dodatkowych skryptów jest GetLatestVimScripts. Dzięki niej jestem w stanie jedną komendą sprawdzić i ściągnąć najnowsze wersje skryptów vimowych.

Nie jest to jedyny sposób na zarządzanie dodatkowymi skryptami dla Vima - istnieje kilka innych rozwiązań, jak choćby debianowy vim-addon-manager napisany w rubym, czy vimowy plugin o tej samej nazwie.

A oto lista skryptów, których używam na co dzień, podzielona ze względu na charakter, w jaki wpływają na Vima:

Liczba rozszerzeń, których używam, ciągle się zmienia, jednakże zestaw który wymieniłem osiadł na dobre i nabyłem już nawyków do ich używania. Oczywiście, czasem próbuję nowych, interesujących mnie szczególnie rozszerzeń, lub usuwam skrypty, których (jak się po czasie okazuje) nie używam w ogóle.

Edycja plików

W mojej obecnej pracy jestem zmuszony do korzystania z Windows (z czego, nie ukrywam, nie jestem szczególnie zadowolony), oraz narzędzi, które sam sobie dobiorę. Na szczęście w moim zespole nie ma przymusu do wyboru konkretnego narzędzia do wprowadzania tekstu.

Z kolei projekt, przy którym pracuję, korzysta z kilku różnych technologii, włączając w to aplikacje desktopowe jak i te oparte o model klient‐serwer, bazujący na serwerze Apache i przeglądarce.

Całość tego wielomodułowego systemu jest spięta przez wspólny mianownik: Python. W Pythonie napisana jest w całości aplikacja po stronie serwera wyżej wspomnianego modułu, jak również w Pythonie wykonana jest znaczna część logiki dla modułów desktopowych.

Korzystając z Vima silnie wykorzystuję to co dają mi wymienione powyżej pluginy, zwłaszcza fuzzyfinder, który wykorzystuję do wyszukania i otwarcia plików, których nazwy mniej więcej pamiętam. Mozolne przedzieranie się przez drzewo plików w NERDTree czy to w project.tar.gz, kompletnie się nie sprawdziło. Pojęcie projektu, który dość mocno zdefiniowany jest w IDE takim jak Eclipse czy Netbeans, ale również w TextMate czy jEdit z wtyczką Project Viewer, nie istnieje w Vimie, i próby jego emulowania mijają się IMO z celem.

Gdy nie pamiętam nazwy pliku, lub chciałbym wyszukać jakiś konkretny wzorzec wśród plików, stosuję :Grep lub :Rgrep, który jest nieco wygodniejszy od :vimgrep. Autor zaleca stosowanie Pod Windows narzędzi grep i find, jednak równie dobrze można skorzystać z tych dostarczanych przez cygwin.

Otwarte pliki zwykle rozkładam po zakładkach, dla różnych modułów systemu, które umieszczone są w różnych miejscach systemu plików, przydatną opcją jest polecenie :lcd, które zmienia bieżący katalog tylko dla obecnego okna/zakładki, przez co można przygotować sobie kilka zakładek w zależności od potrzebnego widoku. Należy pamiętać, że w każdy otwarty plik może być widoczny w dowolnej ilości zakładek, przez co takie zachowanie może sprawić, że bardzo łatwo jest się pogubić, zwłaszcza początkującym.

Python

Python jest językiem, w którym programuję najwięcej (również prywatnie). Vim wspiera pisanie w tym języku w dość sporym zakresie.

Po pierwsze, trzeba dostosować nieco zachowanie Vima, by ładnie się zachowywał z plikami pythonowymi:

setlocal cinkeys-=0#
setlocal indentkeys-=0#
setlocal expandtab
setlocal foldlevel=100
setlocal foldmethod=indent
setlocal list
setlocal noautoindent
setlocal shiftwidth=4
setlocal smartindent
setlocal cinwords=if,elif,else,for,while,try,except,finally,def,class,with
setlocal smarttab
setlocal softtabstop=4
setlocal tabstop=4
setlocal textwidth=78
setlocal colorcolumn=+1
set wildignore+=*.pyc

let python_highlight_all=1

"Load views for py files
autocmd BufWinLeave *.py mkview
autocmd BufWinEnter *.py silent loadview

compiler pylint

Po drugie, wprost z „pudełka” dostarczana jest funkcjonalność, która pozwala na odpowiednie podświetlenie składni języka (syntax), logikę wcięć (indent), możliwość przemieszczania się pomiędzy funkcjami i klasami używając ]] i [[ (klasy i funkcje modułu) oraz ]m i [m (wszystkie klasy, metody i funkcje), oraz podpowiadanie składni. Vim jest też jednym z niewielu edytorów wspierających Pythona3.

Funkcjonalność tę rozszerza plugin python_fn.vim, który usprawnia poruszanie się po kodzie pythonowym wprowadzając możliwość przemieszczania się pomiędzy klasami (]j i ]J), funkcjami (]f i ]F), na początek i koniec bloku o tym samym poziomie wcięć (]t i ]e), zwiększanie i zmniejszanie poziomu wcięć bloku kodu z lub bez zaznaczania (]> i ]<), komentowanie i odkomentowanie linii lub zaznaczenia (]# i ]u) oraz zaznaczanie bloku, klasy lub funkcji (]v, ]c i ]d). Pozwala on także na generowanie menu IM-Python ze strukturą bieżącego pliku.

Jednym z zacniejszych usprawnień, jest wcześniej wspomniany skrypt pyflakes, który periodycznie sprawdza poprawność kodu pythonowego pozwalając na wczesne wyeliminowanie błędów składni, literówek, nieużywanych importów, nieoczekiwanych wcięć, i podobnych błędów możliwych do wychwycenia poprzez statyczną analizę kodu.

Następnym narzędziem, z którego mocno korzystam, jest pylint. Jest ono dużo bardziej skomplikowane i dokładne, przez co może być znacznie wolniejsze niż ultraszybki pyflakes. Jest całkiem sporo tutoriali jak pożenić pylint z Vimem, a nawet jest gotowy skrypt typu „compiler”, jednak ja przygotowałem swoją własną wersję z kilku powodów. Po pierwsze, nie bardzo podobała mi się forma w jakiej wspomniany kompiler zwracał wynik przeprowadzonej analizy, po drugie, w obecnej wersji pylint oprócz linii, potrafi wskazać w której kolumnie znajduje się błąd przy pomocy jednego lub więcej znaków ^:

gryf@mslug ~ $ pylint -rn -iy test2.py
No config file found, using default configuration
************* Module test2
C0111:  1: Missing docstring
C0324:  4: Comma not followed by a space
import os.path,subprocess
              ^^
W0404: 26: Reimport 'subprocess' (imported line 4)
C0111: 30:get_familly: Missing docstring
W0621: 30:get_familly: Redefining name 'pid' from outer scope (line 43)

Po trzecie wreszcie, chciałem mieć możliwość podmiany programu odpowiedzialnego za generowanie raportu.

Pierwszym pomysłem było dostarczenie komendy :Pylint, która będzie się zachowywać jak ja chcę. Ponieważ parser był napisany w Pythonie, wydzieliłem go jako osobny skrypt, który następnie podpiąłem pod compiler Vimowy. Skrypt nie jest jakiś szczególnie skomplikowany, i zapewne dałoby się uruchomić checkery i w jakiś sprytny sposób dobrać się do listy błędów, ale no cóż :) poszedłem na łatwiznę i analizuję jedynie wyjście jakie dostaję z Reportera:

 1 #!/usr/bin/env python
 2 """
 3 This script can be used as a pylint command replacement, especially useful as
 4 a "make" command for Vim
 5 """
 6 import sys
 7 import re
 8 from StringIO import StringIO
 9 from optparse import OptionParser
10 
11 from pylint import lint
12 from pylint.reporters.text import TextReporter
13 
14 
15 SYS_STDERR = sys.stderr
16 DUMMY_STDERR = StringIO()
17 CONF_MSG = 'No config file found, using default configuration\n'
18 
19 def parsable_pylint(filename):
20     """
21     Simple wrapper for pylint checker. Provides nice, parseable output.
22     filename - python fileneame to check
23 
24     Returns list of dicts of errors, i.e.:
25     [{'lnum': 5, 'col': 10, 'type': 'C0324',
26       'text': 'Comma not followed by a space'},
27      {'lnum': 12, 'type': 'C0111', 'text': 'Missing docstring'},
28      ....
29     ]
30 
31     """
32     # args
33     args = ['-rn',  # display only the messages instead of full report
34             '-iy',  # Include message's id in output
35             filename]
36 
37     buf = StringIO()  # file-like buffer, instead of stdout
38     reporter = TextReporter(buf)
39 
40     sys.stderr = DUMMY_STDERR
41     lint.Run(args, reporter=reporter, exit=False)
42     sys.stderr = SYS_STDERR
43 
44     # see, if we have other errors than 'No config found...' message
45     DUMMY_STDERR.seek(0)
46     error_list = DUMMY_STDERR.readlines()
47     DUMMY_STDERR.truncate(0)
48     if error_list and CONF_MSG in error_list:
49         error_list.remove(CONF_MSG)
50         if error_list:
51             raise Exception(''.join(error_list))
52 
53     buf.seek(0)
54 
55     code_line = {}
56     error_list = []
57 
58     carriage_re = re.compile(r'\s*\^+$')
59     error_re = re.compile(r'^([C,R,W,E,F].+):\s+?([0-9]+):?.*:\s(.*)$')
60 
61     for line in buf:
62         line = line.rstrip()  # remove trailing newline character
63 
64         if error_re.match(line):
65             if code_line:
66                 error_list.append(code_line)
67                 code_line = {}
68 
69             code_line['type'], code_line['lnum'], code_line['text'] = \
70                     error_re.match(line).groups()
71 
72         if carriage_re.match(line) and code_line:
73             code_line['col'] = carriage_re.match(line).group().find('^') + 1
74 
75     return error_list
76 
77 if __name__ == "__main__":
78     parser = OptionParser("usage: %prog python_file")
79     (options, args) = parser.parse_args()
80     if len(args) == 1:
81         for line in parsable_pylint(args[0]):
82             line['short'] = line['type'][0]
83             line['fname'] = args[0]
84             out = "%(fname)s: %(short)s: %(lnum)s: %(col)s: %(type)s %(text)s"
85             if 'col' not in line:
86                 out = "%(fname)s: %(short)s: %(lnum)s: 0: %(type)s %(text)s"
87 
88             print out % line

Po umieszczeniu skryptu gdzieś w ścieżce, napisanie skryptu „kompilatora” jest banałem:

" Vim compiler file for Python
" Compiler:     Static code checking tool for Python
" Maintainer:   Roman 'gryf' Dobosz
" Last Change:  2010-09-12
" Version:      1.0
if exists("current_compiler")
    finish
endif

let current_compiler = "pylint"
CompilerSet makeprg=pylint_parseable.py\ %
CompilerSet efm=%f:\ %t:\ %l:\ %c:\ %m,%f:\ %t:\ %l:\ %m

:make można wywołać albo poprzez linię komend Vima (wpisując po prostu :make), albo podmapować wywołanie pod klawisz lub zdarzenie. Ja wykorzystuję klawisz <F5>. Tak może wyglądać wynik działania:

Pylint w akcji

Pylint w akcji

Podobna sytuacja miała miejsce z narzędziem Pep8, które uzupełnia pylint jak i pyflakes. Pozwala ono sprawdzić czy styl, w jakim napisany jest plik pythonowy, zgodny jest z zaleceniami zawartymi w PEP8. W odróżnieniu od pylinta, narzędzie to otrzymało swoją własną komendę (:Pep8) i zostało napisane z wykorzystaniem języka Python:

 48 if exists("b:did_pep8_plugin")
 49     finish " only load once
 50 else
 51     let b:did_pep8_plugin = 1
 52 endif
 53 
 54 if !exists("b:did_pep8_init")
 55     let b:did_pep8_init = 0
 56 
 57     if !has('python')
 58         echoerr "pep8_fn.vim plugin requires Vim to be compiled with +python"
 59         finish
 60     endif
 61 
 62     python << EOF
 63 import vim
 64 import sys
 65 from StringIO import StringIO
 66 
 67 try:
 68     import pep8
 69 except ImportError:
 70     raise AssertionError('Error: pep8_fn.vim requires module pep8')
 71 
 72 class VimPep8(object):
 73 
 74     def __init__(self):
 75         self.fname = vim.current.buffer.name
 76         self.bufnr = vim.current.buffer.number
 77         self.output = []
 78 
 79     def reporter(self, lnum, col, text, check):
 80         self.output.append([lnum, col, text])
 81 
 82     def run(self):
 83         pep8.process_options(['-r', vim.current.buffer.name])
 84         checker = pep8.Checker(vim.current.buffer.name)
 85         checker.report_error = self.reporter
 86         checker.check_all()
 87         self.process_output()
 88 
 89     def process_output(self):
 90         vim.command('call setqflist([])')
 91         qf_list = []
 92         qf_dict = {}
 93 
 94         for line in self.output:
 95             qf_dict['bufnr'] = self.bufnr
 96             qf_dict['lnum'] = line[0]
 97             qf_dict['col'] = line[1]
 98             qf_dict['text'] = line[2]
 99             qf_dict['type'] = line[2][0]
100             qf_list.append(qf_dict)
101             qf_dict = {}
102 
103         self.output = []
104         vim.command('call setqflist(%s)' % str(qf_list))
105         if qf_list:
106             vim.command('copen')
107 EOF
108     let b:did_pep8_init = 1
109 endif
110 
111 if !exists('*s:Pep8')
112     function s:Pep8()
113         python << EOF
114 VimPep8().run()
115 EOF
116     endfunction
117 endif
118 
119 if !exists(":Pep8")
120     command Pep8 call s:Pep8()
121 endif
Pep8 w akcji

Pep8 w akcji

Przydatną funkcją podczas pracy z plikami pythonowymi (i ogólnie z jakimkolwiek kodem źródłowym) jest usuwanie zbędnych białych znaków na końcach linii. Kilka przykładów takiej funkcji można znaleźć na stronach Vim Wiki. Złożoną z kilku pomysłów funkcja StripTrailingWhitespaces:

" Remove trailing whitespace
function <SID>StripTrailingWhitespaces()
    " Preparation: save last search, and cursor position.
    let _s=@/
    let l = line(".")
    let c = col(".")
    " Do the business:
    %s/\s\+$//e
    " Clean up: restore previous search history, and cursor position
    let @/=_s
    call cursor(l, c)
endfunction

która nie zaśmieca historii wyszukiwania oraz zapamiętuje ostatnią pozycję kursora, podpiąłem pod zdarzenie BufWritePre dla plików Pythona, reStructuredText, wiki, javascript CSS i XML:

"remove all trailing whitespace for specified files before write
autocmd BufWritePre *.py :call <SID>StripTrailingWhitespaces()
autocmd BufWritePre *.rst :call <SID>StripTrailingWhitespaces()
autocmd BufWritePre *.wiki :call <SID>StripTrailingWhitespaces()
autocmd BufWritePre *.js :call <SID>StripTrailingWhitespaces()
autocmd BufWritePre *.css :call <SID>StripTrailingWhitespaces()
autocmd BufWritePre *.xml :call <SID>StripTrailingWhitespaces()

Dodałem też kilka szczególnie często wykorzystywanych fragmentów kodu pythonowego wykorzystając plugin snipMate, dla przykładu, będąc w trybie insert i wpisując w dowolnym miejscu kodu pythonowego:

dbg<TAB>

otrzymuję:

import ipdb; ipdb.set_trace()

I to nie wszystko. Strasznie ciężko napisać jeden zwarty artykuł by pokazać ogromny potencjał drzemiący w tym niepozornym edytorze.

Chyba nie będzie niespodzianką, jeśli napiszę, że artykuł, który właśnie czytasz został napisany w reST przy użyciu Vima, ze wspomaganiem - czyli ze sprawdzaniem pisowni oraz funkcją do generowania HTML ad hoc, a wszelkie listingi to produkt komendy :TOhtml (EDIT: nie jest to już aktualne, z uwagi na to, że od jakiegoś czasu wykorzystuję Pygments przy kolorowaniu dyrektywy code):

Gvim + Firefox

Gvim + Firefox

Zmory

Skłamałbym, jakbym napisał, że Vim jest idealny. Vim ma swoje wady, choć często wynikające (jak się później okazuje) z mojej niewiedzy, ale nie tylko.

Jednym z podstawowych, wkurzających zachowań Gvima (graficznej wersji Vima) pod Linuxem jest jego okno. Nie wiem, dlaczego twórcy linuksowej (uniksowej) wersji w GTK zaimplementowali Gvima w ten sposób, ale okno zachowuje się jak licealistka - mianowicie, podczas normalnej edycji wszystko jest ok, do czasu, gdy doda/ujmie się nowy element interfejsu (stworzy tab/usunie ostatni, doda/ujmie menu, pasek narzędzi itp.) lub gdy rozdzieli się okno (split), np. wywołując bufor quickfix czy jakikolwiek inny, okno edytora zmienia nieznacznie swój rozmiar, co po kilkunastu takich zmianach prowadzi do częściowego przesłonienia dockappów czy zminimalizowanych aplikacji w Window Makerze. Również w innych menadżerach okien (Awesome, Openbox), jakie miałem okazję używać występują podobne objawy, z tym że okno, co prawda jest rozciągnięte na cały ekran, jednakże zaczynają się dziać dziwne rzeczy wewnątrz okna Gvima - zostaje sporo miejsca, które bez problemu mogłoby zostać wykorzystane przez kolumnę bądź wiersz w edytorze.

Ok, zdaję sobie sprawę, że Gvim (tak jak i Vim) wywodzi się z edytora mocno eksploatowanego w terminalach, jednakże wersja Windowsowa nie ma takich problemów i gładko radzi sobie z wypełnieniem miejsca w oknie bez ciągłej zmiany rozmiarów tegoż. Da się? Jak widać.

Epilog

Czy mogę powiedzieć, że nauczyłem się Vima na tyle, żeby móc z całą pewnością powiedzieć, że go umiem? Na pewno nie. Co rusz uczę się nowych komend, lub odkrywam stare, o których nie wiedziałem, że można używać ich w inny sposób.

Niewątpliwie też, nie wyczerpałem tematu w całości, wobec czego następne wpisy będę się starał dozować :)


, Etykiety: python, vim