ropevim.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. """ropevim, a vim mode for using rope refactoring library"""
  2. import glob
  3. import os
  4. import tempfile
  5. import re
  6. from pylibs.ropemode import decorators
  7. from pylibs.ropemode import environment
  8. from pylibs.ropemode import interface
  9. import vim
  10. # Gobal var to be able to shutup output
  11. _rope_quiet = False
  12. class VimUtils(environment.Environment):
  13. def __init__(self, *args, **kwargs):
  14. super(VimUtils, self).__init__(*args, **kwargs)
  15. self.completeopt = vim.eval('&completeopt')
  16. self.preview = 'preview' in self.completeopt
  17. def ask(self, prompt, default=None, starting=None):
  18. if starting is None:
  19. starting = ''
  20. if default is not None:
  21. prompt = prompt + '[{0}] '.format(default)
  22. result = call('input("{0}", "{1}")'.format(prompt, starting))
  23. if default is not None and result == '':
  24. return default
  25. return result
  26. def ask_values(self, prompt, values, default=None,
  27. starting=None, show_values=None):
  28. if show_values or (show_values is None and len(values) < 14):
  29. self._print_values(values)
  30. if default is not None:
  31. prompt = prompt + '[{0}] '.format(default)
  32. starting = starting or ''
  33. _completer.values = values
  34. answer = call(
  35. 'input("{0}", "{1}", "customlist,RopeValueCompleter")'.format(
  36. prompt, starting
  37. )
  38. )
  39. if answer is None:
  40. if 'cancel' in values:
  41. return 'cancel'
  42. return
  43. if default is not None and not answer:
  44. return default
  45. if answer.isdigit() and 0 <= int(answer) < len(values):
  46. return values[int(answer)]
  47. return answer
  48. def _print_values(self, values):
  49. numbered = []
  50. for index, value in enumerate(values):
  51. numbered.append('%s. %s' % (index, str(value)))
  52. echo('\n'.join(numbered) + '\n')
  53. def ask_directory(self, prompt, default=None, starting=None):
  54. return call('input("{0}", ".", "dir")'.format(prompt))
  55. def _update_proposals(self, values):
  56. self.completeopt = vim.eval('&completeopt')
  57. self.preview = 'preview' in self.completeopt
  58. if not self.get('extended_complete'):
  59. return u','.join(u"'{0}'".format(self._completion_text(proposal))
  60. for proposal in values)
  61. return u','.join(self._extended_completion(proposal)
  62. for proposal in values)
  63. def _command(self, command, encode=False):
  64. if encode:
  65. command = command.encode(self._get_encoding())
  66. vim.command(command)
  67. def ask_completion(self, prompt, values, starting=None):
  68. if self.get('vim_completion') and 'i' in call('mode()'):
  69. proposals = self._update_proposals(values)
  70. col = int(call('col(".")'))
  71. if starting:
  72. col -= len(starting)
  73. self._command(u'call complete({0}, [{1}])'.format(col, proposals),
  74. encode=True)
  75. return None
  76. return self.ask_values(prompt, values, starting=starting,
  77. show_values=False)
  78. def message(self, message):
  79. echo(message)
  80. def yes_or_no(self, prompt):
  81. return self.ask_values(prompt, ['yes', 'no']) == 'yes'
  82. def y_or_n(self, prompt):
  83. return self.yes_or_no(prompt)
  84. def get(self, name, default=None):
  85. vimname = 'g:pymode_rope_{0}'.format(name)
  86. if str(vim.eval('exists("{0}")'.format(vimname))) == '0':
  87. return default
  88. result = vim.eval(vimname)
  89. if isinstance(result, str) and result.isdigit():
  90. return int(result)
  91. return result
  92. def get_offset(self):
  93. result = self._position_to_offset(*self.cursor)
  94. return result
  95. @staticmethod
  96. def _get_encoding():
  97. return vim.eval('&encoding') or 'utf-8'
  98. def _encode_line(self, line):
  99. return line.encode(self._get_encoding())
  100. def _decode_line(self, line):
  101. return line.decode(self._get_encoding())
  102. def _position_to_offset(self, lineno, colno):
  103. result = min(colno, len(self.buffer[lineno - 1]) + 1)
  104. for line in self.buffer[:lineno - 1]:
  105. line = self._decode_line(line)
  106. result += len(line) + 1
  107. return result
  108. def get_text(self):
  109. return self._decode_line('\n'.join(self.buffer)) + u'\n'
  110. def get_region(self):
  111. start = self._position_to_offset(*self.buffer.mark('<'))
  112. end = self._position_to_offset(*self.buffer.mark('>'))
  113. return start, end
  114. @property
  115. def buffer(self):
  116. return vim.current.buffer
  117. def _get_cursor(self):
  118. lineno, col = vim.current.window.cursor
  119. line = self._decode_line(vim.current.line[:col])
  120. col = len(line)
  121. return (lineno, col)
  122. def _set_cursor(self, cursor):
  123. lineno, col = cursor
  124. line = self._decode_line(vim.current.line)
  125. line = self._encode_line(line[:col])
  126. col = len(line)
  127. vim.current.window.cursor = (lineno, col)
  128. cursor = property(_get_cursor, _set_cursor)
  129. @staticmethod
  130. def get_cur_dir():
  131. return vim.eval('getcwd()')
  132. def filename(self):
  133. return self.buffer.name
  134. def is_modified(self):
  135. return vim.eval('&modified')
  136. def goto_line(self, lineno):
  137. self.cursor = (lineno, 0)
  138. def insert_line(self, line, lineno):
  139. self.buffer[lineno - 1:lineno - 1] = [line]
  140. def insert(self, text):
  141. lineno, colno = self.cursor
  142. line = self.buffer[lineno - 1]
  143. self.buffer[lineno - 1] = line[:colno] + text + line[colno:]
  144. self.cursor = (lineno, colno + len(text))
  145. def delete(self, start, end):
  146. lineno1, colno1 = self._offset_to_position(start - 1)
  147. lineno2, colno2 = self._offset_to_position(end - 1)
  148. lineno, colno = self.cursor
  149. if lineno1 == lineno2:
  150. line = self.buffer[lineno1 - 1]
  151. self.buffer[lineno1 - 1] = line[:colno1] + line[colno2:]
  152. if lineno == lineno1 and colno >= colno1:
  153. diff = colno2 - colno1
  154. self.cursor = (lineno, max(0, colno - diff))
  155. def _offset_to_position(self, offset):
  156. text = self.get_text()
  157. lineno = text.count('\n', 0, offset) + 1
  158. try:
  159. colno = offset - text.rindex('\n', 0, offset) - 1
  160. except ValueError:
  161. colno = offset
  162. return lineno, colno
  163. def filenames(self):
  164. result = []
  165. for buffer in vim.buffers:
  166. if buffer.name:
  167. result.append(buffer.name)
  168. return result
  169. def save_files(self, filenames):
  170. vim.command('wall')
  171. def reload_files(self, filenames, moves={}):
  172. initial = self.filename()
  173. for filename in filenames:
  174. self.find_file(moves.get(filename, filename), force=True)
  175. if initial:
  176. self.find_file(initial)
  177. def find_file(self, filename, readonly=False, other=False, force=False):
  178. if filename != self.filename() or force:
  179. if other:
  180. vim.command(other)
  181. filename = '\\ '.join(s.rstrip() for s in filename.split())
  182. vim.command('e %s' % filename)
  183. if readonly:
  184. vim.command('set nomodifiable')
  185. def create_progress(self, name):
  186. return VimProgress(name)
  187. def current_word(self):
  188. return vim.eval('expand("<cword>")')
  189. def push_mark(self):
  190. vim.command('mark `')
  191. def prefix_value(self, prefix):
  192. return prefix
  193. def show_occurrences(self, locations):
  194. self._quickfixdefs(locations)
  195. vim.command('cwindow')
  196. def _quickfixdefs(self, locations):
  197. filename = os.path.join(tempfile.gettempdir(), tempfile.mktemp())
  198. try:
  199. self._writedefs(locations, filename)
  200. vim.command('let old_errorfile = &errorfile')
  201. vim.command('let old_errorformat = &errorformat')
  202. vim.command('set errorformat=%f:%l:\ %m')
  203. vim.command('cfile ' + filename)
  204. vim.command('let &errorformat = old_errorformat')
  205. vim.command('let &errorfile = old_errorfile')
  206. finally:
  207. os.remove(filename)
  208. def _writedefs(self, locations, filename):
  209. tofile = open(filename, 'w')
  210. try:
  211. for location in locations:
  212. err = '%s:%d: - %s\n' % (location.filename,
  213. location.lineno, location.note)
  214. echo(err)
  215. tofile.write(err)
  216. finally:
  217. tofile.close()
  218. def show_doc(self, docs, altview=False):
  219. if docs:
  220. vim.command(
  221. 'call pymode#ShowStr("{0}")'.format(docs.replace('"', '\\"'))
  222. )
  223. def preview_changes(self, diffs):
  224. echo(diffs)
  225. return self.y_or_n('Do the changes? ')
  226. def local_command(self, name, callback, key=None, prefix=False):
  227. self._add_command(name, callback, key, prefix,
  228. prekey=self.get('local_prefix'))
  229. def global_command(self, name, callback, key=None, prefix=False):
  230. self._add_command(name, callback, key, prefix,
  231. prekey=self.get('global_prefix'))
  232. def add_hook(self, name, callback, hook):
  233. mapping = {'before_save': 'FileWritePre,BufWritePre',
  234. 'after_save': 'FileWritePost,BufWritePost',
  235. 'exit': 'VimLeave'}
  236. self._add_function(name, callback)
  237. vim.command(
  238. 'autocmd {0} *.py call {1}()'.format(
  239. mapping[hook], _vim_name(name)
  240. )
  241. )
  242. def _add_command(self, name, callback, key, prefix, prekey):
  243. self._add_function(name, callback, prefix)
  244. vim.command(
  245. 'command! -range {0} call {1}()'.format(
  246. _vim_name(name), _vim_name(name)
  247. )
  248. )
  249. if key is not None:
  250. key = prekey + key.replace(' ', '')
  251. vim.command(
  252. 'noremap {0} :call {1}()<cr>'.format(key, _vim_name(name))
  253. )
  254. def _add_function(self, name, callback, prefix=False):
  255. globals()[name] = callback
  256. arg = 'None' if prefix else ''
  257. vim.command(
  258. 'function! {0}()\n'
  259. 'python ropevim.{1}({2})\n'
  260. 'endfunction\n'.format(_vim_name(name), name, arg)
  261. )
  262. def _completion_data(self, proposal):
  263. return proposal
  264. _docstring_re = re.compile('^[\s\t\n]*([^\n]*)')
  265. def _extended_completion(self, proposal):
  266. # we are using extended complete and return dicts instead of strings.
  267. # `ci` means "completion item". see `:help complete-items`
  268. word, _, menu = map(lambda x: x.strip(), proposal.name.partition(':'))
  269. ci = dict(
  270. word=word,
  271. info='',
  272. kind=''.join(
  273. s if s not in 'aeyuo' else '' for s in proposal.type)[:3],
  274. menu=menu or '')
  275. if proposal.scope == 'parameter_keyword':
  276. default = proposal.get_default()
  277. ci["menu"] += '*' if default is None else '= {0}'.format(default)
  278. if self.preview and not ci['menu']:
  279. doc = proposal.get_doc()
  280. ci['info'] = self._docstring_re.match(doc).group(1) if doc else ''
  281. return self._conv(ci)
  282. def _conv(self, obj):
  283. if isinstance(obj, dict):
  284. return u'{' + u','.join([
  285. u"{0}:{1}".format(self._conv(key), self._conv(value))
  286. for key, value in obj.iteritems()]) + u'}'
  287. return u'"{0}"'.format(str(obj).replace(u'"', u'\\"'))
  288. def _vim_name(name):
  289. tokens = name.split('_')
  290. newtokens = ['Rope'] + [token.title() for token in tokens]
  291. return ''.join(newtokens)
  292. class VimProgress(object):
  293. def __init__(self, name):
  294. self.name = name
  295. self.last = 0
  296. status('{0} ... '.format(self.name))
  297. def update(self, percent):
  298. try:
  299. vim.eval('getchar(0)')
  300. except vim.error:
  301. raise KeyboardInterrupt(
  302. 'Task {0} was interrupted!'.format(self.name)
  303. )
  304. if percent > self.last + 4:
  305. status('{0} ... {1}%'.format(self.name, percent))
  306. self.last = percent
  307. def done(self):
  308. status('{0} ... done'.format(self.name))
  309. def echo(message):
  310. if _rope_quiet:
  311. return
  312. if isinstance(message, unicode):
  313. message = message.encode(vim.eval('&encoding'))
  314. print message
  315. def status(message):
  316. if _rope_quiet:
  317. return
  318. if isinstance(message, unicode):
  319. message = message.encode(vim.eval('&encoding'))
  320. vim.command('redraw | echon "{0}"'.format(message))
  321. def call(command):
  322. return vim.eval(command)
  323. class _ValueCompleter(object):
  324. def __init__(self):
  325. self.values = []
  326. vim.command('python import vim')
  327. vim.command('function! RopeValueCompleter(A, L, P)\n'
  328. 'python args = [vim.eval("a:" + p) for p in "ALP"]\n'
  329. 'python ropevim._completer(*args)\n'
  330. 'return s:completions\n'
  331. 'endfunction\n')
  332. def __call__(self, arg_lead, cmd_line, cursor_pos):
  333. # don't know if self.values can be empty but better safe then sorry
  334. if self.values:
  335. if not isinstance(self.values[0], basestring):
  336. result = [proposal.name for proposal in self.values
  337. if proposal.name.startswith(arg_lead)]
  338. else:
  339. result = [proposal for proposal in self.values
  340. if proposal.startswith(arg_lead)]
  341. vim.command('let s:completions = {0}'.format(result))
  342. class RopeMode(interface.RopeMode):
  343. @decorators.global_command('o')
  344. def open_project(self, root=None, quiet=False):
  345. global _rope_quiet
  346. _rope_quiet = quiet
  347. super(RopeMode, self).open_project(root=root)
  348. rope_project_dir = os.path.join(self.project.address, '.ropeproject')
  349. vimfiles = glob.glob(os.path.join(rope_project_dir, '*.vim'))
  350. if not vimfiles:
  351. return
  352. txt = 'Sourcing vim files under \'.ropeproject/\''
  353. progress = self.env.create_progress(txt)
  354. for idx, vimfile in enumerate(sorted(vimfiles)):
  355. progress.name = txt + ' ({0})'.format(os.path.basename(vimfile))
  356. vim.command(':silent source {0}'.format(vimfile))
  357. progress.update(idx * 100 / len(vimfiles))
  358. progress.name = txt
  359. progress.done()
  360. echo('Project opened!')
  361. decorators.logger.message = echo
  362. decorators.logger.only_short = True
  363. _completer = _ValueCompleter()
  364. _env = VimUtils()
  365. _interface = RopeMode(env=_env)