13

I'm wondering if there are some macros for annotating listings as below easily. They are primarily used in books from Manning.

Code annotations

Solution

For anyone who's interested, this is my macro I finally got:

    \newcommand*{\AddNote}[5]{%
    \begin{tikzpicture}[overlay, remember picture]
    \coordinate (x) at (#2,0);
    \coordinate (a) at ($(x)!(#1.north)!($(x)+(0,1)$)$);
    \coordinate (b) at ($(a)+(0.8,0)$);
    \coordinate (c) at ($(b)+(0,#3)$);
    \draw [open triangle 45-] (a) -- (b) -- (c);
    \node[#5] at (c) {\bf\sffamily\smaller#4};
    \end{tikzpicture}%
    }

Usage is: \AddNote{linemarker}{x-offset}{y-offset}{text}{style}

4
  • 4
    Related Question: How can I put a curly brace inside an algorithm to group code lines?. Commented Dec 9, 2012 at 22:48
  • If the linked to question does not help you get the desired result, I would recommend you compose a fully compilable MWE including \documentclass and the appropriate packages that sets up the problem. While solving problems is fun, setting them up is not. Then those trying to help can simply cut and paste your MWE and get started on solving problem. Commented Dec 10, 2012 at 4:37
  • The shift to the right is because your macro introduces two spaces: one before \begin{tikzpicture} and one after \end{tikzpicture}. Put percent signs after the opening brace of the \newcommand and after \end{tikzpicture} and that should stop things shifting. Commented Dec 10, 2012 at 22:16
  • (Oh, I see you asked a new question about that - ignore my above comment.) Commented Dec 10, 2012 at 22:20

3 Answers 3

14

Edit 2018-05-13: This is now in the tikzmark package which is available from all good TeX distributions. See the tikzmark documentation for up to date instructions.


Without a MWE it's hard to know exactly what sort of thing you're after but I think that the listings extension to tikzmark can handle this sort of thing. You need tikzmark.dtx from TeX-SX Launchpad, run tex tikzmark.dtx and put the generated files somewhere that tex can find them (if you run latex tikzmark.dtx or pdflatex tikzmark.dtx it will complain about a missing file - ignore that).

Then the following code:

\documentclass{article}
%\url{http://tex.stackexchange.com/q/86309/86}
\usepackage{listings}
\usepackage{tikz}
\usetikzlibrary{arrows,tikzmark,shadows}
\usetikzmarkextra{listings}

\tikzset{
  comment/.style={
    draw,
    fill=blue!70,
    text=white,
    rounded corners,
    drop shadow,
    align=left,
  },
}

\begin{document}

\begin{lstlisting}[language=TeX,name=texcode,numbers=left,breakatwhitespace=true,breaklines=true]
\newcommand\balloon[4]{%
  \pgfmathtruncatemacro\firstline{%
    #3-1
  }%
  \iftikzmark{line-#2-\firstline-start}{%
    \iftikzmark{line-#2-#3-first}{%
      \xdef\blines{({pic cs:line-#2-\firstline-start} -| {pic           cs:line-#2-#3-first})}%
    }{%
      \iftikzmark{line-#2-#3-start}{%
        \xdef\blines{({pic cs:line-#2-\firstline-start} -| {pic             cs:line-#2-#3-start})}%
      }{%
        \xdef\blines{(pic cs:line-#2-\firstline-start)}%
      }%
    }%
  }{%
    \xdef\blines{}%
  }%
  \foreach \k in {#3,...,#4} {%
    \iftikzmark{line-#2-\k-first}{%
      \xdef\blines{\blines (pic cs:line-#2-\k-first) }
    }{}
    \iftikzmark{line-#2-\k-end}{%
      \xdef\blines{\blines (pic cs:line-#2-\k-end) }
    }{}
  }%
  \ifx\blines\empty
  \else
  \edef\temp{\noexpand\tikz[remember picture,overlay] \noexpand\node[fit={\blines},balloon] (#1) {};}%
\temp
  \fi
}
\end{lstlisting}

\begin{tikzpicture}[remember picture,overlay,>=stealth']
\draw[<-,ultra thick] (pic cs:line-texcode-1-end) +(1em,.7ex) -| +(2.5,1) node[above,comment,thin] {Command name};
\draw[<-,ultra thick] (pic cs:line-texcode-3-end) ++(1em,.7ex) -| +(5.8,1) node[above right,comment,thin] {Find previous line};
\draw[<-,ultra thick] (pic cs:line-texcode-5-end) ++(1em,.7ex) -| +(2.2,.5) node[above,comment,thin] {If previous line exists, add to the list};
\draw[<-,ultra thick] (pic cs:line-texcode-18-end) ++(1em,.7ex) -| +(2.2,.5) node[above,comment,thin] {Loop through rest of lines};
\draw[<-,ultra thick] (pic cs:line-texcode-28-end) ++(1em,.7ex) -| +(1,1.5) node[above,comment,thin] {Add a node covering all the lines};
\end{tikzpicture}
\end{document}

produces:

annotated listings

(Only it looks much nicer in the PDF. And note that there's been an eminent suggestion to replace \usetikzmarkextra with \usetikzmarklibrary which will happen sometime soon so if the above complains about \usetikzmarkextra try switching it.)

5
  • Hi Andrew, thanks for your answer. I already managed to make a macro that fits my needs (although its not perfect ;)). Nevertheless I still have problems when using \onehalfspacing. I think I'll start a new question with that...
    – Matthias
    Commented Dec 10, 2012 at 23:17
  • Note that drawing the comments after the listing has been set is too late if the listing spans more than one page and some of the lines to be commented are not on the last page. Commented Dec 12, 2012 at 21:57
  • @cyberSingularity True. There are (at least) two ways to solve that. The first is to hook in to some part of the page that is always there (for example, the header) and add code that says "If there's a mark, draw its diagram". The other is to use the same hooks as I do in this code but add the actual drawing instructions there (this might be more difficult - I had issues with adding code to those hooks). Commented Dec 12, 2012 at 22:29
  • @cyberSingularity I freely admit that the code underlying the above was developed using the "Let's just try everything and see what works" method. So if anyone should be embarrassed about their ugly code, it's me. Commented Dec 12, 2012 at 22:55
  • Yes, using \usetikzmarklibrary is better. Great answer!
    – Andry
    Commented Oct 8, 2017 at 12:57
7

I promised a not-necessarily-pretty solution to extend Andrew Stacey's excellent answer to support cases where the code spans over more than one page:

\documentclass{article}
\usepackage{listings}
\usepackage{tikz}
\usetikzlibrary{arrows,tikzmark,shadows,calc}
\usetikzmarklibrary{listings}

\tikzset{
  comment/.style={
    draw,
    fill=blue!70,
    text=white,
    rounded corners,
    drop shadow,
    align=left,
  },
}

\usepackage{eso-pic}
\usepackage[calc]{picture}
\newcommand{\addstufftoforegroundall}[1]{%
  \AddToShipoutPictureFG{% Add <stuff> to all following pages' foreground
    \put(0,\paperheight){\vtop{{\null}\makebox[0pt][l]{#1}}}%
  }%
}%
\newcommand{\addstufftoforegroundthis}[1]{%
  \AddToShipoutPictureFG*{% Add <stuff> to the current page foreground
    \put(0,\paperheight){\vtop{{\null}\makebox[0pt][l]{#1}}}%
  }%
}%

\setcounter{errorcontextlines}{\maxdimen}


\makeatletter
\def\getpicturepage#1{%
  \@nameuse{save@pg@\@nameuse{save@pt@#1}}%
}

\newcommand*{\iftikzmarkcurrentpage}[3]{%
    \iftikzmark{#1}{%
        \ifcsname save@pg@\@nameuse{save@pt@#1}\endcsname
            \expandafter\ifnum\getpicturepage{#1}=\the\c@page\relax
                #2%
            \else
                #3%
            \fi
        \else
            #3%
        \fi
    }{%
        #3
    }%
}

\newcommand*{\appendhookorcreatenew}[2]{%#1<- hook csname, #2<- code to add
    % note: code in #2 is not expanded at this stage!
    \ifcsname #1\endcsname
        \expandafter\g@addto@macro\csname #1\endcsname{\unexpanded{#2}}%
    \else
        \@namedef{#1}{\unexpanded{#2}}%
    \fi
}
\newcommand*{\deferlinecode}[3]{% #1<-listing name, #2<-line number, #3<-code
    \appendhookorcreatenew{listings-deferline-#1-#2}{#3}%
}
\newcommand*{\atEOLcode}[3]{% #1<-listing name, #2<-line number, #3<-code
    \appendhookorcreatenew{listings-eol-execute-#1-#2}{#3}%
}
\newcommand*{\commenton}[4][comment,thin]{% #1<- node keys, #2<- listing name, #3<- line number, #4<- comment text
    \deferlinecode{#2}{#3}{\tikz[overlay,remember picture] \draw[,>=stealth',<-,ultra thick] (pic cs:line-#2-#3-end) +(1em,.7ex) -| ($(node cs:name=current page,anchor=north east)!(pic cs:line-#2-#3-end)!(node cs:name=current page,anchor=south east) +(-5cm,0cm)$) node[#1] {#4};}%
}

\lst@AddToHook{EOL}{%
    \begingroup
    \ifcsname listings-deferline-\lst@name-\the\c@lstnumber\endcsname
        \iftikzmarkcurrentpage{line-\lst@name-\the\c@lstnumber-end}{%
            \let\addstufftoforeground\addstufftoforegroundthis
        }{%
            \let\addstufftoforeground\addstufftoforegroundall
        }%
        \edef\pagehookstuff{%
            \noexpand\addstufftoforeground{%
                \noexpand\iftikzmarkcurrentpage{line-\lst@name-\the\c@lstnumber-end}{%
                    \@nameuse{listings-deferline-\lst@name-\the\c@lstnumber}%
                }{}%
            }%
        }%
        \global\expandafter\let\csname listings-deferline-\lst@name-\the\c@lstnumber\endcsname\@undefined
        \pagehookstuff
    \fi
    \ifcsname listings-eol-execute-\lst@name-\the\c@lstnumber\endcsname
        \@nameuse{listings-eol-execute-\lst@name-\the\c@lstnumber}%
    \fi
    \endgroup
}



\commenton[comment,thin,text width=5cm]{texcode}{3}{This line sets the title of the document but does not result in any visible output, yet.}
\atEOLcode{texcode}{3}{\vspace{5ex}}
\commenton[comment,thin]{texcode}{4}{Similarly for author.}
\atEOLcode{texcode}{4}{\vspace{2ex}}
\commenton[comment,thin]{texcode}{5}{And date.}
\commenton[comment,thin,fill=red!70,text=black]{texcode}{7}{This line actually shows them.}


\lstset{
    language={[LaTeX]TeX},
    numbers=left,
    breaklines=true,
    basicstyle=\small\ttfamily,
    columns=flexible,
}

\usepackage[a5paper,landscape]{geometry}
\usepackage{url}

\begin{document}

\title{Example output of code annotation}
\author{\url{http://tex.stackexchange.com/questions/86309/macros-for-code-annotations}}
\date{16 December 2012}
\maketitle

This is only to illustrate how one might go about adding annotations to code that spans across more than one page.

The annotations to be added have to specified before the listing appears.
It is not very robust to lost notes, eg if attempting to add an annotation to a blank line where there may not be an end marker.

\begin{lstlisting}[name=texcode]
\documentclass{article}
\usepackage{listings}
\title{Sample Document}
\author{John Smith}
\date{\today}
\begin{document}
\maketitle
Hello World!
% This is a comment.
\end{document}
\end{lstlisting}

\end{document}

(Example listing borrowed from elsewhere.)

If adjacent comment nodes might overlap, I suggested just adding some extra space below commented lines. It is probably evident that I struggle a lot with positioning in TikZ.

enter image description here

0

Based on the previous answers I have created some general purpose macros that alswo work on listings split across multiple pages:

\documentclass{article}
\usepackage[a5paper,landscape]{geometry}
\usepackage{url}
\usepackage{listings}
\usepackage{tikz}
\usepackage{eso-pic}
\usepackage[calc]{picture}
\usetikzlibrary{decorations.pathreplacing,arrows,tikzmark,calc}

\usetikzmarklibrary{listings}

\newcommand{\addstufftoforegroundall}[1]{%
    \AddToShipoutPictureFG{% Add <stuff> to all following pages' foreground
        \put(0,\paperheight){\vtop{{\null}\makebox[0pt][l]{#1}}}%
    }%
}%
\newcommand{\addstufftoforegroundthis}[1]{%
    \AddToShipoutPictureFG*{% Add <stuff> to the current page foreground
        \put(0,\paperheight){\vtop{{\null}\makebox[0pt][l]{#1}}}%
    }%
}%

\makeatletter
\def\getpicturepage#1{%
    \@nameuse{save@pg@\@nameuse{save@pt@#1}}%
}

\lst@AddToHook{EOL}{%
    \begingroup
    \ifcsname listings-deferline-\lst@name-\the\c@lstnumber\endcsname
    \iftikzmarkcurrentpage{line-\lst@name-\the\c@lstnumber-end}{%
        \let\addstufftoforeground\addstufftoforegroundthis
    }{%
        \let\addstufftoforeground\addstufftoforegroundall
    }%
    \edef\pagehookstuff{%
        \noexpand\addstufftoforeground{%
            \noexpand\iftikzmarkcurrentpage{line-\lst@name-\the\c@lstnumber-end}{%
                \@nameuse{listings-deferline-\lst@name-\the\c@lstnumber}%
            }{}%
        }%
    }%
    \global\expandafter\let\csname listings-deferline-\lst@name-\the\c@lstnumber\endcsname\@undefined
    \pagehookstuff
    \fi
    \ifcsname listings-eol-execute-\lst@name-\the\c@lstnumber\endcsname
    \@nameuse{listings-eol-execute-\lst@name-\the\c@lstnumber}%
    \fi
    \endgroup
}

\newcommand*{\iftikzmarkcurrentpage}[3]{%
    \iftikzmark{#1}{%
        \ifcsname save@pg@\@nameuse{save@pt@#1}\endcsname
        \expandafter\ifnum\getpicturepage{#1}=\the\c@page\relax
        #2%
        \else
        #3%
        \fi
        \else
        #3%
        \fi
    }{%
        #3
    }%
}
\newcommand*{\appendhookorcreatenew}[2]{%#1<- hook csname, #2<- code to add
    % note: code in #2 is not expanded at this stage!
    \ifcsname #1\endcsname
    \expandafter\g@addto@macro\csname #1\endcsname{\unexpanded{#2}}%
    \else
    \@namedef{#1}{\unexpanded{#2}}%
    \fi
}
\newcommand*{\deferlinecode}[3]{% #1<-listing name, #2<-line number, #3<-code
    \appendhookorcreatenew{listings-deferline-#1-#2}{#3}%
}

\colorlet{annotationColor}{red}
\newlength\annotationLinewidth
\setlength\annotationLinewidth{0.6pt}
\tikzset{%
    note/.style={annotationColor,
        font=\sffamily\scriptsize,%\bfseries
        align=center, 
        text width=2.5cm
    },
    every round node/.style={
        draw,
        shape=rounded rectangle,
        rounded rectangle arc length=180,
        inner sep=+.333em,
        text depth=+.1ex}
}
% utility function
\makeatletter
\def\ifPositive#1{%
    \@ifnextchar{-}%
    {\expandafter\@secondoftwo\remove@to@nnil}%
    {\expandafter\@firstoftwo\remove@to@nnil}%
    #1\@nnil
}
\makeatother
%\AddNoteVBrace{listing}{line-start}{line-end}{x-pos}{text}
\newcommand*{\AddNoteVBrace}[5]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \draw [decoration={brace,amplitude=0.5em},decorate,annotationColor,line width=\annotationLinewidth]
        ($([xshift=#4]pic cs:line-#1-#2-start)!([yshift=1ex]pic cs:line-#1-#2-start)!($([xshift=#4]pic cs:line-#1-#2-start)-(0,1)$)$) --  
        ($([xshift=#4]pic cs:line-#1-#3-start)!([yshift=-.25ex]pic cs:line-#1-#3-start)!($([xshift=#4]pic cs:line-#1-#3-start)-(0,1)$)$)
        node [note, pos=0.5,anchor=west] {#5};}%
}%
%\AddNoteVLine{listing}{line-start}{line-end}{x-pos}{text}
\newcommand*{\AddNoteVLine}[5]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \draw [annotationColor,line width=\annotationLinewidth]
        ($([xshift=#4]pic cs:line-#1-#2-start)!([yshift=1ex]pic cs:line-#1-#2-start)!($([xshift=#4]pic cs:line-#1-#2-start)-(0,1)$)$) --  
        ($([xshift=#4]pic cs:line-#1-#3-start)!([yshift=-.25ex]pic cs:line-#1-#3-start)!($([xshift=#4]pic cs:line-#1-#2-start)-(0,1)$)$)
        node [note, align=left, pos=0.5, anchor=west] {#5};}%
}%
%\AddNoteRight{listing}{line}{label-x-offset}{label-y-offset}{text}
\newcommand*{\AddNoteRight}[5]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \def\anchor{south}\ifx&#4&\else \ifPositive{#4}{}{\def\anchor{north}}\fi
        \draw[<-,>=stealth,annotationColor,line width=\annotationLinewidth] (pic cs:line-#1-#2-end) +(0.25,.5ex) -| +(\ifx&#3&1\else#3\fi,\ifx&#4&2.5ex\else#4\fi) node[note,anchor=\anchor] {#5};}%
}%
%\AddNoteTop{listing}{line}{x-pos}{label-x-offset}{label-y-offset}{text}
\newcommand*{\AddNoteTop}[6]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \def\anchor{west}\def\align{left}
        \ifx&#4&\else \ifPositive{#4}{}{\def\anchor{east}\def\align{right}}\fi
        \draw[<-,>=stealth,annotationColor,line width=\annotationLinewidth] ($([xshift=#3]pic cs:line-#1-#2-start) +(0,2.5ex)$) |- +(\ifx&#4&1\else#4\fi,\ifx&#5&2.5ex\else#5\fi) 
        node[note,align=\align,anchor=\anchor] {#6};}%
}%
%\AddNoteBottom{listing}{line}{x-pos}{label-x-offset}{label-y-offset}{text}
\newcommand*{\AddNoteBottom}[6]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \def\anchor{west}\def\align{left}
        \ifx&#4&\else \ifPositive{#4}{}{\def\anchor{east}\def\align{right}}\fi
        \draw[<-,>=stealth,annotationColor,line width=\annotationLinewidth] ($([xshift=#3]pic cs:line-#1-#2-start) +(0,-1.5ex)$) |- +(\ifx&#4&1\else#4\fi,\ifx&#5&-2.5ex\else-#5\fi) 
        node[note,align=\align,anchor=\anchor] {#6};}%
}%
%\AddBox{listing}{line}{x-start}{x-end}
\newcommand*{\AddBox}[4]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
    \draw[annotationColor,line width=\annotationLinewidth] ($([xshift=#3]pic cs:line-#1-#2-start) +(-.25ex,-.25ex)$) rectangle ($([xshift=#4]pic cs:line-#1-#2-start) +(.5ex,2.0ex)$);}%
}%
%\AddUnderline{listing}{line}{x-start}{x-end}
\newcommand*{\AddUnderline}[4]{%
    \deferlinecode{#1}{#2}{\tikz[overlay, remember picture]
        \draw[annotationColor,line width=\annotationLinewidth] ($([xshift=#3]pic cs:line-#1-#2-start) +(0,-.25ex)$) -- ($([xshift=#4]pic cs:line-#1-#2-start) +(0,-.25ex)$);}%
}%


\AddNoteVBrace{texcode}{1}{3}{4cm}{Note VBrace}
\AddNoteVLine{texcode}{5}{7}{4cm}{Note VLine}
\AddUnderline{texcode}{9}{0cm}{3cm}
\AddBox{texcode}{11}{1cm}{3cm}
\AddNoteTop{texcode}{15}{0cm}{5cm}{}{Note Top}
\AddNoteRight{texcode}{15}{3cm}{-2cm}{Note Right}
\AddNoteBottom{texcode}{15}{1cm}{}{}{Note Bottom}

\lstset{
    language={[LaTeX]TeX},
    numbers=left,
    breaklines=true,
    basicstyle=\small\ttfamily,
    columns=flexible,
}

\begin{document}
    
\title{Example output of code annotation}
\author{\url{http://tex.stackexchange.com/questions/86309/macros-for-code-annotations}}
\maketitle

This is only to illustrate how one might go about adding annotations to code that spans across more than one page.

The annotations to be added have to specified before the listing appears.
It is not very robust to lost notes, eg if attempting to add an annotation to a blank line where there may not be an end marker.

\begin{lstlisting}[name=texcode]
\documentclass{article}

\usepackage{listings}

\title{Sample Document}
\author{John Smith}
\date{\today}

\begin{document}
    
    \maketitle
    Hello World!
    % This is a comment.
    
\end{document}
\end{lstlisting}
    
\end{document}

enter image description here

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .