pythoncomplete.vim 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. "pythoncomplete.vim - Omni Completion for python
  2. " Maintainer: <vacancy>
  3. " Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
  4. " Version: 0.9
  5. " Last Updated: 2020 Oct 9
  6. "
  7. " Changes
  8. " TODO:
  9. " 'info' item output can use some formatting work
  10. " Add an "unsafe eval" mode, to allow for return type evaluation
  11. " Complete basic syntax along with import statements
  12. " i.e. "import url<c-x,c-o>"
  13. " Continue parsing on invalid line??
  14. "
  15. " v 0.9
  16. " * Fixed docstring parsing for classes and functions
  17. " * Fixed parsing of *args and **kwargs type arguments
  18. " * Better function param parsing to handle things like tuples and
  19. " lambda defaults args
  20. "
  21. " v 0.8
  22. " * Fixed an issue where the FIRST assignment was always used instead of
  23. " using a subsequent assignment for a variable
  24. " * Fixed a scoping issue when working inside a parameterless function
  25. "
  26. "
  27. " v 0.7
  28. " * Fixed function list sorting (_ and __ at the bottom)
  29. " * Removed newline removal from docs. It appears vim handles these better in
  30. " recent patches
  31. "
  32. " v 0.6:
  33. " * Fixed argument completion
  34. " * Removed the 'kind' completions, as they are better indicated
  35. " with real syntax
  36. " * Added tuple assignment parsing (whoops, that was forgotten)
  37. " * Fixed import handling when flattening scope
  38. "
  39. " v 0.5:
  40. " Yeah, I skipped a version number - 0.4 was never public.
  41. " It was a bugfix version on top of 0.3. This is a complete
  42. " rewrite.
  43. "
  44. if !has('python')
  45. echo 'Error: Requires python + pynvim. :help provider-python'
  46. finish
  47. endif
  48. function! pythoncomplete#Complete(findstart, base)
  49. "findstart = 1 when we need to get the text length
  50. if a:findstart == 1
  51. let line = getline('.')
  52. let idx = col('.')
  53. while idx > 0
  54. let idx -= 1
  55. let c = line[idx]
  56. if c =~ '\w'
  57. continue
  58. elseif ! c =~ '\.'
  59. let idx = -1
  60. break
  61. else
  62. break
  63. endif
  64. endwhile
  65. return idx
  66. "findstart = 0 when we need to return the list of completions
  67. else
  68. "vim no longer moves the cursor upon completion... fix that
  69. let line = getline('.')
  70. let idx = col('.')
  71. let cword = ''
  72. while idx > 0
  73. let idx -= 1
  74. let c = line[idx]
  75. if c =~ '\w' || c =~ '\.'
  76. let cword = c . cword
  77. continue
  78. elseif strlen(cword) > 0 || idx == 0
  79. break
  80. endif
  81. endwhile
  82. execute "python vimcomplete('" . escape(cword, "'") . "', '" . escape(a:base, "'") . "')"
  83. return g:pythoncomplete_completions
  84. endif
  85. endfunction
  86. function! s:DefPython()
  87. python << PYTHONEOF
  88. import sys, tokenize, cStringIO, types
  89. from token import NAME, DEDENT, NEWLINE, STRING
  90. debugstmts=[]
  91. def dbg(s): debugstmts.append(s)
  92. def showdbg():
  93. for d in debugstmts: print "DBG: %s " % d
  94. def vimcomplete(context,match):
  95. global debugstmts
  96. debugstmts = []
  97. try:
  98. import vim
  99. def complsort(x,y):
  100. try:
  101. xa = x['abbr']
  102. ya = y['abbr']
  103. if xa[0] == '_':
  104. if xa[1] == '_' and ya[0:2] == '__':
  105. return xa > ya
  106. elif ya[0:2] == '__':
  107. return -1
  108. elif y[0] == '_':
  109. return xa > ya
  110. else:
  111. return 1
  112. elif ya[0] == '_':
  113. return -1
  114. else:
  115. return xa > ya
  116. except:
  117. return 0
  118. cmpl = Completer()
  119. cmpl.evalsource('\n'.join(vim.current.buffer),vim.eval("line('.')"))
  120. all = cmpl.get_completions(context,match)
  121. all.sort(complsort)
  122. dictstr = '['
  123. # have to do this for double quoting
  124. for cmpl in all:
  125. dictstr += '{'
  126. for x in cmpl: dictstr += '"%s":"%s",' % (x,cmpl[x])
  127. dictstr += '"icase":0},'
  128. if dictstr[-1] == ',': dictstr = dictstr[:-1]
  129. dictstr += ']'
  130. #dbg("dict: %s" % dictstr)
  131. vim.command("silent let g:pythoncomplete_completions = %s" % dictstr)
  132. #dbg("Completion dict:\n%s" % all)
  133. except vim.error:
  134. dbg("VIM Error: %s" % vim.error)
  135. class Completer(object):
  136. def __init__(self):
  137. self.compldict = {}
  138. self.parser = PyParser()
  139. def evalsource(self,text,line=0):
  140. sc = self.parser.parse(text,line)
  141. src = sc.get_code()
  142. dbg("source: %s" % src)
  143. try: exec(src) in self.compldict
  144. except: dbg("parser: %s, %s" % (sys.exc_info()[0],sys.exc_info()[1]))
  145. for l in sc.locals:
  146. try: exec(l) in self.compldict
  147. except: dbg("locals: %s, %s [%s]" % (sys.exc_info()[0],sys.exc_info()[1],l))
  148. def _cleanstr(self,doc):
  149. return doc.replace('"',' ').replace("'",' ')
  150. def get_arguments(self,func_obj):
  151. def _ctor(obj):
  152. try: return class_ob.__init__.im_func
  153. except AttributeError:
  154. for base in class_ob.__bases__:
  155. rc = _find_constructor(base)
  156. if rc is not None: return rc
  157. return None
  158. arg_offset = 1
  159. if type(func_obj) == types.ClassType: func_obj = _ctor(func_obj)
  160. elif type(func_obj) == types.MethodType: func_obj = func_obj.im_func
  161. else: arg_offset = 0
  162. arg_text=''
  163. if type(func_obj) in [types.FunctionType, types.LambdaType]:
  164. try:
  165. cd = func_obj.func_code
  166. real_args = cd.co_varnames[arg_offset:cd.co_argcount]
  167. defaults = func_obj.func_defaults or ''
  168. defaults = map(lambda name: "=%s" % name, defaults)
  169. defaults = [""] * (len(real_args)-len(defaults)) + defaults
  170. items = map(lambda a,d: a+d, real_args, defaults)
  171. if func_obj.func_code.co_flags & 0x4:
  172. items.append("...")
  173. if func_obj.func_code.co_flags & 0x8:
  174. items.append("***")
  175. arg_text = (','.join(items)) + ')'
  176. except:
  177. dbg("arg completion: %s: %s" % (sys.exc_info()[0],sys.exc_info()[1]))
  178. pass
  179. if len(arg_text) == 0:
  180. # The doc string sometimes contains the function signature
  181. # this works for a lot of C modules that are part of the
  182. # standard library
  183. doc = func_obj.__doc__
  184. if doc:
  185. doc = doc.lstrip()
  186. pos = doc.find('\n')
  187. if pos > 0:
  188. sigline = doc[:pos]
  189. lidx = sigline.find('(')
  190. ridx = sigline.find(')')
  191. if lidx > 0 and ridx > 0:
  192. arg_text = sigline[lidx+1:ridx] + ')'
  193. if len(arg_text) == 0: arg_text = ')'
  194. return arg_text
  195. def get_completions(self,context,match):
  196. dbg("get_completions('%s','%s')" % (context,match))
  197. stmt = ''
  198. if context: stmt += str(context)
  199. if match: stmt += str(match)
  200. try:
  201. result = None
  202. all = {}
  203. ridx = stmt.rfind('.')
  204. if len(stmt) > 0 and stmt[-1] == '(':
  205. result = eval(_sanitize(stmt[:-1]), self.compldict)
  206. doc = result.__doc__
  207. if doc is None: doc = ''
  208. args = self.get_arguments(result)
  209. return [{'word':self._cleanstr(args),'info':self._cleanstr(doc)}]
  210. elif ridx == -1:
  211. match = stmt
  212. all = self.compldict
  213. else:
  214. match = stmt[ridx+1:]
  215. stmt = _sanitize(stmt[:ridx])
  216. result = eval(stmt, self.compldict)
  217. all = dir(result)
  218. dbg("completing: stmt:%s" % stmt)
  219. completions = []
  220. try: maindoc = result.__doc__
  221. except: maindoc = ' '
  222. if maindoc is None: maindoc = ' '
  223. for m in all:
  224. if m == "_PyCmplNoType": continue #this is internal
  225. try:
  226. dbg('possible completion: %s' % m)
  227. if m.find(match) == 0:
  228. if result is None: inst = all[m]
  229. else: inst = getattr(result,m)
  230. try: doc = inst.__doc__
  231. except: doc = maindoc
  232. typestr = str(inst)
  233. if doc is None or doc == '': doc = maindoc
  234. wrd = m[len(match):]
  235. c = {'word':wrd, 'abbr':m, 'info':self._cleanstr(doc)}
  236. if "function" in typestr:
  237. c['word'] += '('
  238. c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
  239. elif "method" in typestr:
  240. c['word'] += '('
  241. c['abbr'] += '(' + self._cleanstr(self.get_arguments(inst))
  242. elif "module" in typestr:
  243. c['word'] += '.'
  244. elif "class" in typestr:
  245. c['word'] += '('
  246. c['abbr'] += '('
  247. completions.append(c)
  248. except:
  249. i = sys.exc_info()
  250. dbg("inner completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
  251. return completions
  252. except:
  253. i = sys.exc_info()
  254. dbg("completion: %s,%s [stmt='%s']" % (i[0],i[1],stmt))
  255. return []
  256. class Scope(object):
  257. def __init__(self,name,indent,docstr=''):
  258. self.subscopes = []
  259. self.docstr = docstr
  260. self.locals = []
  261. self.parent = None
  262. self.name = name
  263. self.indent = indent
  264. def add(self,sub):
  265. #print 'push scope: [%s@%s]' % (sub.name,sub.indent)
  266. sub.parent = self
  267. self.subscopes.append(sub)
  268. return sub
  269. def doc(self,str):
  270. """ Clean up a docstring """
  271. d = str.replace('\n',' ')
  272. d = d.replace('\t',' ')
  273. while d.find(' ') > -1: d = d.replace(' ',' ')
  274. while d[0] in '"\'\t ': d = d[1:]
  275. while d[-1] in '"\'\t ': d = d[:-1]
  276. dbg("Scope(%s)::docstr = %s" % (self,d))
  277. self.docstr = d
  278. def local(self,loc):
  279. self._checkexisting(loc)
  280. self.locals.append(loc)
  281. def copy_decl(self,indent=0):
  282. """ Copy a scope's declaration only, at the specified indent level - not local variables """
  283. return Scope(self.name,indent,self.docstr)
  284. def _checkexisting(self,test):
  285. "Convienance function... keep out duplicates"
  286. if test.find('=') > -1:
  287. var = test.split('=')[0].strip()
  288. for l in self.locals:
  289. if l.find('=') > -1 and var == l.split('=')[0].strip():
  290. self.locals.remove(l)
  291. def get_code(self):
  292. str = ""
  293. if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n'
  294. for l in self.locals:
  295. if l.startswith('import'): str += l+'\n'
  296. str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n'
  297. for sub in self.subscopes:
  298. str += sub.get_code()
  299. for l in self.locals:
  300. if not l.startswith('import'): str += l+'\n'
  301. return str
  302. def pop(self,indent):
  303. #print 'pop scope: [%s] to [%s]' % (self.indent,indent)
  304. outer = self
  305. while outer.parent != None and outer.indent >= indent:
  306. outer = outer.parent
  307. return outer
  308. def currentindent(self):
  309. #print 'parse current indent: %s' % self.indent
  310. return ' '*self.indent
  311. def childindent(self):
  312. #print 'parse child indent: [%s]' % (self.indent+1)
  313. return ' '*(self.indent+1)
  314. class Class(Scope):
  315. def __init__(self, name, supers, indent, docstr=''):
  316. Scope.__init__(self,name,indent, docstr)
  317. self.supers = supers
  318. def copy_decl(self,indent=0):
  319. c = Class(self.name,self.supers,indent, self.docstr)
  320. for s in self.subscopes:
  321. c.add(s.copy_decl(indent+1))
  322. return c
  323. def get_code(self):
  324. str = '%sclass %s' % (self.currentindent(),self.name)
  325. if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers)
  326. str += ':\n'
  327. if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
  328. if len(self.subscopes) > 0:
  329. for s in self.subscopes: str += s.get_code()
  330. else:
  331. str += '%spass\n' % self.childindent()
  332. return str
  333. class Function(Scope):
  334. def __init__(self, name, params, indent, docstr=''):
  335. Scope.__init__(self,name,indent, docstr)
  336. self.params = params
  337. def copy_decl(self,indent=0):
  338. return Function(self.name,self.params,indent, self.docstr)
  339. def get_code(self):
  340. str = "%sdef %s(%s):\n" % \
  341. (self.currentindent(),self.name,','.join(self.params))
  342. if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
  343. str += "%spass\n" % self.childindent()
  344. return str
  345. class PyParser:
  346. def __init__(self):
  347. self.top = Scope('global',0)
  348. self.scope = self.top
  349. self.parserline = 0
  350. def _parsedotname(self,pre=None):
  351. #returns (dottedname, nexttoken)
  352. name = []
  353. if pre is None:
  354. tokentype, token, indent = self.next()
  355. if tokentype != NAME and token != '*':
  356. return ('', token)
  357. else: token = pre
  358. name.append(token)
  359. while True:
  360. tokentype, token, indent = self.next()
  361. if token != '.': break
  362. tokentype, token, indent = self.next()
  363. if tokentype != NAME: break
  364. name.append(token)
  365. return (".".join(name), token)
  366. def _parseimportlist(self):
  367. imports = []
  368. while True:
  369. name, token = self._parsedotname()
  370. if not name: break
  371. name2 = ''
  372. if token == 'as': name2, token = self._parsedotname()
  373. imports.append((name, name2))
  374. while token != "," and "\n" not in token:
  375. tokentype, token, indent = self.next()
  376. if token != ",": break
  377. return imports
  378. def _parenparse(self):
  379. name = ''
  380. names = []
  381. level = 1
  382. while True:
  383. tokentype, token, indent = self.next()
  384. if token in (')', ',') and level == 1:
  385. if '=' not in name: name = name.replace(' ', '')
  386. names.append(name.strip())
  387. name = ''
  388. if token == '(':
  389. level += 1
  390. name += "("
  391. elif token == ')':
  392. level -= 1
  393. if level == 0: break
  394. else: name += ")"
  395. elif token == ',' and level == 1:
  396. pass
  397. else:
  398. name += "%s " % str(token)
  399. return names
  400. def _parsefunction(self,indent):
  401. self.scope=self.scope.pop(indent)
  402. tokentype, fname, ind = self.next()
  403. if tokentype != NAME: return None
  404. tokentype, open, ind = self.next()
  405. if open != '(': return None
  406. params=self._parenparse()
  407. tokentype, colon, ind = self.next()
  408. if colon != ':': return None
  409. return Function(fname,params,indent)
  410. def _parseclass(self,indent):
  411. self.scope=self.scope.pop(indent)
  412. tokentype, cname, ind = self.next()
  413. if tokentype != NAME: return None
  414. super = []
  415. tokentype, next, ind = self.next()
  416. if next == '(':
  417. super=self._parenparse()
  418. elif next != ':': return None
  419. return Class(cname,super,indent)
  420. def _parseassignment(self):
  421. assign=''
  422. tokentype, token, indent = self.next()
  423. if tokentype == tokenize.STRING or token == 'str':
  424. return '""'
  425. elif token == '(' or token == 'tuple':
  426. return '()'
  427. elif token == '[' or token == 'list':
  428. return '[]'
  429. elif token == '{' or token == 'dict':
  430. return '{}'
  431. elif tokentype == tokenize.NUMBER:
  432. return '0'
  433. elif token == 'open' or token == 'file':
  434. return 'file'
  435. elif token == 'None':
  436. return '_PyCmplNoType()'
  437. elif token == 'type':
  438. return 'type(_PyCmplNoType)' #only for method resolution
  439. else:
  440. assign += token
  441. level = 0
  442. while True:
  443. tokentype, token, indent = self.next()
  444. if token in ('(','{','['):
  445. level += 1
  446. elif token in (']','}',')'):
  447. level -= 1
  448. if level == 0: break
  449. elif level == 0:
  450. if token in (';','\n'): break
  451. assign += token
  452. return "%s" % assign
  453. def next(self):
  454. type, token, (lineno, indent), end, self.parserline = self.gen.next()
  455. if lineno == self.curline:
  456. #print 'line found [%s] scope=%s' % (line.replace('\n',''),self.scope.name)
  457. self.currentscope = self.scope
  458. return (type, token, indent)
  459. def _adjustvisibility(self):
  460. newscope = Scope('result',0)
  461. scp = self.currentscope
  462. while scp != None:
  463. if type(scp) == Function:
  464. slice = 0
  465. #Handle 'self' params
  466. if scp.parent != None and type(scp.parent) == Class:
  467. slice = 1
  468. newscope.local('%s = %s' % (scp.params[0],scp.parent.name))
  469. for p in scp.params[slice:]:
  470. i = p.find('=')
  471. if len(p) == 0: continue
  472. pvar = ''
  473. ptype = ''
  474. if i == -1:
  475. pvar = p
  476. ptype = '_PyCmplNoType()'
  477. else:
  478. pvar = p[:i]
  479. ptype = _sanitize(p[i+1:])
  480. if pvar.startswith('**'):
  481. pvar = pvar[2:]
  482. ptype = '{}'
  483. elif pvar.startswith('*'):
  484. pvar = pvar[1:]
  485. ptype = '[]'
  486. newscope.local('%s = %s' % (pvar,ptype))
  487. for s in scp.subscopes:
  488. ns = s.copy_decl(0)
  489. newscope.add(ns)
  490. for l in scp.locals: newscope.local(l)
  491. scp = scp.parent
  492. self.currentscope = newscope
  493. return self.currentscope
  494. #p.parse(vim.current.buffer[:],vim.eval("line('.')"))
  495. def parse(self,text,curline=0):
  496. self.curline = int(curline)
  497. buf = cStringIO.StringIO(''.join(text) + '\n')
  498. self.gen = tokenize.generate_tokens(buf.readline)
  499. self.currentscope = self.scope
  500. try:
  501. freshscope=True
  502. while True:
  503. tokentype, token, indent = self.next()
  504. #dbg( 'main: token=[%s] indent=[%s]' % (token,indent))
  505. if tokentype == DEDENT or token == "pass":
  506. self.scope = self.scope.pop(indent)
  507. elif token == 'def':
  508. func = self._parsefunction(indent)
  509. if func is None:
  510. print "function: syntax error..."
  511. continue
  512. dbg("new scope: function")
  513. freshscope = True
  514. self.scope = self.scope.add(func)
  515. elif token == 'class':
  516. cls = self._parseclass(indent)
  517. if cls is None:
  518. print "class: syntax error..."
  519. continue
  520. freshscope = True
  521. dbg("new scope: class")
  522. self.scope = self.scope.add(cls)
  523. elif token == 'import':
  524. imports = self._parseimportlist()
  525. for mod, alias in imports:
  526. loc = "import %s" % mod
  527. if len(alias) > 0: loc += " as %s" % alias
  528. self.scope.local(loc)
  529. freshscope = False
  530. elif token == 'from':
  531. mod, token = self._parsedotname()
  532. if not mod or token != "import":
  533. print "from: syntax error..."
  534. continue
  535. names = self._parseimportlist()
  536. for name, alias in names:
  537. loc = "from %s import %s" % (mod,name)
  538. if len(alias) > 0: loc += " as %s" % alias
  539. self.scope.local(loc)
  540. freshscope = False
  541. elif tokentype == STRING:
  542. if freshscope: self.scope.doc(token)
  543. elif tokentype == NAME:
  544. name,token = self._parsedotname(token)
  545. if token == '=':
  546. stmt = self._parseassignment()
  547. dbg("parseassignment: %s = %s" % (name, stmt))
  548. if stmt != None:
  549. self.scope.local("%s = %s" % (name,stmt))
  550. freshscope = False
  551. except StopIteration: #thrown on EOF
  552. pass
  553. except:
  554. dbg("parse error: %s, %s @ %s" %
  555. (sys.exc_info()[0], sys.exc_info()[1], self.parserline))
  556. return self._adjustvisibility()
  557. def _sanitize(str):
  558. val = ''
  559. level = 0
  560. for c in str:
  561. if c in ('(','{','['):
  562. level += 1
  563. elif c in (']','}',')'):
  564. level -= 1
  565. elif level == 0:
  566. val += c
  567. return val
  568. sys.path.extend(['.','..'])
  569. PYTHONEOF
  570. endfunction
  571. call s:DefPython()
  572. " vim: set et ts=4: