LaTeX plugin update: threading for fun and profit

I finally bit the bullet and rewrote the "texify" command so as to provide better feedback during compilation. The latest version of the plugin behaves as follows: as soon as you invoke the "texify" command (bound to ctrl+alt+t by default), a quick panel is displayed, initially showing the exact texify command issued. When compilation finishes, errors and warnings are displayed, followed by the contents of the log file. Previously, the quick panel was only shown upon completion of the texify command, so one was left wondering whether compilation had actually started upon hitting ctrl+alt+t. The new behavior is a lot more informative and user-friendly. As was the case previously, you can click on any error message containing a line number to jump to the corresponding line in the source file. The new code also has other niceties related to error detection; I will discuss them in a later post.

Figuring out how to display some text in the quick panel, leave the latter open while an external command runs, and then add more text to the quick panel took me on an interesting, if lengthy trip into Python threading land. Suppose you try the following code:

	class PanelTestCommand(sublimeplugin.WindowCommand):
	   def run(self, window, args):
	      args = ["wait 5 seconds..."]
	      window.showQuickPanel("panel", "", args)
	      print "Wait"
	      import time
	      time.sleep(5)
	      args.append("Done!")
	      print "Done"
	      window.showQuickPanel("panel", "", args)


Update: WordPress seems to kill spacing at the beginning of a line… please pretend the above listing and the one below are correctly indented :-)

Update 2: Actually, there is a nice, easy way to post source code: the sourcecode shortcode! See here for details. The only issue seems to be that quotes are transformed into HTML entities, so when you get back your post from WordPress, it looks a bit messy. I am also worried about round-tripping. But, I’ll look into this later.

For non-SublimeText people, you define a command by subclassing sublimeplugin.WindowCommand and redefining the run method. The showQuickPanel method takes three arguments, the last one being a list of strings to be displayed. The print statement displays text in the console, which is useful for debugging purposes. The rest of the code should be self-explanatory; the 5-second wait is a stand-in for a blocking system call (e.g. to texify). Unfortunately, when you invoke this command, nothing happens for 5 seconds, after which (a) all output is shown in the console, and (b) the quick panel is displayed.

This reply by the Sublime Text author, Jon Skinner, to a post of mine in the tech support forum pointed me in the right direction. Sublime Text works asynchronously, and apparently buffers output as needed. So, to get the intended behavior, you need to do things in a somewhat roundabout way:

  • Spawn a thread that invokes the required command (or, in the case of the above example, waits 5 seconds)
  • Invoke showQuickPanel from the thread, via the sublime.setTimeout method.
  • This reminds me of how you deal with the event dispatch thread in Java Swing; I guess something similar is going on. The bottom line is, I had to learn the bare minimum about threading in Python, and then take care of certain annoyances. This is the relevant portion of the current implementation of the “texify” command:

    	class TexifyCommand(sublimeplugin.WindowCommand):
    		def run(self, window, args):
    			
    			[omitted: set things up]
    		
    			view = window.activeView()
    			texFile, texExt = os.path.splitext(view.fileName())
    
    			[omitted: create texify command line]
    
    			pContents = ["Texify executing command:", cmd]
    			window.showQuickPanel("texify", "", pContents)
    
    			class CmdThread ( threading.Thread ):
    
    				def __init__ (self, cmd, pContent, texFile, window):
    					self.cmd = cmd
    					self.pContent = pContent
    					self.texFile = texFile
    					self.qp = window.showQuickPanel
    					threading.Thread.__init__ ( self )
    
    				def run ( self ):
    
    					[omitted: set things up]
    
    					p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
    					stdoutdata, stderrdata = p.communicate()
    
    					pContent = [omitted: define content to be displayed]
    					
    					sublime.setTimeout(functools.partial(self.qp,"texify","gotoTeXError", pContent), 0)
    					
    			CmdThread(cmd, pContents, texFile, window).start()
    

    Let’s take a close look. When the command is invoked, we do some housecleaning, and then create the texify command line from the name of the file in the current buffer (details omitted); this is stored in the variable named cmd. We then display this information in the quick panel.

    Next, we use the Python threading module. We must subclass threading.Thread; all the action takes place in the run method, which sets things up, invokes the texify command, and then uses sublime.setTimeout to display the results via showQuickPanel.

    While this description is correct, it overlooks an important fact: the CmdThread class does not have access to variables such as cmd or window, and hence to the window.showQuickPanel method. To address this, we use the __init__ method to save copies of the objects we need—including the window.showQuickPanel method, which is stored in self.qp.

    But there is one more wrinkle. The setTimeout method accepts two parameters: the method to be called, and a delay value. In our case, we want to call the self.qp = window.showQuickPanel method… but how can we pass parameters (such as the text to be displayed!) to it? The functools
    Python module comes to the rescue: using functools.partial, we create a new method that works like window.showQuickPanel, but with the parameters we need—in particular, the content to be displayed is in the variable code pContent.

    Voila’! We then finally create our thread object and start it. So, to summarize, the key “tricks” are:

  • Do all blocking work in a thread, and call showQuickPanel from that thread via setTimeout
  • To pass parameters to the thread, including the showQuickPanel method, use the thread object’s __init__ method.
  • To set the parameters of the showQuickPanel call, use functools.partial.
  • Comments and advice is most welcome—I’m still learning this stuff!

    About these ads

    3 responses to “LaTeX plugin update: threading for fun and profit

    1. Hi, may I know how do I go about using OTF fonts with Latex? I tried setting the environment variable PDFLATEX to xelatex, and it works in the command line, but not when using the texify (ctrl alt t combination) within sublime text.

      But I lose microtype. Also, the texify when manually entered in the command line always downloads fontspec. Why doesnt it stay installed?

    2. Also, using your plug in, I cant get Ctrl Alt C to work at all.

      Traceback (most recent call last):
      File “.\sublimeplugin.py”, line 119, in execTextCommand
      File “.\texCite.py”, line 42, in run
      AttributeError: ‘NoneType’ object has no attribute ‘group’

    3. Hi yihong—I’m sorry I haven’t made much progress on the bundle (or blog!) recently. But, I’m not set up to work a bit more frequently on it, so please keep the comments coming!
      Re. OTF fonts, I think there should be a way to configure texify to do what you want it to; I’ll have to look into this, and possibly provide some kind of front-end under sublime text.
      Re. Ctrl Alt C, it looks like the issue is that the plugin cannot find the bibliography file, or that no bibliographic entry matches the first fiew letters to the left of the cursor when you hit the key combination. It is, in any case, a bug, because it should at least fail gracefully and tell you what’s wrong. Again, please be patient—I’ll fix it in the next iteration.

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s