PageRenderTime 76ms CodeModel.GetById 36ms app.highlight 31ms RepoModel.GetById 5ms app.codeStats 0ms

/latex_cite_completions.py

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