Monday, May 24, 2010

LaTeX: Listings and labels

I have blogged about the listing package here several times (nicely formatting, and hyphenations). This time I will write about creating labels inside a listing. First of all, the listing package allows to define a caption and a label for a listing, e.g.:
\begin{lstlisting}[caption={myListing},label={lst:myListing}]
void foo(int x) {
 doSomething();
 doMore();
}
\end{lstlisting}
Now the listing can be referenced via \ref{lst:myListing}. The listing package also nicely supports line numbers, there are a whole bunch of settings for that. But why do we need line numbers? In most cases, line numbers are used to refer to a certain line within a listing. E.g, we maybe want to write something like
In line 2 of the listing 1 we call method 'doSomething'.
Well, "listing 1" will look like "listing \ref{lst:myListing}" in the LaTeX source code, but how do we reference the line? Fortunately, the listing packages allows us to escape to latex inside a listing and add a label which can then be referenced:
\begin{lstlisting}[caption={myListing},label={lst:myListing},numbers=left,escapeinside={@}{@}]
void foo(int x) {
 @\label{lst:myListing_2}@doSomething();
 doMore();
}
\end{lstlisting}
Now we can reference the line: In line \ref{lst:myListing_2} ... Actually, you can use any label text, however I usually use the listings label with the line number or a small marker, in order to avoid conflicts with duplicate labels. As I do not want to write "line..." and "listing ..." over and over again, I use the autoref command from the hyperref package:
In \autoref{lst:myListing_2} of \autoref{lst:myListing}
autoref automatically adds a name to the reference counter. As there is no name defined for line numbers, it has to be defined previously:
\providecommand*{\lstnumberautorefname}{line} 
If you need names in a another language, you can add a translation:
\addto\extrasngerman{% 
\def\lstlistingautorefname{Quellcode}% 
\def\lstnumberautorefname{Zeile}% 
}
OK, we can add a line label and easily create a reference to that label. However, writing line labels means a lot of work, especially if you use a listing relative name pattern. So, let's add a macro to TeXShop, which uses some shell commands (mainly sed) and AppleScript in order to automatically create the label, even adding the line number to the label:
--Applescript direct
--
-- Create a label inside a listing, must be invoked at the position at which the label is
-- to be created.
-- Precondition: the listing is defined inside a lstlisting environment and 
--   a label is defined in the parameter list of the environment (label={..}).
--
-- New newly created label consists of the label of the listing with the line number
-- appended to the label. You can configure escape characters (as specified in
-- escapeinside={..}{..}) and the separator between listing label and line number.
--
-- E.g. label={myListing}, on line 5 leads to the following output:
-- @\label{lst: myListing!5}@
--
-- (C) 2010 Jens von Pilgrim, http://jevopi.blogspot.com

property texapp : "TeXShop"
property escapeLeft : "@"
property escapeRight : "@"
property separator : "_"
property maxLength : 2000

try
 tell application texapp
  
  if texapp = "TeXShop" then
   tell application "TeXShop" to set pos to the offset of the selection of the front document
   tell application "TeXShop" to set currentSelection to the content of the selection of the front document
  else if texapp = "iTeXMac" then
   -- ??
  end if
  
  set start to 1
  if (pos > maxLength) then
   set start to pos - maxLength
  end if
  
  set preceeding_text to (characters start thru pos of (the text of the front document)) as string
  set preceeding_text to my findAndReplace(preceeding_text, "\\", "_")
  
  set lineNumbers to do shell script ¬
   "echo " & the quoted form of preceeding_text & ¬
   "| sed -n '/label[:space:]*=[:space:]*{[^}]*}/=;$ {x;=;}'"
  
  
  
  set theLines to my splitLines(lineNumbers) -- as list
  set currentLine to last item of theLines as integer
  -- set startLine to item ((length of theLines) - 1) of theLines as integer
  -- length is not working when executed as macro... workaround:
  set startLine to first item of (rest of (reverse of theLines))
  
  set lstLine to currentLine - startLine
  
  set lastLabel to do shell script ¬
   "echo " & the quoted form of preceeding_text & ¬
   "| sed -n '/label[:space:]*=[:space:]*{[^}]*}/h;$ {x;p;}'" & ¬
   "| sed -n 's/.*label[:space:]*=[:space:]*{\\([^}]*\\)}.*/\\1/g;p'"
  
  set newSelection to escapeLeft & "\\label{" & lastLabel & separator & lstLine & "}" & escapeRight & currentSelection
  
  
  if texapp = "TeXShop" then
   tell application "TeXShop" to set the selection of the front document to newSelection
  else if texapp = "iTeXMac" then
   --tell application "iTeXMac" to insert new_section in the text of the front document
  end if
  
 end tell
on error errmesg number errn
 beep
 display dialog errmesg
 return
end try


on findAndReplace(strInString, strFind, strReplace)
 set ditd to text item delimiters
 set text item delimiters to strFind
 set textItems to text items of strInString
 set text item delimiters to strReplace
 if (class of strInString is string) then
  set res to textItems as string
 else -- if (class of TheString is Unicode text) then
  set res to textItems as Unicode text
 end if
 set text item delimiters to ditd
 return res
end findAndReplace


on splitLines(strInString)
 set ditd to text item delimiters
 set text item delimiters to "
"
 set textItems to text items of strInString
 set text item delimiters to ditd
 return textItems
end splitLines
Now things are really easy: Simply activate the macro at the appropriate location in the listing, and a new label will automatically be created. Note: The listing must have a label defined! You can configure the script by changing the properties at the beginning of the macro.