python3complete.vim 21 KB

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