PR #89 TemplateProcessingTracer reworked:
- Instead of passing several arguments to the TemplateProcessingTracer method, only pass a single TracedElement object. This allow use to add new methods later without breaking backward compatibility.
- Added TracedElement.getDescription() to give a single-line canonical description of the element (same format that we use in FTL stack taces)
- Expose the Environment object to TemplateProcessingTracer methods
- Do not expose the TracedElement to exitElement, because we can't always ensure that it's available because of some internal tricks
- Added special branch to iterator directives and if-elseif-else, as because of the element stack tricks they apply normally, some elements were hidden from the tracer
- Some minor renaming.
- Added getter to Environment.
- Extended test coverage, and fixed quotation logic in the test (it was broken for example for ?interpret-ed snippets)
- Added more documentation
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 6cbfe28..66f1214 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -188,7 +188,7 @@
 
     private boolean fastInvalidReferenceExceptions;
 
-    private TemplateProcessingTracer currentTracer;
+    private TemplateProcessingTracer templateProcessingTracer;
 
     /**
      * Retrieves the environment object associated with the current thread, or {@code null} if there's no template
@@ -2882,10 +2882,22 @@
     }
 
     /**
-     * Sets the tracer to use for this environment.
+     * Sets the {@link TemplateProcessingTracer} to use for this {@link Environment};
+     * can be {@code null} to not have one. The default is also {@code null}.
+     *
+     * @since 2.3.33
      */
-    public void setTracer(TemplateProcessingTracer tracer) {
-        currentTracer = tracer;
+    public void setTemplateProcessingTracer(TemplateProcessingTracer templateProcessingTracer) {
+        this.templateProcessingTracer = templateProcessingTracer;
+    }
+
+    /**
+     * Getter pair of {@link #setTemplateProcessingTracer(TemplateProcessingTracer)}. Can be {@code null}.
+     *
+     * @since 2.3.33
+     */
+    public TemplateProcessingTracer getTemplateProcessingTracer() {
+        return templateProcessingTracer;
     }
 
     private void pushElement(TemplateElement element) {
@@ -2900,19 +2912,15 @@
             this.instructionStack = instructionStack;
         }
         instructionStack[newSize - 1] = element;
-        if (currentTracer != null) {
-            currentTracer.enterElement(element.getTemplate(),
-                    element.getBeginColumn(), element.getBeginLine(),
-                    element.getEndColumn(), element.getEndLine(), element.isLeaf());
+        if (templateProcessingTracer != null) {
+            templateProcessingTracer.enterElement(this, element);
         }
     }
 
     private void popElement() {
-        if (currentTracer != null) {
+        if (templateProcessingTracer != null) {
             TemplateElement element = instructionStack[instructionStackSize - 1];
-            currentTracer.exitElement(element.getTemplate(),
-                    element.getBeginColumn(), element.getBeginLine(),
-                    element.getEndColumn(), element.getEndLine());
+            templateProcessingTracer.exitElement(this);
         }
         instructionStackSize--;
     }
diff --git a/src/main/java/freemarker/core/IfBlock.java b/src/main/java/freemarker/core/IfBlock.java
index 223d755..52615d2 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -42,12 +42,24 @@
     @Override
     TemplateElement[] accept(Environment env) throws TemplateException, IOException {
         int ln  = getChildCount();
-        for (int i = 0; i < ln; i++) {
-            ConditionalBlock cblock = (ConditionalBlock) getChild(i);
-            Expression condition = cblock.condition;
-            env.replaceElementStackTop(cblock);
-            if (condition == null || condition.evalToBoolean(env)) {
-                return cblock.getChildBuffer();
+        if (env.getTemplateProcessingTracer() == null) {
+            for (int i = 0; i < ln; i++) {
+                ConditionalBlock cblock = (ConditionalBlock) getChild(i);
+                Expression condition = cblock.condition;
+                env.replaceElementStackTop(cblock);
+                if (condition == null || condition.evalToBoolean(env)) {
+                    return cblock.getChildBuffer();
+                }
+            }
+        } else {
+            for (int i = 0; i < ln; i++) {
+                ConditionalBlock cblock = (ConditionalBlock) getChild(i);
+                Expression condition = cblock.condition;
+                env.replaceElementStackTop(cblock);
+                if (condition == null || condition.evalToBoolean(env)) {
+                    env.visit(cblock);
+                    return null;
+                }
             }
         }
         return null;
diff --git a/src/main/java/freemarker/core/ListElseContainer.java b/src/main/java/freemarker/core/ListElseContainer.java
index 856e5b0..4e307c3 100644
--- a/src/main/java/freemarker/core/ListElseContainer.java
+++ b/src/main/java/freemarker/core/ListElseContainer.java
@@ -37,9 +37,24 @@
 
     @Override
     TemplateElement[] accept(Environment env) throws TemplateException, IOException {
-        if (listPart.acceptWithResult(env)) {
+        boolean hadItems;
+
+        TemplateProcessingTracer templateProcessingTracer = env.getTemplateProcessingTracer();
+        if (templateProcessingTracer == null) {
+            hadItems = listPart.acceptWithResult(env);
+        } else {
+            templateProcessingTracer.enterElement(env, listPart);
+            try {
+                hadItems = listPart.acceptWithResult(env);
+            } finally {
+                templateProcessingTracer.exitElement(env);
+            }
+        }
+
+        if (hadItems) {
             return null;
         }
+
         return new TemplateElement[] { elsePart };
     }
 
diff --git a/src/main/java/freemarker/core/TemplateElement.java b/src/main/java/freemarker/core/TemplateElement.java
index 6cb9b54..2e79d93 100644
--- a/src/main/java/freemarker/core/TemplateElement.java
+++ b/src/main/java/freemarker/core/TemplateElement.java
@@ -37,7 +37,7 @@
  *             it.
  */
 @Deprecated
-abstract public class TemplateElement extends TemplateObject {
+abstract public class TemplateElement extends TemplateObject implements TemplateProcessingTracer.TracedElement {
 
     private static final int INITIAL_REGULATED_CHILD_BUFFER_CAPACITY = 6;
 
@@ -89,9 +89,9 @@
      * One-line description of the element, that contains all the information that is used in
      * {@link #getCanonicalForm()}, except the nested content (elements) of the element. The expressions inside the
      * element (the parameters) has to be shown. Meant to be used for stack traces, also for tree views that don't go
-     * down to the expression-level. There are no backward-compatibility guarantees regarding the format used ATM, but
-     * it must be regular enough to be machine-parseable, and it must contain all information necessary for restoring an
-     * AST equivalent to the original.
+     * down to the expression-level. There are no backward-compatibility guarantees regarding the format used, although
+     * it shouldn't change unless to fix a bug. It must be regular enough to be machine-parseable, and it must contain
+     * all information necessary for restoring an AST equivalent to the original.
      * 
      * This final implementation calls {@link #dump(boolean) dump(false)}.
      * 
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index e535d08..7ef6cfb 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -86,24 +86,36 @@
         this.endColumn = endColumn;
         this.endLine = endLine;
     }
-    
-    public final int getBeginColumn() {
-        return beginColumn;
-    }
 
+    /**
+     * 1-based index of the line (row) of the first character of the element in the template.
+     */
     public final int getBeginLine() {
         return beginLine;
     }
 
-    public final int getEndColumn() {
-        return endColumn;
+    /**
+     * 1-based index of the column of the first character of the element in the template.
+     */
+    public final int getBeginColumn() {
+        return beginColumn;
     }
 
+    /**
+     * 1-based index of the line (row) of the last character of the element in the template.
+     */
     public final int getEndLine() {
         return endLine;
     }
 
     /**
+     * 1-based index of the column of the last character of the element in the template.
+     */
+    public final int getEndColumn() {
+        return endColumn;
+    }
+
+    /**
      * Returns a string that indicates
      * where in the template source, this object is.
      */
diff --git a/src/main/java/freemarker/core/TemplateProcessingTracer.java b/src/main/java/freemarker/core/TemplateProcessingTracer.java
index 434891e..2130e6c 100644
--- a/src/main/java/freemarker/core/TemplateProcessingTracer.java
+++ b/src/main/java/freemarker/core/TemplateProcessingTracer.java
@@ -19,38 +19,77 @@
 
 package freemarker.core;
 
-import freemarker.ext.util.IdentityHashMap;
-import freemarker.template.Configuration;
 import freemarker.template.Template;
-import freemarker.template.TemplateDirectiveModel;
-import freemarker.template.TemplateTransformModel;
-import freemarker.template.utility.ObjectFactory;
 
 /**
- * Run-time tracer plug-in. This may be * used to implement profiling, coverage analytis, execution tracing,
+ * Hooks to monitor as templates run. This may be used to implement profiling, coverage analysis, execution tracing,
  * and other on-the-fly debugging mechanisms.
  * <p>
- * Use {@link Environment#setTracer(TemplateProcessingTracer)} to configure a tracer for the current environment.
+ * Use {@link Environment#setTemplateProcessingTracer(TemplateProcessingTracer)} to set a tracer for the current
+ * environment.
  * 
  * @since 2.3.33
  */
 public interface TemplateProcessingTracer {
 
     /**
-     * Invoked by {@link Environment} whenever it starts processing a new template element. {@code
-     * isLeafElement} indicates whether this element is a leaf, or whether the tracer should expect
-     * to receive lower-level elements within the context of this one.
+     * Invoked by {@link Environment} whenever it starts processing a new template element. A template element is a
+     * directive call, an interpolation (like <code>${...}</code>), a comment block, or static text. Expressions
+     * are not template elements.
      * 
      * @since 2.3.23
      */
-    void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine,
-            boolean isLeafElement);
+    void enterElement(Environment env, TracedElement tracedElement);
 
     /**
      * Invoked by {@link Environment} whenever it completes processing a new template element.
+     *
+     * @see #enterElement(Environment, TracedElement)
      * 
      * @since 2.3.23
      */
-    void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine);
+    void exitElement(Environment env);
+
+    /**
+     * Information about the template element that we enter of exit.
+     */
+    interface TracedElement {
+        /**
+         * The {@link Template} that contains this element.
+         */
+        Template getTemplate();
+
+        /**
+         * 1-based index of the line (row) of the first character of the element in the template.
+         */
+        int getBeginLine();
+
+        /**
+         * 1-based index of the column of the first character of the element in the template.
+         */
+        int getBeginColumn();
+
+        /**
+         * 1-based index of the line (row) of the last character of the element in the template.
+         */
+        int getEndColumn();
+
+        /**
+         * 1-based index of the column of the last character of the element in the template.
+         */
+        int getEndLine();
+
+        /**
+         * If this is an element that has no nested elements.
+         */
+        boolean isLeaf();
+
+        /**
+         * One-line description of the element, that also contains the parameter expressions, but not the nested content
+         * (child elements). There are no hard backward-compatibility guarantees regarding the format used, although
+         * it shouldn't change unless to fix a bug.
+         */
+        String getDescription();
+    }
 
 }
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index 967d222..c6fcc31 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -764,7 +764,8 @@
      * 
      * @param beginColumn the first column of the requested source, 1-based
      * @param beginLine the first line of the requested source, 1-based
-     * @param endColumn the last column of the requested source, 1-based
+     * @param endColumn the last column of the requested source, 1-based. If this is beyond the last character of the
+     *                  line, it assumes that you want to whole line.
      * @param endLine the last line of the requested source, 1-based
      * 
      * @see freemarker.core.TemplateObject#getSource()
@@ -787,7 +788,7 @@
             }
         }
         int lastLineLength = lines.get(endLine).toString().length();
-        int trailingCharsToDelete = lastLineLength - endColumn - 1;
+        int trailingCharsToDelete = endColumn < lastLineLength ? lastLineLength - endColumn - 1 : 0;
         buf.delete(0, beginColumn);
         buf.delete(buf.length() - trailingCharsToDelete, buf.length());
         return buf.toString();
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 26bc653..d5969fe 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -30140,6 +30140,21 @@
                 </listitem>
               </itemizedlist>
             </listitem>
+
+            <listitem>
+              <para><link
+              xlink:href="https://github.com/apache/freemarker/pull/89">GitHub
+              PR 89</link>: Added <literal>TemplateProcessingTracer</literal>
+              mechanism, that can be used to monitor coverage, and performance
+              <emphasis>inside</emphasis> templates as they are being
+              processed. For example, you could construct a heat map for how
+              often the different parts run, or finding the performance hot
+              spots. (There can be other creative uses, like watching for a
+              variable to have a certain value.) Use
+              <literal>Environment.setTemplateProcessingTracer(TemplateProcessingTracer)</literal>
+              to enable this kind of monitoring. (See the API docs for
+              more.)</para>
+            </listitem>
           </itemizedlist>
         </section>
 
diff --git a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
index f069dc1..c73554d 100644
--- a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
+++ b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
@@ -20,15 +20,15 @@
 
 import static org.junit.Assert.*;
 
-import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 import org.junit.Test;
 
 import freemarker.template.Configuration;
 import freemarker.template.Template;
+import freemarker.template.utility.NullWriter;
+import freemarker.template.utility.StringUtil;
 
 public class TemplateProcessingTracerTest {
 
@@ -43,41 +43,203 @@
             "<#list [] as item>\n" +
             "${item}<#else>" +
             "Yup.\n" +
-            "</#list>\n";
+            "</#list>\n" +
+            "<#list 1..2 as i>${i}</#list>" +
+            "<#list 1..3 as j>${j}<#sep>, </#list>" +
+            "<#foreach k in 1..2>k=${k}</#foreach>" +
+            "<#attempt>succeed<#recover>not visited</#attempt>" +
+            "<#attempt>will fail${fail}<#recover>recover</#attempt>" +
+            "<@('x'?interpret) />" +
+            "<#if true>t<#else>f</#if>" +
+            "<#if false>t<#else>f</#if>" +
+            "<#if false>t1<#elseif false>f1<#else>f2</#if>" +
+            "<#if false>t1<#elseif true>t2<#else>f2</#if>" +
+            "<#switch 2>" +
+            "<#case 1>C1<#break>" +
+            "<#case 2>C2<#break>" +
+            "<#case 3>C3<#break>" +
+            "<#default>D" +
+            "</#switch>" +
+            "<#switch 3>" +
+            "<#case 1>C1<#break>" +
+            "<#case 2>C3<#break>" +
+            "<#default>D" +
+            "</#switch>" +
+            "<#macro m>Hello from m!</#macro>" +
+            "Calling macro: <@m />" +
+            "<#assign t>captured</#assign>" +
+            "\n";
 
     @Test
     public void test() throws Exception {
         Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
         Template t = new Template("test.ftl", TEMPLATE_TEXT, cfg);
-        StringWriter sw = new StringWriter();
-        Tracer tracer = new Tracer(TEMPLATE_TEXT);
-        Environment env = t.createProcessingEnvironment(null, sw);
-        env.setTracer(tracer);
+        TestTemplateProcessingTracer tracer = new TestTemplateProcessingTracer();
+        Environment env = t.createProcessingEnvironment(null, NullWriter.INSTANCE);
+        env.setTemplateProcessingTracer(tracer);
         env.process();
 
-        List<String> expected = Arrays.asList("Yup.", "Always.", "${item}", "${item}", "${item}", "Yup.");
-        assertEquals(expected, tracer.elementsVisited);
+        System.out.println();
+        for (String it : tracer.leafElementSourceSnippets) {
+            System.out.println(StringUtil.jQuote(it) + ",");
+        }
+        System.out.println();
+        for (String it : tracer.indentedElementDescriptions) {
+            System.out.println("|" + it);
+        }
+        System.out.println();
+
+        assertEquals(
+                List.of(
+                        "Yup.\n",
+                        "Always.\n",
+                        "${item}",
+                        "${item}",
+                        "${item}",
+                        "Yup.\n",
+                        "${i}",
+                        "${i}",
+                        "${j}",
+                        ", ",
+                        "${j}",
+                        ", ",
+                        "${j}",
+                        "k=",
+                        "${k}",
+                        "k=",
+                        "${k}",
+                        "succeed",
+                        "will fail",
+                        "${fail}",
+                        "recover",
+                        "<@('x'?interpret) />",
+                        "x",
+                        "t",
+                        "f",
+                        "f2",
+                        "t2",
+                        "C2",
+                        "<#break>",
+                        "D",
+                        "Calling macro: ",
+                        "<@m />",
+                        "Hello from m!",
+                        "captured",
+                        "\n"
+                ),
+                tracer.leafElementSourceSnippets);
+
+        assertEquals(
+                List.of(
+                        "root",
+                        " #if 0 == 1",
+                        " #if 1 == 1",
+                        "  text \"Yup.\\n\"",
+                        " text \"Always.\\n\"",
+                        " #list-#else-container",
+                        "  #list [1, 2, 3] as item",
+                        "   ${item}",
+                        "   ${item}",
+                        "   ${item}",
+                        " #list-#else-container",
+                        "  #list [] as item",
+                        "  #else",
+                        "   text \"Yup.\\n\"",
+                        " #list 1..2 as i",
+                        "  ${i}",
+                        "  ${i}",
+                        " #list 1..3 as j",
+                        "  ${j}",
+                        "  #sep",
+                        "   text \", \"",
+                        "  ${j}",
+                        "  #sep",
+                        "   text \", \"",
+                        "  ${j}",
+                        "  #sep",
+                        " #foreach k in 1..2",
+                        "  text \"k=\"",
+                        "  ${k}",
+                        "  text \"k=\"",
+                        "  ${k}",
+                        " #attempt",
+                        "  text \"succeed\"",
+                        " #attempt",
+                        "  #mixed_content",
+                        "   text \"will fail\"",
+                        "   ${fail}",
+                        "  #recover",
+                        "   text \"recover\"",
+                        " @(\"x\"?interpret)",
+                        "  text \"x\"",
+                        " #if-#elseif-#else-container",
+                        "  #if true",
+                        "   text \"t\"",
+                        " #if-#elseif-#else-container",
+                        "  #else",
+                        "   text \"f\"",
+                        " #if-#elseif-#else-container",
+                        "  #else",
+                        "   text \"f2\"",
+                        " #if-#elseif-#else-container",
+                        "  #elseif true",
+                        "   text \"t2\"",
+                        " #switch 2",
+                        "  #case 2",
+                        "   text \"C2\"",
+                        "   #break",
+                        " #switch 3",
+                        "  #default",
+                        "   text \"D\"",
+                        " #macro m",
+                        " text \"Calling macro: \"",
+                        " @m",
+                        "  #macro m",
+                        "   text \"Hello from m!\"",
+                        " #assign t = .nested_output",
+                        "  text \"captured\"",
+                        " text \"\\n\""
+                ),
+                tracer.indentedElementDescriptions);
     }
 
-    private static class Tracer implements TemplateProcessingTracer {
-        final ArrayList<String> elementsVisited;
-        final String[] templateLines;
+    private static class TestTemplateProcessingTracer implements TemplateProcessingTracer {
+        private final List<String> leafElementSourceSnippets = new ArrayList<>();
+        private final List<String> indentedElementDescriptions = new ArrayList<>();
+        private String indentation = null;
 
-        Tracer(String template) {
-            elementsVisited = new ArrayList<>();
-            templateLines = template.split("\\n");
-        }
+        public void enterElement(Environment env, TracedElement tracedElement) {
+            if (indentation == null) {
+                indentation = "";
+            } else {
+                indentation += " ";
+            }
 
-        public void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine,
-            boolean isLeafElement) {
-            if (isLeafElement) {
-                String line = templateLines[beginLine - 1];
-                String elementText = line.substring(beginColumn - 1,
-                        endLine == beginLine ? Math.min(endColumn, line.length()) : line.length());
-                elementsVisited.add(elementText);
+            indentedElementDescriptions.add(indentation + tracedElement.getDescription());
+
+            if (tracedElement.isLeaf()) {
+                int beginColumn = tracedElement.getBeginColumn();
+                int beginLine = tracedElement.getBeginLine();
+                int endLine = tracedElement.getEndLine();
+                int endColumn = tracedElement.getEndColumn();
+
+                String suffix;
+                if (beginLine != endLine) {
+                    endLine = beginLine;
+                    endColumn = Integer.MAX_VALUE;
+                    suffix = "[...]";
+                } else {
+                    suffix = "";
+                }
+
+                String sourceQuotation = tracedElement.getTemplate()
+                        .getSource(beginColumn, beginLine, endColumn, endLine);
+                leafElementSourceSnippets.add(sourceQuotation + suffix);
             }
         }
 
-        public void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {}
+        public void exitElement(Environment env) {
+            indentation = indentation.isEmpty() ? null : indentation.substring(0, indentation.length() - 1);
+        }
     }
 }