bgneal@8
|
1 " pyflakes.vim - A script to highlight Python code on the fly with warnings
|
bgneal@8
|
2 " from Pyflakes, a Python lint tool.
|
bgneal@8
|
3 "
|
bgneal@8
|
4 " Place this script and the accompanying pyflakes directory in
|
bgneal@8
|
5 " .vim/ftplugin/python.
|
bgneal@8
|
6 "
|
bgneal@8
|
7 " See README for additional installation and information.
|
bgneal@8
|
8 "
|
bgneal@8
|
9 " Thanks to matlib.vim for ideas/code on interactive linting.
|
bgneal@8
|
10 "
|
bgneal@8
|
11 " Maintainer: Kevin Watters <kevin.watters@gmail.com>
|
bgneal@8
|
12 " Version: 0.1
|
bgneal@8
|
13
|
bgneal@8
|
14 if exists("b:did_pyflakes_plugin")
|
bgneal@8
|
15 finish " only load once
|
bgneal@8
|
16 else
|
bgneal@8
|
17 let b:did_pyflakes_plugin = 1
|
bgneal@8
|
18 endif
|
bgneal@8
|
19
|
bgneal@8
|
20 if !exists('g:pyflakes_builtins')
|
bgneal@8
|
21 let g:pyflakes_builtins = []
|
bgneal@8
|
22 endif
|
bgneal@8
|
23
|
bgneal@8
|
24 if !exists("b:did_python_init")
|
bgneal@8
|
25 let b:did_python_init = 0
|
bgneal@8
|
26
|
bgneal@8
|
27 if !has('python')
|
bgneal@8
|
28 echoerr "Error: the pyflakes.vim plugin requires Vim to be compiled with +python"
|
bgneal@8
|
29 finish
|
bgneal@8
|
30 endif
|
bgneal@8
|
31
|
bgneal@8
|
32 if !exists('g:pyflakes_use_quickfix')
|
bgneal@8
|
33 let g:pyflakes_use_quickfix = 1
|
bgneal@8
|
34 endif
|
bgneal@8
|
35
|
bgneal@8
|
36
|
bgneal@8
|
37 python << EOF
|
bgneal@8
|
38 import vim
|
bgneal@8
|
39 import os.path
|
bgneal@8
|
40 import sys
|
bgneal@8
|
41
|
bgneal@8
|
42 if sys.version_info[:2] < (2, 5):
|
bgneal@8
|
43 raise AssertionError('Vim must be compiled with Python 2.5 or higher; you have ' + sys.version)
|
bgneal@8
|
44
|
bgneal@8
|
45 # get the directory this script is in: the pyflakes python module should be installed there.
|
bgneal@8
|
46 scriptdir = os.path.join(os.path.dirname(vim.eval('expand("<sfile>")')), 'pyflakes')
|
bgneal@8
|
47 sys.path.insert(0, scriptdir)
|
bgneal@8
|
48
|
bgneal@8
|
49 import ast
|
bgneal@8
|
50 from pyflakes import checker, messages
|
bgneal@8
|
51 from operator import attrgetter
|
bgneal@8
|
52 import re
|
bgneal@8
|
53
|
bgneal@8
|
54 class loc(object):
|
bgneal@8
|
55 def __init__(self, lineno, col=None):
|
bgneal@8
|
56 self.lineno = lineno
|
bgneal@8
|
57 self.col_offset = col
|
bgneal@8
|
58
|
bgneal@8
|
59 class SyntaxError(messages.Message):
|
bgneal@8
|
60 message = 'could not compile: %s'
|
bgneal@8
|
61 def __init__(self, filename, lineno, col, message):
|
bgneal@8
|
62 messages.Message.__init__(self, filename, loc(lineno, col))
|
bgneal@8
|
63 self.message_args = (message,)
|
bgneal@8
|
64
|
bgneal@8
|
65 class blackhole(object):
|
bgneal@8
|
66 write = flush = lambda *a, **k: None
|
bgneal@8
|
67
|
bgneal@8
|
68 def check(buffer):
|
bgneal@8
|
69 filename = buffer.name
|
bgneal@8
|
70 contents = buffer[:]
|
bgneal@8
|
71
|
bgneal@8
|
72 # shebang usually found at the top of the file, followed by source code encoding marker.
|
bgneal@8
|
73 # assume everything else that follows is encoded in the encoding.
|
bgneal@8
|
74 encoding_found = False
|
bgneal@8
|
75 for n, line in enumerate(contents):
|
bgneal@8
|
76 if n >= 2:
|
bgneal@8
|
77 break
|
bgneal@8
|
78 elif re.match(r'#.*coding[:=]\s*([-\w.]+)', line):
|
bgneal@8
|
79 contents = ['']*(n+1) + contents[n+1:]
|
bgneal@8
|
80 break
|
bgneal@8
|
81
|
bgneal@8
|
82 contents = '\n'.join(contents) + '\n'
|
bgneal@8
|
83
|
bgneal@8
|
84 vimenc = vim.eval('&encoding')
|
bgneal@8
|
85 if vimenc:
|
bgneal@8
|
86 contents = contents.decode(vimenc)
|
bgneal@8
|
87
|
bgneal@8
|
88 builtins = set(['__file__'])
|
bgneal@8
|
89 try:
|
bgneal@8
|
90 builtins.update(set(eval(vim.eval('string(g:pyflakes_builtins)'))))
|
bgneal@8
|
91 except Exception:
|
bgneal@8
|
92 pass
|
bgneal@8
|
93
|
bgneal@8
|
94 try:
|
bgneal@8
|
95 # TODO: use warnings filters instead of ignoring stderr
|
bgneal@8
|
96 old_stderr, sys.stderr = sys.stderr, blackhole()
|
bgneal@8
|
97 try:
|
bgneal@8
|
98 tree = ast.parse(contents, filename or '<unknown>')
|
bgneal@8
|
99 finally:
|
bgneal@8
|
100 sys.stderr = old_stderr
|
bgneal@8
|
101 except:
|
bgneal@8
|
102 try:
|
bgneal@8
|
103 value = sys.exc_info()[1]
|
bgneal@8
|
104 lineno, offset, line = value[1][1:]
|
bgneal@8
|
105 except IndexError:
|
bgneal@8
|
106 lineno, offset, line = 1, 0, ''
|
bgneal@8
|
107 if line and line.endswith("\n"):
|
bgneal@8
|
108 line = line[:-1]
|
bgneal@8
|
109
|
bgneal@8
|
110 return [SyntaxError(filename, lineno, offset, str(value))]
|
bgneal@8
|
111 else:
|
bgneal@8
|
112 # pyflakes looks to _MAGIC_GLOBALS in checker.py to see which
|
bgneal@8
|
113 # UndefinedNames to ignore
|
bgneal@8
|
114 old_globals = getattr(checker,' _MAGIC_GLOBALS', [])
|
bgneal@8
|
115 checker._MAGIC_GLOBALS = set(old_globals) | builtins
|
bgneal@8
|
116
|
bgneal@8
|
117 w = checker.Checker(tree, filename)
|
bgneal@8
|
118
|
bgneal@8
|
119 checker._MAGIC_GLOBALS = old_globals
|
bgneal@8
|
120
|
bgneal@8
|
121 w.messages.sort(key = attrgetter('lineno'))
|
bgneal@8
|
122 return w.messages
|
bgneal@8
|
123
|
bgneal@8
|
124
|
bgneal@8
|
125 def vim_quote(s):
|
bgneal@8
|
126 return s.replace("'", "''")
|
bgneal@8
|
127 EOF
|
bgneal@8
|
128 let b:did_python_init = 1
|
bgneal@8
|
129 endif
|
bgneal@8
|
130
|
bgneal@8
|
131 if !b:did_python_init
|
bgneal@8
|
132 finish
|
bgneal@8
|
133 endif
|
bgneal@8
|
134
|
bgneal@8
|
135 au BufLeave <buffer> call s:ClearPyflakes()
|
bgneal@8
|
136
|
bgneal@8
|
137 au BufEnter <buffer> call s:RunPyflakes()
|
bgneal@8
|
138 au InsertLeave <buffer> call s:RunPyflakes()
|
bgneal@8
|
139 au InsertEnter <buffer> call s:RunPyflakes()
|
bgneal@8
|
140 au BufWritePost <buffer> call s:RunPyflakes()
|
bgneal@8
|
141
|
bgneal@8
|
142 au CursorHold <buffer> call s:RunPyflakes()
|
bgneal@8
|
143 au CursorHoldI <buffer> call s:RunPyflakes()
|
bgneal@8
|
144
|
bgneal@8
|
145 au CursorHold <buffer> call s:GetPyflakesMessage()
|
bgneal@8
|
146 au CursorMoved <buffer> call s:GetPyflakesMessage()
|
bgneal@8
|
147
|
bgneal@8
|
148 if !exists("*s:PyflakesUpdate")
|
bgneal@8
|
149 function s:PyflakesUpdate()
|
bgneal@8
|
150 silent call s:RunPyflakes()
|
bgneal@8
|
151 call s:GetPyflakesMessage()
|
bgneal@8
|
152 endfunction
|
bgneal@8
|
153 endif
|
bgneal@8
|
154
|
bgneal@8
|
155 " Call this function in your .vimrc to update PyFlakes
|
bgneal@8
|
156 if !exists(":PyflakesUpdate")
|
bgneal@8
|
157 command PyflakesUpdate :call s:PyflakesUpdate()
|
bgneal@8
|
158 endif
|
bgneal@8
|
159
|
bgneal@8
|
160 " Hook common text manipulation commands to update PyFlakes
|
bgneal@8
|
161 " TODO: is there a more general "text op" autocommand we could register
|
bgneal@8
|
162 " for here?
|
bgneal@8
|
163 noremap <buffer><silent> dd dd:PyflakesUpdate<CR>
|
bgneal@8
|
164 noremap <buffer><silent> dw dw:PyflakesUpdate<CR>
|
bgneal@8
|
165 noremap <buffer><silent> u u:PyflakesUpdate<CR>
|
bgneal@8
|
166 noremap <buffer><silent> <C-R> <C-R>:PyflakesUpdate<CR>
|
bgneal@8
|
167
|
bgneal@8
|
168 " WideMsg() prints [long] message up to (&columns-1) length
|
bgneal@8
|
169 " guaranteed without "Press Enter" prompt.
|
bgneal@8
|
170 if !exists("*s:WideMsg")
|
bgneal@8
|
171 function s:WideMsg(msg)
|
bgneal@8
|
172 let x=&ruler | let y=&showcmd
|
bgneal@8
|
173 set noruler noshowcmd
|
bgneal@8
|
174 redraw
|
bgneal@8
|
175 echo strpart(a:msg, 0, &columns-1)
|
bgneal@8
|
176 let &ruler=x | let &showcmd=y
|
bgneal@8
|
177 endfun
|
bgneal@8
|
178 endif
|
bgneal@8
|
179
|
bgneal@8
|
180 if !exists("*s:GetQuickFixStackCount")
|
bgneal@8
|
181 function s:GetQuickFixStackCount()
|
bgneal@8
|
182 let l:stack_count = 0
|
bgneal@8
|
183 try
|
bgneal@8
|
184 silent colder 9
|
bgneal@8
|
185 catch /E380:/
|
bgneal@8
|
186 endtry
|
bgneal@8
|
187
|
bgneal@8
|
188 try
|
bgneal@8
|
189 for i in range(9)
|
bgneal@8
|
190 silent cnewer
|
bgneal@8
|
191 let l:stack_count = l:stack_count + 1
|
bgneal@8
|
192 endfor
|
bgneal@8
|
193 catch /E381:/
|
bgneal@8
|
194 return l:stack_count
|
bgneal@8
|
195 endtry
|
bgneal@8
|
196 endfunction
|
bgneal@8
|
197 endif
|
bgneal@8
|
198
|
bgneal@8
|
199 if !exists("*s:ActivatePyflakesQuickFixWindow")
|
bgneal@8
|
200 function s:ActivatePyflakesQuickFixWindow()
|
bgneal@8
|
201 try
|
bgneal@8
|
202 silent colder 9 " go to the bottom of quickfix stack
|
bgneal@8
|
203 catch /E380:/
|
bgneal@8
|
204 endtry
|
bgneal@8
|
205
|
bgneal@8
|
206 if s:pyflakes_qf > 0
|
bgneal@8
|
207 try
|
bgneal@8
|
208 exe "silent cnewer " . s:pyflakes_qf
|
bgneal@8
|
209 catch /E381:/
|
bgneal@8
|
210 echoerr "Could not activate Pyflakes Quickfix Window."
|
bgneal@8
|
211 endtry
|
bgneal@8
|
212 endif
|
bgneal@8
|
213 endfunction
|
bgneal@8
|
214 endif
|
bgneal@8
|
215
|
bgneal@8
|
216 if !exists("*s:RunPyflakes")
|
bgneal@8
|
217 function s:RunPyflakes()
|
bgneal@8
|
218 highlight link PyFlakes SpellBad
|
bgneal@8
|
219
|
bgneal@8
|
220 if exists("b:cleared")
|
bgneal@8
|
221 if b:cleared == 0
|
bgneal@8
|
222 silent call s:ClearPyflakes()
|
bgneal@8
|
223 let b:cleared = 1
|
bgneal@8
|
224 endif
|
bgneal@8
|
225 else
|
bgneal@8
|
226 let b:cleared = 1
|
bgneal@8
|
227 endif
|
bgneal@8
|
228
|
bgneal@8
|
229 let b:matched = []
|
bgneal@8
|
230 let b:matchedlines = {}
|
bgneal@8
|
231
|
bgneal@8
|
232 let b:qf_list = []
|
bgneal@8
|
233 let b:qf_window_count = -1
|
bgneal@8
|
234
|
bgneal@8
|
235 python << EOF
|
bgneal@8
|
236 for w in check(vim.current.buffer):
|
bgneal@8
|
237 vim.command('let s:matchDict = {}')
|
bgneal@8
|
238 vim.command("let s:matchDict['lineNum'] = " + str(w.lineno))
|
bgneal@8
|
239 vim.command("let s:matchDict['message'] = '%s'" % vim_quote(w.message % w.message_args))
|
bgneal@8
|
240 vim.command("let b:matchedlines[" + str(w.lineno) + "] = s:matchDict")
|
bgneal@8
|
241
|
bgneal@8
|
242 vim.command("let l:qf_item = {}")
|
bgneal@8
|
243 vim.command("let l:qf_item.bufnr = bufnr('%')")
|
bgneal@8
|
244 vim.command("let l:qf_item.filename = expand('%')")
|
bgneal@8
|
245 vim.command("let l:qf_item.lnum = %s" % str(w.lineno))
|
bgneal@8
|
246 vim.command("let l:qf_item.text = '%s'" % vim_quote(w.message % w.message_args))
|
bgneal@8
|
247 vim.command("let l:qf_item.type = 'E'")
|
bgneal@8
|
248
|
bgneal@8
|
249 if getattr(w, 'col', None) is None or isinstance(w, SyntaxError):
|
bgneal@8
|
250 # without column information, just highlight the whole line
|
bgneal@8
|
251 # (minus the newline)
|
bgneal@8
|
252 vim.command(r"let s:mID = matchadd('PyFlakes', '\%" + str(w.lineno) + r"l\n\@!')")
|
bgneal@8
|
253 else:
|
bgneal@8
|
254 # with a column number, highlight the first keyword there
|
bgneal@8
|
255 vim.command(r"let s:mID = matchadd('PyFlakes', '^\%" + str(w.lineno) + r"l\_.\{-}\zs\k\+\k\@!\%>" + str(w.col) + r"c')")
|
bgneal@8
|
256
|
bgneal@8
|
257 vim.command("let l:qf_item.vcol = 1")
|
bgneal@8
|
258 vim.command("let l:qf_item.col = %s" % str(w.col + 1))
|
bgneal@8
|
259
|
bgneal@8
|
260 vim.command("call add(b:matched, s:matchDict)")
|
bgneal@8
|
261 vim.command("call add(b:qf_list, l:qf_item)")
|
bgneal@8
|
262 EOF
|
bgneal@8
|
263 if g:pyflakes_use_quickfix == 1
|
bgneal@8
|
264 if exists("s:pyflakes_qf")
|
bgneal@8
|
265 " if pyflakes quickfix window is already created, reuse it
|
bgneal@8
|
266 call s:ActivatePyflakesQuickFixWindow()
|
bgneal@8
|
267 call setqflist(b:qf_list, 'r')
|
bgneal@8
|
268 else
|
bgneal@8
|
269 " one pyflakes quickfix window for all buffer
|
bgneal@8
|
270 call setqflist(b:qf_list, '')
|
bgneal@8
|
271 let s:pyflakes_qf = s:GetQuickFixStackCount()
|
bgneal@8
|
272 endif
|
bgneal@8
|
273 endif
|
bgneal@8
|
274
|
bgneal@8
|
275 let b:cleared = 0
|
bgneal@8
|
276 endfunction
|
bgneal@8
|
277 end
|
bgneal@8
|
278
|
bgneal@8
|
279 " keep track of whether or not we are showing a message
|
bgneal@8
|
280 let b:showing_message = 0
|
bgneal@8
|
281
|
bgneal@8
|
282 if !exists("*s:GetPyflakesMessage")
|
bgneal@8
|
283 function s:GetPyflakesMessage()
|
bgneal@8
|
284 let s:cursorPos = getpos(".")
|
bgneal@8
|
285
|
bgneal@8
|
286 " Bail if RunPyflakes hasn't been called yet.
|
bgneal@8
|
287 if !exists('b:matchedlines')
|
bgneal@8
|
288 return
|
bgneal@8
|
289 endif
|
bgneal@8
|
290
|
bgneal@8
|
291 " if there's a message for the line the cursor is currently on, echo
|
bgneal@8
|
292 " it to the console
|
bgneal@8
|
293 if has_key(b:matchedlines, s:cursorPos[1])
|
bgneal@8
|
294 let s:pyflakesMatch = get(b:matchedlines, s:cursorPos[1])
|
bgneal@8
|
295 call s:WideMsg(s:pyflakesMatch['message'])
|
bgneal@8
|
296 let b:showing_message = 1
|
bgneal@8
|
297 return
|
bgneal@8
|
298 endif
|
bgneal@8
|
299
|
bgneal@8
|
300 " otherwise, if we're showing a message, clear it
|
bgneal@8
|
301 if b:showing_message == 1
|
bgneal@8
|
302 echo
|
bgneal@8
|
303 let b:showing_message = 0
|
bgneal@8
|
304 endif
|
bgneal@8
|
305 endfunction
|
bgneal@8
|
306 endif
|
bgneal@8
|
307
|
bgneal@8
|
308 if !exists('*s:ClearPyflakes')
|
bgneal@8
|
309 function s:ClearPyflakes()
|
bgneal@8
|
310 let s:matches = getmatches()
|
bgneal@8
|
311 for s:matchId in s:matches
|
bgneal@8
|
312 if s:matchId['group'] == 'PyFlakes'
|
bgneal@8
|
313 call matchdelete(s:matchId['id'])
|
bgneal@8
|
314 endif
|
bgneal@8
|
315 endfor
|
bgneal@8
|
316 let b:matched = []
|
bgneal@8
|
317 let b:matchedlines = {}
|
bgneal@8
|
318 let b:cleared = 1
|
bgneal@8
|
319 endfunction
|
bgneal@8
|
320 endif
|
bgneal@8
|
321
|