PageRenderTime 74ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/latex_cite_completions.py

https://github.com/dozed/LaTeXTools
Python | 341 lines | 185 code | 50 blank | 106 comment | 51 complexity | d99b4a846c06a349b7d010e92b46b90d MD5 | raw file
  1. import sublime, sublime_plugin
  2. import os, os.path
  3. import re
  4. def match(rex, str):
  5. m = rex.match(str)
  6. if m:
  7. return m.group(0)
  8. else:
  9. return None
  10. # Based on html_completions.py
  11. # see also latex_ref_completions.py
  12. #
  13. # It expands citations; activated by
  14. # cite<tab>
  15. # citep<tab> and friends
  16. #
  17. # Furthermore, you can "pre-filter" the completions: e.g. use
  18. #
  19. # cite_sec
  20. #
  21. # to select all citation keywords starting with "sec".
  22. #
  23. # There is only one problem: if you have a keyword "sec:intro", for instance,
  24. # doing "cite_intro:" will find it correctly, but when you insert it, this will be done
  25. # right after the ":", so the "cite_intro:" won't go away. The problem is that ":" is a
  26. # word boundary. Then again, TextMate has similar limitations :-)
  27. #
  28. # There is also another problem: * is also a word boundary :-( So, use e.g. citeX if
  29. # what you want is \cite*{...}; the plugin handles the substitution
  30. class LatexCiteCompletions(sublime_plugin.EventListener):
  31. def on_query_completions(self, view, prefix, locations):
  32. # Only trigger within LaTeX
  33. if not view.match_selector(locations[0],
  34. "text.tex.latex"):
  35. return []
  36. # Get the contents of line 0, from the beginning of the line to
  37. # the current point
  38. l = locations[0]
  39. line = view.substr(sublime.Region(view.line(l).a, l))
  40. # Reverse, to simulate having the regex
  41. # match backwards (cool trick jps btw!)
  42. line = line[::-1]
  43. #print line
  44. # Check the first location looks like a ref, but backward
  45. # NOTE: use lazy match for the fancy cite part!!!
  46. # NOTE2: restrict what to match for fancy cite
  47. rex = re.compile("([^_]*_)?([a-zX]*?)etic")
  48. expr = match(rex, line)
  49. print expr
  50. if not expr:
  51. return []
  52. # Return the completions
  53. prefix, fancy_cite = rex.match(expr).groups()
  54. if prefix:
  55. prefix = prefix[::-1] # reverse
  56. prefix = prefix[1:] # chop off #
  57. if fancy_cite:
  58. fancy_cite = fancy_cite[::-1]
  59. # fancy_cite = fancy_cite[1:] # no need to chop off?
  60. if fancy_cite[-1] == "X":
  61. fancy_cite = fancy_cite[:-1] + "*"
  62. print prefix, fancy_cite
  63. # Reverse back expr
  64. expr = expr[::-1]
  65. # Replace cite expression with "C" to save space in drop-down menu
  66. expr_region = sublime.Region(l-len(expr),l)
  67. #print expr, view.substr(expr_region)
  68. ed = view.begin_edit()
  69. view.replace(ed, expr_region, "C")
  70. view.end_edit(ed)
  71. expr = "C"
  72. completions = ["TEST"]
  73. #### GET COMPLETIONS HERE #####
  74. # Allow for multiple bib files; remove whitespace in names
  75. # Note improved regex: matching fails with , or }, so no need to be
  76. # explicit and add it after []+
  77. bib_regions = view.find_all(r'\\bibliography\{[^\}]+')
  78. # The \bibliography command may be commented out: find this out
  79. # We check every match until we find the first command that is not
  80. # commented out
  81. bib_found = False
  82. for bib_region in bib_regions:
  83. bib_line = view.line(bib_region)
  84. bib_command = view.substr(bib_line).strip()
  85. if bib_command[0] == '\\':
  86. print bib_command
  87. bib_found = True
  88. break
  89. if not bib_found:
  90. sublime.error_message("Cannot find \\bibliography{} command!")
  91. return []
  92. bib_files = re.search(r'\{([^\}]+)', bib_command).group(1).split(',')
  93. if not bib_files:
  94. print "Error!"
  95. return []
  96. bib_files = ([x.strip() for x in bib_files])
  97. print "Files:"
  98. print bib_files
  99. # bibp = re.compile(r'\{(.+)') # we dropped last , or } so don't look for it
  100. # bibmatch = bibp.search(bibcmd)
  101. # if not bibmatch:
  102. # print "Cannot parse bibliography command: " + bibcmd
  103. # return
  104. # bibfname = bibmatch.group(1)
  105. # print bibfname
  106. completions = []
  107. kp = re.compile(r'@[^\{]+\{(.+),')
  108. # new and improved regex
  109. # we must have "title" then "=", possibly with spaces
  110. # then either {, maybe repeated twice, or "
  111. # then spaces and finally the title
  112. # We capture till the end of the line as maybe entry is broken over several lines
  113. # and in the end we MAY but need not have }'s and "s
  114. tp = re.compile(r'\btitle\s*=\s*(?:\{+|")\s*(.+)', re.IGNORECASE) # note no comma!
  115. kp2 = re.compile(r'([^\t]+)\t*')
  116. for bibfname in bib_files:
  117. if bibfname[-4:] != ".bib":
  118. bibfname = bibfname + ".bib"
  119. texfiledir = os.path.dirname(view.file_name())
  120. # fix from Tobias Schmidt to allow for absolute paths
  121. bibfname = os.path.normpath(os.path.join(texfiledir, bibfname))
  122. print bibfname
  123. try:
  124. bibf = open(bibfname)
  125. except IOError:
  126. sublime.error_message("Cannot open bibliography file %s !" % (bibfname,))
  127. return []
  128. else:
  129. bib = bibf.readlines()
  130. bibf.close()
  131. print "%s has %s lines" % (bibfname, len(bib))
  132. # note Unicode trickery
  133. keywords = [kp.search(line).group(1).decode('ascii','ignore') for line in bib if line[0] == '@']
  134. titles = [tp.search(line).group(1).decode('ascii','ignore') for line in bib if tp.search(line)]
  135. if len(keywords) != len(titles):
  136. print "Bibliography " + bibfname + " is broken!"
  137. # Filter out }'s and ,'s at the end. Ugly!
  138. nobraces = re.compile(r'\s*,*\}*(.+)')
  139. titles = [nobraces.search(t[::-1]).group(1)[::-1] for t in titles]
  140. completions += zip(keywords, titles)
  141. #### END COMPLETIONS HERE ####
  142. print completions
  143. if prefix:
  144. completions = [comp for comp in completions if prefix.lower() in "%s %s" % (comp[0].lower(),comp[1].lower())]
  145. # popup is 40chars wide...
  146. t_end = 40 - len(expr)
  147. r = [(expr + " "+title[:t_end], "\\cite" + fancy_cite + "{" + keyword + "}")
  148. for (keyword, title) in completions]
  149. print r
  150. def on_done(i):
  151. print "latex_cite_completion called with index %d" % (i,)
  152. print "selected" + r[i][1]
  153. print view.window()
  154. return r
  155. class LatexCiteCommand(sublime_plugin.TextCommand):
  156. # Remember that this gets passed an edit object
  157. def run(self, edit):
  158. # get view and location of first selection, which we expect to be just the cursor position
  159. view = self.view
  160. point = view.sel()[0].b
  161. print point
  162. # Only trigger within LaTeX
  163. # Note using score_selector rather than match_selector
  164. if not view.score_selector(point,
  165. "text.tex.latex"):
  166. return
  167. # Get the contents of the current line, from the beginning of the line to
  168. # the current point
  169. line = view.substr(sublime.Region(view.line(point).a, point))
  170. print line
  171. # Reverse, to simulate having the regex
  172. # match backwards (cool trick jps btw!)
  173. line = line[::-1]
  174. #print line
  175. # Check the first location looks like a ref, but backward
  176. # NOTE: use lazy match for the fancy cite part!!!
  177. # NOTE2: restrict what to match for fancy cite
  178. rex = re.compile("([^_]*_)?([a-zX]*?)etic")
  179. expr = match(rex, line)
  180. print expr
  181. if not expr:
  182. return []
  183. # Return the completions
  184. prefix, fancy_cite = rex.match(expr).groups()
  185. if prefix:
  186. prefix = prefix[::-1] # reverse
  187. prefix = prefix[1:] # chop off #
  188. if fancy_cite:
  189. fancy_cite = fancy_cite[::-1]
  190. # fancy_cite = fancy_cite[1:] # no need to chop off?
  191. if fancy_cite[-1] == "X":
  192. fancy_cite = fancy_cite[:-1] + "*"
  193. print prefix, fancy_cite
  194. # Reverse back expr
  195. expr = expr[::-1]
  196. # We no longer need this stuff:
  197. # # Replace cite expression with "C" to save space in drop-down menu
  198. # expr_region = sublime.Region(point-len(expr),point)
  199. # #print expr, view.substr(expr_region)
  200. # ed = view.begin_edit()
  201. # view.replace(ed, expr_region, "C")
  202. # view.end_edit(ed)
  203. # expr = "C"
  204. completions = ["TEST"]
  205. #### GET COMPLETIONS HERE #####
  206. # Allow for multiple bib files; remove whitespace in names
  207. # Note improved regex: matching fails with , or }, so no need to be
  208. # explicit and add it after []+
  209. bib_regions = view.find_all(r'\\bibliography\{[^\}]+')
  210. # The \bibliography command may be commented out: find this out
  211. # We check every match until we find the first command that is not
  212. # commented out
  213. bib_found = False
  214. for bib_region in bib_regions:
  215. bib_line = view.line(bib_region)
  216. bib_command = view.substr(bib_line).strip()
  217. if bib_command[0] == '\\':
  218. print bib_command
  219. bib_found = True
  220. break
  221. if not bib_found:
  222. sublime.error_message("Cannot find \\bibliography{} command!")
  223. return []
  224. bib_files = re.search(r'\{([^\}]+)', bib_command).group(1).split(',')
  225. if not bib_files:
  226. print "Error!"
  227. return []
  228. bib_files = ([x.strip() for x in bib_files])
  229. print "Files:"
  230. print bib_files
  231. # bibp = re.compile(r'\{(.+)') # we dropped last , or } so don't look for it
  232. # bibmatch = bibp.search(bibcmd)
  233. # if not bibmatch:
  234. # print "Cannot parse bibliography command: " + bibcmd
  235. # return
  236. # bibfname = bibmatch.group(1)
  237. # print bibfname
  238. completions = []
  239. kp = re.compile(r'@[^\{]+\{(.+),')
  240. # new and improved regex
  241. # we must have "title" then "=", possibly with spaces
  242. # then either {, maybe repeated twice, or "
  243. # then spaces and finally the title
  244. # We capture till the end of the line as maybe entry is broken over several lines
  245. # and in the end we MAY but need not have }'s and "s
  246. tp = re.compile(r'\btitle\s*=\s*(?:\{+|")\s*(.+)', re.IGNORECASE) # note no comma!
  247. kp2 = re.compile(r'([^\t]+)\t*')
  248. for bibfname in bib_files:
  249. if bibfname[-4:] != ".bib":
  250. bibfname = bibfname + ".bib"
  251. texfiledir = os.path.dirname(view.file_name())
  252. # fix from Tobias Schmidt to allow for absolute paths
  253. bibfname = os.path.normpath(os.path.join(texfiledir, bibfname))
  254. print bibfname
  255. try:
  256. bibf = open(bibfname)
  257. except IOError:
  258. sublime.error_message("Cannot open bibliography file %s !" % (bibfname,))
  259. return []
  260. else:
  261. bib = bibf.readlines()
  262. bibf.close()
  263. print "%s has %s lines" % (bibfname, len(bib))
  264. # note Unicode trickery
  265. keywords = [kp.search(line).group(1).decode('ascii','ignore') for line in bib if line[0] == '@']
  266. titles = [tp.search(line).group(1).decode('ascii','ignore') for line in bib if tp.search(line)]
  267. if len(keywords) != len(titles):
  268. print "Bibliography " + bibfname + " is broken!"
  269. # Filter out }'s and ,'s at the end. Ugly!
  270. nobraces = re.compile(r'\s*,*\}*(.+)')
  271. titles = [nobraces.search(t[::-1]).group(1)[::-1] for t in titles]
  272. completions += zip(keywords, titles)
  273. #### END COMPLETIONS HERE ####
  274. print completions
  275. if prefix:
  276. completions = [comp for comp in completions if prefix.lower() in "%s %s" % (comp[0].lower(),comp[1].lower())]
  277. # Note we now generate citation on the fly. Less copying of vectors! Win!
  278. def on_done(i):
  279. print "latex_cite_completion called with index %d" % (i,)
  280. cite = "\\cite" + fancy_cite + "{" + completions[i][0] + "}"
  281. print "selected %s:%s" % completions[i]
  282. # Replace cite expression with citation
  283. expr_region = sublime.Region(point-len(expr),point)
  284. ed = view.begin_edit()
  285. view.replace(ed, expr_region, cite)
  286. view.end_edit(ed)
  287. view.window().show_quick_panel([[title, keyword] for (keyword,title) in completions], on_done)