In progress: [Issue 104] Update "Next Id" comment when generating a the
tag number of a field or literal

Almost done. Need to finish preference page to add/edit/remove patterns.
diff --git a/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder_matchingCommentNode_Test.java b/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder_matchingCommentNode_Test.java
index bbf9db4..2ac8385 100644
--- a/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder_matchingCommentNode_Test.java
+++ b/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder_matchingCommentNode_Test.java
@@ -10,22 +10,23 @@
 
 import static com.google.eclipse.protobuf.junit.util.Finder.findProperty;
 import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.*;
 import static org.junit.Assert.assertThat;
 
+import java.util.regex.Matcher;
+
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.xtext.nodemodel.INode;
+import org.eclipse.xtext.util.Pair;
+import org.junit.*;
+
 import com.google.eclipse.protobuf.junit.core.XtextRule;
 import com.google.eclipse.protobuf.junit.util.MultiLineTextBuilder;
 import com.google.eclipse.protobuf.protobuf.*;
 
-import org.eclipse.emf.ecore.EObject;
-import org.eclipse.xtext.nodemodel.INode;
-import org.junit.*;
-
-import java.util.regex.Pattern;
-
 /**
- * Tests for <code>{@link CommentNodesFinder#matchingCommentNode(EObject, Pattern...)}</code>.
- * 
+ * Tests for <code>{@link CommentNodesFinder#matchingCommentNode(EObject, String...)}</code>.
+ *
  * @author alruiz@google.com (Alex Ruiz)
  */
 public class CommentNodesFinder_matchingCommentNode_Test {
@@ -33,34 +34,48 @@
   @Rule public XtextRule xtext = new XtextRule();
 
   private CommentNodesFinder finder;
-  
+
   @Before public void setUp() {
     finder = xtext.getInstanceOf(CommentNodesFinder.class);
   }
-  
+
   @Test public void should_return_matching_single_line_comment_of_element() {
     MultiLineTextBuilder proto = new MultiLineTextBuilder();
-    proto.append("message Person {                                   ")
-         .append("  // Indicates whether the person is active or not.")
-         .append("  optional bool active = 1;                        ")
-         .append("}                                                  ");
+    proto.append("message Person {           ")
+         .append("  // Next Id: 6            ")
+         .append("  optional bool active = 1;")
+         .append("}                          ");
     Protobuf root = xtext.parse(proto);
     Property active = findProperty("active", root);
-    INode node = finder.matchingCommentNode(active, Pattern.compile(".*"));
-    assertThat(node.getText().trim(), equalTo("// Indicates whether the person is active or not."));
+    Pair<INode, Matcher> match = finder.matchingCommentNode(active, "next id: [\\d]+");
+    INode node = match.getFirst();
+    assertThat(node.getText().trim(), equalTo("// Next Id: 6"));
   }
 
   @Test public void should_return_matching_multi_line_comment_of_element() {
     MultiLineTextBuilder proto = new MultiLineTextBuilder();
-    proto.append("message Person {                                   ")
-         .append("  /*                                               ")
-         .append("   * Indicates whether the person is active or not.")
-         .append("   */                                              ")
-         .append("  optional bool active = 1;                        ")
-         .append("}                                                  ");
+    proto.append("message Person {           ")
+         .append("  /*                       ")
+         .append("   * Next Id: 6            ")
+         .append("   */                      ")
+         .append("  optional bool active = 1;")
+         .append("}                          ");
     Protobuf root = xtext.parse(proto);
     Property active = findProperty("active", root);
-    INode node = finder.matchingCommentNode(active, Pattern.compile(".*"));
+    Pair<INode, Matcher> match = finder.matchingCommentNode(active, "NEXT ID: [\\d]+");
+    INode node = match.getFirst();
     assertThat(node, notNullValue());
   }
+
+  @Test public void should_return_null_if_no_matching_node_found() {
+    MultiLineTextBuilder proto = new MultiLineTextBuilder();
+    proto.append("message Person {           ")
+         .append("  // Next Id: 6            ")
+         .append("  optional bool active = 1;")
+         .append("}                          ");
+    Protobuf root = xtext.parse(proto);
+    Property active = findProperty("active", root);
+    Pair<INode, Matcher> match = finder.matchingCommentNode(active, "Hello");
+    assertThat(match, nullValue());
+  }
 }
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder.java
index 475c344..4773067 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/CommentNodesFinder.java
@@ -9,16 +9,20 @@
 package com.google.eclipse.protobuf.ui.commands;
 
 import static com.google.eclipse.protobuf.junit.util.SystemProperties.lineSeparator;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
 import static org.eclipse.xtext.nodemodel.util.NodeModelUtils.getNode;
 import static org.eclipse.xtext.util.Strings.isEmpty;
+import static org.eclipse.xtext.util.Tuples.pair;
 
-import com.google.eclipse.protobuf.util.ModelNodes;
-import com.google.inject.*;
+import java.util.*;
+import java.util.regex.*;
 
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.xtext.nodemodel.*;
+import org.eclipse.xtext.util.*;
 
-import java.util.regex.*;
+import com.google.eclipse.protobuf.util.ModelNodes;
+import com.google.inject.*;
 
 /**
  * @author alruiz@google.com (Alex Ruiz)
@@ -26,9 +30,12 @@
 @Singleton
 class CommentNodesFinder {
 
-  @Inject private ModelNodes nodes;
+  private static final String MATCH_ANYTHING = ".*";
 
-  INode matchingCommentNode(EObject target, Pattern...patternsToMatch) {
+  @Inject private ModelNodes nodes;
+  @Inject private final IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE;
+
+  Pair<INode, Matcher> matchingCommentNode(EObject target, String...patternsToMatch) {
     ICompositeNode node = getNode(target);
     for (INode currentNode : node.getAsTreeIterable()) {
       if (currentNode instanceof ILeafNode && !((ILeafNode) currentNode).isHidden()) break;
@@ -37,13 +44,26 @@
         if (isEmpty(rawComment)) continue;
         String[] comment = rawComment.split(lineSeparator());
         for (String line : comment) {
-          for (Pattern pattern : patternsToMatch) {
+          for (Pattern pattern : compile(patternsToMatch, target)) {
             Matcher matcher = pattern.matcher(line);
-            if (matcher.matches()) return currentNode;
+            if (matcher.matches()) return pair(currentNode, matcher);
           }
         }
       }
     }
     return null;
   }
+
+  private List<Pattern> compile(String[] patterns, EObject target) {
+    List<Pattern> compiled = new ArrayList<Pattern>();
+    for (final String s : patterns) {
+      Pattern p = cache.get(s, target.eResource(), new Provider<Pattern>() {
+        public Pattern get() {
+          return Pattern.compile(MATCH_ANYTHING + s + MATCH_ANYTHING, CASE_INSENSITIVE);
+        }
+      });
+      compiled.add(p);
+    }
+    return compiled;
+  }
 }
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/SmartSemicolonHandler.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/SmartSemicolonHandler.java
index 957a9c5..f57ed44 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/SmartSemicolonHandler.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/SmartSemicolonHandler.java
@@ -8,15 +8,12 @@
  */
 package com.google.eclipse.protobuf.ui.commands;
 
-import static com.google.eclipse.protobuf.grammar.CommonKeyword.SEMICOLON;
-import static com.google.eclipse.protobuf.junit.util.SystemProperties.lineSeparator;
 import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.*;
+import static org.eclipse.xtext.util.Strings.isEmpty;
 
-import com.google.eclipse.protobuf.protobuf.*;
-import com.google.eclipse.protobuf.ui.util.*;
-import com.google.eclipse.protobuf.util.*;
-import com.google.inject.Inject;
+import java.util.regex.Matcher;
 
+import org.apache.log4j.Logger;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.swt.custom.StyledText;
@@ -27,10 +24,14 @@
 import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext;
 import org.eclipse.xtext.ui.editor.contentassist.antlr.ParserBasedContentAssistContextFactory;
 import org.eclipse.xtext.ui.editor.model.IXtextDocument;
+import org.eclipse.xtext.util.Pair;
 import org.eclipse.xtext.util.concurrent.IUnitOfWork;
-import org.eclipse.xtext.validation.IConcreteSyntaxValidator.InvalidConcreteSyntaxException;
 
-import java.util.regex.Pattern;
+import com.google.eclipse.protobuf.grammar.CommonKeyword;
+import com.google.eclipse.protobuf.protobuf.*;
+import com.google.eclipse.protobuf.ui.util.*;
+import com.google.eclipse.protobuf.util.ModelNodes;
+import com.google.inject.Inject;
 
 /**
  * Inserts a semicolon at the end of a line, regardless of the current position of the caret in the editor. If the
@@ -41,13 +42,17 @@
  */
 public class SmartSemicolonHandler extends SmartInsertHandler {
 
+  private static Logger logger = Logger.getLogger(SmartSemicolonHandler.class);
+
   @Inject private CommentNodesFinder commentNodesFinder;
   @Inject private Fields fields;
   @Inject private Literals literals;
   @Inject private ModelNodes nodes;
   @Inject private ParserBasedContentAssistContextFactory contextFactory;
 
-  private final String semicolon = SEMICOLON.toString();
+  private static final String SEMICOLON = CommonKeyword.SEMICOLON.toString();
+
+  private static final ContentToInsert INSERT_SEMICOLON_AT_CURRENT_LOCATION = new ContentToInsert(SEMICOLON, Location.CURRENT);
 
   /** {@inheritDoc} */
   @Override protected void insertContent(XtextEditor editor, StyledText styledText) {
@@ -64,22 +69,23 @@
     styledText.insert(newContent.value);
     styledText.setCaretOffset(offset + newContent.value.length());
   }
-  
+
   private ContentToInsert newContent(final XtextEditor editor, final StyledText styledText, final String line) {
+    if (line.endsWith(SEMICOLON)) return INSERT_SEMICOLON_AT_CURRENT_LOCATION;
+    final IXtextDocument document = editor.getDocument();
+    ContentToInsert contentToInsert = ContentToInsert.NONE;
     try {
-      final IXtextDocument document = editor.getDocument();
-      return document.modify(new IUnitOfWork<ContentToInsert, XtextResource>() {
+      contentToInsert = document.modify(new IUnitOfWork<ContentToInsert, XtextResource>() {
         public ContentToInsert exec(XtextResource state) {
           int offset = styledText.getCaretOffset();
           ContentAssistContext[] context = contextFactory.create(editor.getInternalSourceViewer(), offset, state);
           for (ContentAssistContext c : context) {
-            if (isCommentOrString(c.getCurrentNode())) break;
+            if (isCommentOrString(c.getCurrentNode())) continue;
             EObject model = c.getCurrentModel();
             if (model instanceof FieldOption) {
               FieldOption option = (FieldOption) model;
               model = option.eContainer();
             }
-            if (line.endsWith(semicolon)) break;
             if (model instanceof Literal) {
               Literal literal = (Literal) model;
               ContentToInsert content = newContent(literal);
@@ -101,13 +107,16 @@
               return content;
             }
           }
-          return new ContentToInsert(semicolon, Location.CURRENT);
+          return INSERT_SEMICOLON_AT_CURRENT_LOCATION;
         }
       });
-    } catch (InvalidConcreteSyntaxException e) {}
-    return ContentToInsert.NONE;
+    } catch (Throwable e) {
+      logger.error("Unable to generate tag number", e);
+      return INSERT_SEMICOLON_AT_CURRENT_LOCATION;
+    }
+    return contentToInsert;
   }
-  
+
   private boolean isCommentOrString(INode currentNode) {
     return nodes.wasCreatedByAnyComment(currentNode) || wasCreatedByString(currentNode);
   }
@@ -125,43 +134,45 @@
     INode indexNode = nodes.firstNodeForFeature(literal, LITERAL__INDEX);
     return newContent(indexNode);
   }
-  
+
   private ContentToInsert newContent(Property property) {
     INode indexNode = nodes.firstNodeForFeature(property, FIELD__INDEX);
     return newContent(indexNode);
   }
 
   private ContentToInsert newContent(INode indexNode) {
-    return (indexNode != null) ? new ContentToInsert(semicolon, Location.END) : ContentToInsert.NONE;
+    boolean hasIndex = indexNode != null && !isEmpty(indexNode.getText());
+    return hasIndex ? new ContentToInsert(SEMICOLON, Location.END) : ContentToInsert.NONE;
   }
-  
+
   private void updateIndexInCommentOfParent(EObject o, int index, IXtextDocument document) {
-//    EObject parent = o.eContainer();
-//    if (parent == null) return;
-//    INode node = commentNodesFinder.matchingCommentNode(parent, Pattern.compile("// Next Id: [0-9]"));
-//    if (node == null) {
-//      System.out.println("No matching node");
-//      return;
-//    }
-//    try {
-//      document.replace(node.getOffset(), node.getText().length(), "// Next Id: " + (index + 1) + lineSeparator());
-//    } catch (BadLocationException e) {
-//      e.printStackTrace();
-//    }
+    EObject parent = o.eContainer();
+    if (parent == null) return;
+    String pattern = "Next[\\s]+Id:[\\s]+([\\d])+";
+    Pair<INode, Matcher> match = commentNodesFinder.matchingCommentNode(parent, pattern);
+    if (match == null) return;
+    String originalText = match.getSecond().group();
+    String replacement = originalText.replaceAll("[\\d]+", String.valueOf(index + 1));
+    INode node = match.getFirst();
+    try {
+      document.replace(node.getOffset() + node.getText().indexOf(originalText), originalText.length(), replacement);
+    } catch (BadLocationException e) {
+      logger.error("Unable to update comment tracking next tag number", e);
+    }
   }
-  
+
   private static class ContentToInsert {
     final String value;
     final Location location;
 
     static final ContentToInsert NONE = new ContentToInsert("", Location.NONE);
-    
+
     ContentToInsert(String value, Location location) {
       this.value = value;
       this.location = location;
     }
   }
-  
+
   private static enum Location {
     NONE, CURRENT, END;
   }