PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/latex_cite_completions.py

https://github.com/mstaal/LaTeXTools
Python | 420 lines | 323 code | 38 blank | 59 comment | 48 complexity | d5a5b23a3127bf3add67a226f60b1a36 MD5 | raw file
  1. import sublime, sublime_plugin
  2. import os, os.path
  3. import re
  4. import getTeXRoot
  5. def match(rex, str):
  6. m = rex.match(str)
  7. if m:
  8. return m.group(0)
  9. else:
  10. return None
  11. # recursively search all linked tex files to find all
  12. # included bibliography tags in the document and extract
  13. # the absolute filepaths of the bib files
  14. def find_bib_files(rootdir, src, bibfiles):
  15. if src[-4:].lower() != ".tex":
  16. src = src + ".tex"
  17. file_path = os.path.normpath(os.path.join(rootdir,src))
  18. print "Searching file: " + repr(file_path)
  19. # See latex_ref_completion.py for why the following is wrong:
  20. #dir_name = os.path.dirname(file_path)
  21. # read src file and extract all bibliography tags
  22. try:
  23. src_file = open(file_path, "r")
  24. except IOError:
  25. sublime.status_message("LaTeXTools WARNING: cannot open included file " + file_path)
  26. print "WARNING! I can't find it! Check your \\include's and \\input's."
  27. return
  28. src_content = re.sub("%.*","",src_file.read())
  29. bibtags = re.findall(r'\\bibliography\{[^\}]+\}', src_content)
  30. # extract absolute filepath for each bib file
  31. for tag in bibtags:
  32. bfiles = re.search(r'\{([^\}]+)', tag).group(1).split(',')
  33. for bf in bfiles:
  34. if bf[-4:].lower() != '.bib':
  35. bf = bf + '.bib'
  36. # We join with rootdir - everything is off the dir of the master file
  37. bf = os.path.normpath(os.path.join(rootdir,bf))
  38. bibfiles.append(bf)
  39. # search through input tex files recursively
  40. for f in re.findall(r'\\(?:input|include)\{[^\}]+\}',src_content):
  41. input_f = re.search(r'\{([^\}]+)', f).group(1)
  42. find_bib_files(rootdir, input_f, bibfiles)
  43. # Based on html_completions.py
  44. # see also latex_ref_completions.py
  45. #
  46. # It expands citations; activated by
  47. # cite<tab>
  48. # citep<tab> and friends
  49. #
  50. # Furthermore, you can "pre-filter" the completions: e.g. use
  51. #
  52. # cite_sec
  53. #
  54. # to select all citation keywords starting with "sec".
  55. #
  56. # There is only one problem: if you have a keyword "sec:intro", for instance,
  57. # doing "cite_intro:" will find it correctly, but when you insert it, this will be done
  58. # right after the ":", so the "cite_intro:" won't go away. The problem is that ":" is a
  59. # word boundary. Then again, TextMate has similar limitations :-)
  60. #
  61. # There is also another problem: * is also a word boundary :-( So, use e.g. citeX if
  62. # what you want is \cite*{...}; the plugin handles the substitution
  63. class LatexCiteCompletions(sublime_plugin.EventListener):
  64. def on_query_completions(self, view, prefix, locations):
  65. # Only trigger within LaTeX
  66. if not view.match_selector(locations[0],
  67. "text.tex.latex"):
  68. return []
  69. # Get the contents of line 0, from the beginning of the line to
  70. # the current point
  71. l = locations[0]
  72. line = view.substr(sublime.Region(view.line(l).a, l))
  73. # Reverse, to simulate having the regex
  74. # match backwards (cool trick jps btw!)
  75. line = line[::-1]
  76. #print line
  77. # Check the first location looks like a ref, but backward
  78. # NOTE: use lazy match for the fancy cite part!!!
  79. # NOTE2: restrict what to match for fancy cite
  80. rex = re.compile("([^_]*_)?([a-zX]*?)etic")
  81. expr = match(rex, line)
  82. #print expr
  83. # See first if we have a cite_ trigger
  84. if expr:
  85. # Return the completions
  86. prefix, fancy_cite = rex.match(expr).groups()
  87. preformatted = False
  88. post_brace = "}"
  89. if prefix:
  90. prefix = prefix[::-1] # reverse
  91. if prefix[0]=='_':
  92. prefix = prefix[1:] # chop off if there was a _
  93. else:
  94. prefix = "" # because this could be a None, not ""
  95. if fancy_cite:
  96. fancy_cite = fancy_cite[::-1]
  97. # fancy_cite = fancy_cite[1:] # no need to chop off?
  98. if fancy_cite[-1] == "X":
  99. fancy_cite = fancy_cite[:-1] + "*"
  100. else:
  101. fancy_cite = "" # just in case it's a None
  102. print prefix, fancy_cite
  103. # Otherwise, see if we have a preformatted \cite{}
  104. else:
  105. rex = re.compile(r"([^{}]*)\{?([a-zX*]*?)etic\\")
  106. expr = match(rex, line)
  107. if not expr:
  108. return []
  109. preformatted = True
  110. post_brace = ""
  111. prefix, fancy_cite = rex.match(expr).groups()
  112. if prefix:
  113. prefix = prefix[::-1]
  114. else:
  115. prefix = ""
  116. if fancy_cite:
  117. fancy_cite = fancy_cite[::-1]
  118. if fancy_cite[-1] == "X":
  119. fancy_cite = fancy_cite[:-1] + "*"
  120. else:
  121. fancy_cite = ""
  122. print prefix, fancy_cite
  123. # Reverse back expr
  124. expr = expr[::-1]
  125. if not preformatted:
  126. # Replace cite expression with "C" to save space in drop-down menu
  127. expr_region = sublime.Region(l-len(expr),l)
  128. #print expr, view.substr(expr_region)
  129. ed = view.begin_edit()
  130. expr = "\cite" + fancy_cite + "{" + prefix
  131. view.replace(ed, expr_region, expr)
  132. view.end_edit(ed)
  133. completions = ["TEST"]
  134. #### GET COMPLETIONS HERE #####
  135. root = getTeXRoot.get_tex_root(view)
  136. print "TEX root: " + repr(root)
  137. bib_files = []
  138. find_bib_files(os.path.dirname(root),root,bib_files)
  139. # remove duplicate bib files
  140. bib_files = list(set(bib_files))
  141. print "Bib files found: ",
  142. print repr(bib_files)
  143. if not bib_files:
  144. print "Error!"
  145. return []
  146. bib_files = ([x.strip() for x in bib_files])
  147. print "Files:"
  148. print repr(bib_files)
  149. completions = []
  150. kp = re.compile(r'@[^\{]+\{(.+),')
  151. # new and improved regex
  152. # we must have "title" then "=", possibly with spaces
  153. # then either {, maybe repeated twice, or "
  154. # then spaces and finally the title
  155. # We capture till the end of the line as maybe entry is broken over several lines
  156. # and in the end we MAY but need not have }'s and "s
  157. tp = re.compile(r'\btitle\s*=\s*(?:\{+|")\s*(.+)', re.IGNORECASE) # note no comma!
  158. kp2 = re.compile(r'([^\t]+)\t*')
  159. for bibfname in bib_files:
  160. # # THIS IS NO LONGER NEEDED as find_bib_files() takes care of it
  161. # if bibfname[-4:] != ".bib":
  162. # bibfname = bibfname + ".bib"
  163. # texfiledir = os.path.dirname(view.file_name())
  164. # # fix from Tobias Schmidt to allow for absolute paths
  165. # bibfname = os.path.normpath(os.path.join(texfiledir, bibfname))
  166. # print repr(bibfname)
  167. try:
  168. bibf = open(bibfname)
  169. except IOError:
  170. print "Cannot open bibliography file %s !" % (bibfname,)
  171. sublime.status_message("Cannot open bibliography file %s !" % (bibfname,))
  172. continue
  173. else:
  174. bib = bibf.readlines()
  175. bibf.close()
  176. print "%s has %s lines" % (repr(bibfname), len(bib))
  177. # note Unicode trickery
  178. keywords = [kp.search(line).group(1).decode('ascii','ignore') for line in bib if line[0] == '@']
  179. titles = [tp.search(line).group(1).decode('ascii','ignore') for line in bib if tp.search(line)]
  180. if len(keywords) != len(titles):
  181. print "Bibliography " + repr(bibfname) + " is broken!"
  182. # Filter out }'s and ,'s at the end. Ugly!
  183. nobraces = re.compile(r'\s*,*\}*(.+)')
  184. titles = [nobraces.search(t[::-1]).group(1)[::-1] for t in titles]
  185. completions += zip(keywords, titles)
  186. #### END COMPLETIONS HERE ####
  187. print "Found %d completions" % (len(completions),)
  188. if prefix:
  189. completions = [comp for comp in completions if prefix.lower() in "%s %s" % (comp[0].lower(),comp[1].lower())]
  190. # popup is 40chars wide...
  191. t_end = 80 - len(expr)
  192. r = [(prefix + " "+title[:t_end], keyword + post_brace)
  193. for (keyword, title) in completions]
  194. print "%d bib entries matching %s" % (len(r), prefix)
  195. # def on_done(i):
  196. # print "latex_cite_completion called with index %d" % (i,)
  197. # print "selected" + r[i][1]
  198. # print view.window()
  199. return r
  200. class LatexCiteCommand(sublime_plugin.TextCommand):
  201. # Remember that this gets passed an edit object
  202. def run(self, edit):
  203. # get view and location of first selection, which we expect to be just the cursor position
  204. view = self.view
  205. point = view.sel()[0].b
  206. print point
  207. # Only trigger within LaTeX
  208. # Note using score_selector rather than match_selector
  209. if not view.score_selector(point,
  210. "text.tex.latex"):
  211. return
  212. # Get the contents of the current line, from the beginning of the line to
  213. # the current point
  214. line = view.substr(sublime.Region(view.line(point).a, point))
  215. print line
  216. # Reverse, to simulate having the regex
  217. # match backwards (cool trick jps btw!)
  218. line = line[::-1]
  219. #print line
  220. # Check the first location looks like a cite_, but backward
  221. # NOTE: use lazy match for the fancy cite part!!!
  222. # NOTE2: restrict what to match for fancy cite
  223. rex = re.compile("([^_]*_)?([a-zX]*?)etic")
  224. expr = match(rex, line)
  225. # See first if we have a cite_ trigger
  226. if expr:
  227. # Return the completions
  228. prefix, fancy_cite = rex.match(expr).groups()
  229. preformatted = False
  230. post_brace = "}"
  231. if prefix:
  232. prefix = prefix[::-1] # reverse
  233. prefix = prefix[1:] # chop off
  234. else:
  235. prefix = "" # just in case it's None, though here
  236. # it shouldn't happen!
  237. if fancy_cite:
  238. fancy_cite = fancy_cite[::-1]
  239. # fancy_cite = fancy_cite[1:] # no need to chop off?
  240. if fancy_cite[-1] == "X":
  241. fancy_cite = fancy_cite[:-1] + "*"
  242. else:
  243. fancy_cite = "" # again just in case
  244. print prefix, fancy_cite
  245. # Otherwise, see if we have a preformatted \cite{}
  246. else:
  247. rex = re.compile(r"([^{}]*)\{?([a-zX*]*?)etic\\")
  248. expr = match(rex, line)
  249. if not expr:
  250. return []
  251. preformatted = True
  252. post_brace = ""
  253. prefix, fancy_cite = rex.match(expr).groups()
  254. if prefix:
  255. prefix = prefix[::-1]
  256. else:
  257. prefix = ""
  258. if fancy_cite:
  259. fancy_cite = fancy_cite[::-1]
  260. if fancy_cite[-1] == "X":
  261. fancy_cite = fancy_cite[:-1] + "*"
  262. else:
  263. fancy_cite = ""
  264. print prefix, fancy_cite
  265. # Reverse back expr
  266. expr = expr[::-1]
  267. #### GET COMPLETIONS HERE #####
  268. root = getTeXRoot.get_tex_root(view)
  269. print "TEX root: " + repr(root)
  270. bib_files = []
  271. find_bib_files(os.path.dirname(root),root,bib_files)
  272. # remove duplicate bib files
  273. bib_files = list(set(bib_files))
  274. print "Bib files found: ",
  275. print repr(bib_files)
  276. if not bib_files:
  277. sublime.error_message("No bib files found!") # here we can!
  278. return []
  279. bib_files = ([x.strip() for x in bib_files])
  280. print "Files:"
  281. print repr(bib_files)
  282. completions = []
  283. kp = re.compile(r'@[^\{]+\{(.+),')
  284. # new and improved regex
  285. # we must have "title" then "=", possibly with spaces
  286. # then either {, maybe repeated twice, or "
  287. # then spaces and finally the title
  288. # We capture till the end of the line as maybe entry is broken over several lines
  289. # and in the end we MAY but need not have }'s and "s
  290. tp = re.compile(r'\btitle\s*=\s*(?:\{+|")\s*(.+)', re.IGNORECASE) # note no comma!
  291. # Tentatively do the same for author
  292. ap = re.compile(r'\bauthor\s*=\s*(?:\{+|")\s*(.+)', re.IGNORECASE)
  293. kp2 = re.compile(r'([^\t]+)\t*')
  294. for bibfname in bib_files:
  295. # # NO LONGER NEEDED: see above
  296. # if bibfname[-4:] != ".bib":
  297. # bibfname = bibfname + ".bib"
  298. # texfiledir = os.path.dirname(view.file_name())
  299. # # fix from Tobias Schmidt to allow for absolute paths
  300. # bibfname = os.path.normpath(os.path.join(texfiledir, bibfname))
  301. # print repr(bibfname)
  302. try:
  303. bibf = open(bibfname)
  304. except IOError:
  305. print "Cannot open bibliography file %s !" % (bibfname,)
  306. sublime.status_message("Cannot open bibliography file %s !" % (bibfname,))
  307. continue
  308. else:
  309. bib = bibf.readlines()
  310. bibf.close()
  311. print "%s has %s lines" % (repr(bibfname), len(bib))
  312. # note Unicode trickery
  313. keywords = [kp.search(line).group(1).decode('ascii','ignore') for line in bib if line[0] == '@']
  314. titles = [tp.search(line).group(1).decode('ascii','ignore') for line in bib if tp.search(line)]
  315. authors = [ap.search(line).group(1).decode('ascii','ignore') for line in bib if ap.search(line)]
  316. # print zip(keywords,titles,authors)
  317. if len(keywords) != len(titles):
  318. print "Bibliography " + repr(bibfname) + " is broken!"
  319. return
  320. # if len(keywords) != len(authors):
  321. # print "Bibliography " + bibfname + " is broken (authors)!"
  322. # return
  323. print "Found %d total bib entries" % (len(keywords),)
  324. # Filter out }'s and ,'s at the end. Ugly!
  325. nobraces = re.compile(r'\s*,*\}*(.+)')
  326. titles = [nobraces.search(t[::-1]).group(1)[::-1] for t in titles]
  327. authors = [nobraces.search(a[::-1]).group(1)[::-1] for a in authors]
  328. completions += zip(keywords, titles, authors)
  329. #### END COMPLETIONS HERE ####
  330. # filter against keyword, title, or author
  331. if prefix:
  332. completions = [comp for comp in completions if prefix.lower() in "%s %s %s" \
  333. % (comp[0].lower(),comp[1].lower(), comp[2].lower())]
  334. # Note we now generate citation on the fly. Less copying of vectors! Win!
  335. def on_done(i):
  336. print "latex_cite_completion called with index %d" % (i,)
  337. # Allow user to cancel
  338. if i<0:
  339. return
  340. last_brace = "}" if not preformatted else ""
  341. cite = "\\cite" + fancy_cite + "{" + completions[i][0] + last_brace
  342. print "selected %s:%s by %s" % completions[i]
  343. # Replace cite expression with citation
  344. expr_region = sublime.Region(point-len(expr),point)
  345. ed = view.begin_edit()
  346. view.replace(ed, expr_region, cite)
  347. view.end_edit(ed)
  348. view.window().show_quick_panel([[title + " (" + keyword+ ")", author] \
  349. for (keyword,title, author) in completions], on_done)