Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

367 lines
13 KiB

5 years ago
  1. """Elpy backend using the Jedi library.
  2. This backend uses the Jedi library:
  3. https://github.com/davidhalter/jedi
  4. """
  5. import sys
  6. import traceback
  7. import re
  8. import jedi
  9. from elpy import rpc
  10. class JediBackend(object):
  11. """The Jedi backend class.
  12. Implements the RPC calls we can pass on to Jedi.
  13. Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html
  14. """
  15. name = "jedi"
  16. def __init__(self, project_root):
  17. self.project_root = project_root
  18. self.completions = {}
  19. sys.path.append(project_root)
  20. def rpc_get_completions(self, filename, source, offset):
  21. line, column = pos_to_linecol(source, offset)
  22. proposals = run_with_debug(jedi, 'completions',
  23. source=source, line=line, column=column,
  24. path=filename, encoding='utf-8')
  25. if proposals is None:
  26. return []
  27. self.completions = dict((proposal.name, proposal)
  28. for proposal in proposals)
  29. return [{'name': proposal.name.rstrip("="),
  30. 'suffix': proposal.complete.rstrip("="),
  31. 'annotation': proposal.type,
  32. 'meta': proposal.description}
  33. for proposal in proposals]
  34. def rpc_get_completion_docstring(self, completion):
  35. proposal = self.completions.get(completion)
  36. if proposal is None:
  37. return None
  38. else:
  39. return proposal.docstring(fast=False)
  40. def rpc_get_completion_location(self, completion):
  41. proposal = self.completions.get(completion)
  42. if proposal is None:
  43. return None
  44. else:
  45. return (proposal.module_path, proposal.line)
  46. def rpc_get_docstring(self, filename, source, offset):
  47. line, column = pos_to_linecol(source, offset)
  48. locations = run_with_debug(jedi, 'goto_definitions',
  49. source=source, line=line, column=column,
  50. path=filename, encoding='utf-8')
  51. if locations and locations[-1].docstring():
  52. return ('Documentation for {0}:\n\n'.format(
  53. locations[-1].full_name) + locations[-1].docstring())
  54. else:
  55. return None
  56. def rpc_get_definition(self, filename, source, offset):
  57. line, column = pos_to_linecol(source, offset)
  58. locations = run_with_debug(jedi, 'goto_definitions',
  59. source=source, line=line, column=column,
  60. path=filename, encoding='utf-8')
  61. # goto_definitions() can return silly stuff like __builtin__
  62. # for int variables, so we fall back on goto() in those
  63. # cases. See issue #76.
  64. if (
  65. locations and
  66. (locations[0].module_path is None
  67. or locations[0].module_name == 'builtins'
  68. or locations[0].module_name == '__builtin__')
  69. ):
  70. locations = run_with_debug(jedi, 'goto_assignments',
  71. source=source, line=line,
  72. column=column,
  73. path=filename, encoding='utf-8')
  74. if not locations:
  75. return None
  76. else:
  77. loc = locations[-1]
  78. try:
  79. if loc.module_path:
  80. if loc.module_path == filename:
  81. offset = linecol_to_pos(source,
  82. loc.line,
  83. loc.column)
  84. else:
  85. with open(loc.module_path) as f:
  86. offset = linecol_to_pos(f.read(),
  87. loc.line,
  88. loc.column)
  89. else:
  90. return None
  91. except IOError:
  92. return None
  93. return (loc.module_path, offset)
  94. def rpc_get_assignment(self, filename, source, offset):
  95. line, column = pos_to_linecol(source, offset)
  96. locations = run_with_debug(jedi, 'goto_assignments',
  97. source=source, line=line, column=column,
  98. path=filename, encoding='utf-8')
  99. if not locations:
  100. return None
  101. else:
  102. loc = locations[-1]
  103. try:
  104. if loc.module_path:
  105. if loc.module_path == filename:
  106. offset = linecol_to_pos(source,
  107. loc.line,
  108. loc.column)
  109. else:
  110. with open(loc.module_path) as f:
  111. offset = linecol_to_pos(f.read(),
  112. loc.line,
  113. loc.column)
  114. else:
  115. return None
  116. except IOError:
  117. return None
  118. return (loc.module_path, offset)
  119. def rpc_get_calltip(self, filename, source, offset):
  120. line, column = pos_to_linecol(source, offset)
  121. calls = run_with_debug(jedi, 'call_signatures',
  122. source=source, line=line, column=column,
  123. path=filename, encoding='utf-8')
  124. if calls:
  125. call = calls[0]
  126. else:
  127. call = None
  128. if not call:
  129. return None
  130. # Strip 'param' added by jedi at the beggining of
  131. # parameter names. Should be unecessary for jedi > 0.13.0
  132. params = [re.sub("^param ", '', param.description)
  133. for param in call.params]
  134. return {"name": call.name,
  135. "index": call.index,
  136. "params": params}
  137. def rpc_get_oneline_docstring(self, filename, source, offset):
  138. """Return a oneline docstring for the symbol at offset"""
  139. line, column = pos_to_linecol(source, offset)
  140. definitions = run_with_debug(jedi, 'goto_definitions',
  141. source=source, line=line, column=column,
  142. path=filename, encoding='utf-8')
  143. assignments = run_with_debug(jedi, 'goto_assignments',
  144. source=source, line=line, column=column,
  145. path=filename, encoding='utf-8')
  146. if definitions:
  147. definition = definitions[0]
  148. else:
  149. definition = None
  150. if assignments:
  151. assignment = assignments[0]
  152. else:
  153. assignment = None
  154. if definition:
  155. # Get name
  156. if definition.type in ['function', 'class']:
  157. raw_name = definition.name
  158. name = '{}()'.format(raw_name)
  159. doc = definition.docstring().split('\n')
  160. elif definition.type in ['module']:
  161. raw_name = definition.name
  162. name = '{} {}'.format(raw_name, definition.type)
  163. doc = definition.docstring().split('\n')
  164. elif (definition.type in ['instance']
  165. and hasattr(assignment, "name")):
  166. raw_name = assignment.name
  167. name = raw_name
  168. doc = assignment.docstring().split('\n')
  169. else:
  170. return None
  171. # Keep only the first paragraph that is not a function declaration
  172. lines = []
  173. call = "{}(".format(raw_name)
  174. # last line
  175. doc.append('')
  176. for i in range(len(doc)):
  177. if doc[i] == '' and len(lines) != 0:
  178. paragraph = " ".join(lines)
  179. lines = []
  180. if call != paragraph[0:len(call)]:
  181. break
  182. paragraph = ""
  183. continue
  184. lines.append(doc[i])
  185. # Keep only the first sentence
  186. onelinedoc = paragraph.split('. ', 1)
  187. if len(onelinedoc) == 2:
  188. onelinedoc = onelinedoc[0] + '.'
  189. else:
  190. onelinedoc = onelinedoc[0]
  191. if onelinedoc == '':
  192. onelinedoc = "No documentation"
  193. return {"name": name,
  194. "doc": onelinedoc}
  195. return None
  196. def rpc_get_usages(self, filename, source, offset):
  197. """Return the uses of the symbol at offset.
  198. Returns a list of occurrences of the symbol, as dicts with the
  199. fields name, filename, and offset.
  200. """
  201. line, column = pos_to_linecol(source, offset)
  202. uses = run_with_debug(jedi, 'usages',
  203. source=source, line=line, column=column,
  204. path=filename, encoding='utf-8')
  205. if uses is None:
  206. return None
  207. result = []
  208. for use in uses:
  209. if use.module_path == filename:
  210. offset = linecol_to_pos(source, use.line, use.column)
  211. elif use.module_path is not None:
  212. with open(use.module_path) as f:
  213. text = f.read()
  214. offset = linecol_to_pos(text, use.line, use.column)
  215. result.append({"name": use.name,
  216. "filename": use.module_path,
  217. "offset": offset})
  218. return result
  219. def rpc_get_names(self, filename, source, offset):
  220. """Return the list of possible names"""
  221. names = jedi.api.names(source=source,
  222. path=filename, encoding='utf-8',
  223. all_scopes=True,
  224. definitions=True,
  225. references=True)
  226. result = []
  227. for name in names:
  228. if name.module_path == filename:
  229. offset = linecol_to_pos(source, name.line, name.column)
  230. elif name.module_path is not None:
  231. with open(name.module_path) as f:
  232. text = f.read()
  233. offset = linecol_to_pos(text, name.line, name.column)
  234. result.append({"name": name.name,
  235. "filename": name.module_path,
  236. "offset": offset})
  237. return result
  238. # From the Jedi documentation:
  239. #
  240. # line is the current line you want to perform actions on (starting
  241. # with line #1 as the first line). column represents the current
  242. # column/indent of the cursor (starting with zero). source_path
  243. # should be the path of your file in the file system.
  244. def pos_to_linecol(text, pos):
  245. """Return a tuple of line and column for offset pos in text.
  246. Lines are one-based, columns zero-based.
  247. This is how Jedi wants it. Don't ask me why.
  248. """
  249. line_start = text.rfind("\n", 0, pos) + 1
  250. line = text.count("\n", 0, line_start) + 1
  251. col = pos - line_start
  252. return line, col
  253. def linecol_to_pos(text, line, col):
  254. """Return the offset of this line and column in text.
  255. Lines are one-based, columns zero-based.
  256. This is how Jedi wants it. Don't ask me why.
  257. """
  258. nth_newline_offset = 0
  259. for i in range(line - 1):
  260. new_offset = text.find("\n", nth_newline_offset)
  261. if new_offset < 0:
  262. raise ValueError("Text does not have {0} lines."
  263. .format(line))
  264. nth_newline_offset = new_offset + 1
  265. offset = nth_newline_offset + col
  266. if offset > len(text):
  267. raise ValueError("Line {0} column {1} is not within the text"
  268. .format(line, col))
  269. return offset
  270. def run_with_debug(jedi, name, *args, **kwargs):
  271. re_raise = kwargs.pop('re_raise', ())
  272. try:
  273. script = jedi.Script(*args, **kwargs)
  274. return getattr(script, name)()
  275. except Exception as e:
  276. if isinstance(e, re_raise):
  277. raise
  278. # Bug jedi#485
  279. if (
  280. isinstance(e, ValueError) and
  281. "invalid \\x escape" in str(e)
  282. ):
  283. return None
  284. # Bug jedi#485 in Python 3
  285. if (
  286. isinstance(e, SyntaxError) and
  287. "truncated \\xXX escape" in str(e)
  288. ):
  289. return None
  290. from jedi import debug
  291. debug_info = []
  292. def _debug(level, str_out):
  293. if level == debug.NOTICE:
  294. prefix = "[N]"
  295. elif level == debug.WARNING:
  296. prefix = "[W]"
  297. else:
  298. prefix = "[?]"
  299. debug_info.append(u"{0} {1}".format(prefix, str_out))
  300. jedi.set_debug_function(_debug, speed=False)
  301. try:
  302. script = jedi.Script(*args, **kwargs)
  303. return getattr(script, name)()
  304. except Exception as e:
  305. source = kwargs.get('source')
  306. sc_args = []
  307. sc_args.extend(repr(arg) for arg in args)
  308. sc_args.extend("{0}={1}".format(k, "source" if k == "source"
  309. else repr(v))
  310. for (k, v) in kwargs.items())
  311. data = {
  312. "traceback": traceback.format_exc(),
  313. "jedi_debug_info": {'script_args': ", ".join(sc_args),
  314. 'source': source,
  315. 'method': name,
  316. 'debug_info': debug_info}
  317. }
  318. raise rpc.Fault(message=str(e),
  319. code=500,
  320. data=data)
  321. finally:
  322. jedi.set_debug_function(None)