Fixed: [ Issue 53 ] Add content-assist for field options
https://code.google.com/p/protobuf-dt/issues/detail?id=53

diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/contentassist/ProtobufProposalProvider.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/contentassist/ProtobufProposalProvider.java
index 0111d0c..f0ca40c 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/contentassist/ProtobufProposalProvider.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/contentassist/ProtobufProposalProvider.java
@@ -13,6 +13,10 @@
 import static com.google.eclipse.protobuf.ui.grammar.CommonKeyword.*;
 import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.*;
 import static java.lang.String.valueOf;
+import static java.util.Collections.emptyList;
+
+import java.util.ArrayList;
+import java.util.List;
 
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.jface.text.contentassist.ICompletionProposal;
@@ -25,8 +29,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.eclipse.protobuf.protobuf.*;
 import com.google.eclipse.protobuf.protobuf.Enum;
-import com.google.eclipse.protobuf.scoping.*;
-import com.google.eclipse.protobuf.ui.grammar.*;
+import com.google.eclipse.protobuf.scoping.Descriptor;
+import com.google.eclipse.protobuf.scoping.DescriptorProvider;
+import com.google.eclipse.protobuf.ui.grammar.CommonKeyword;
 import com.google.eclipse.protobuf.ui.grammar.CompoundElement;
 import com.google.eclipse.protobuf.ui.labeling.Images;
 import com.google.eclipse.protobuf.ui.util.*;
@@ -112,21 +117,6 @@
     }
   }
 
-  private void proposeBooleanValues(ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
-    CommonKeyword[] keywords = { FALSE, TRUE };
-    proposeAndAccept(keywords, context, acceptor);
-  }
-
-  private void proposeAndAccept(CommonKeyword[] keywords, ContentAssistContext context,
-      ICompletionProposalAcceptor acceptor) {
-    for (CommonKeyword keyword : keywords) proposeAndAccept(keyword, context, acceptor);
-  }
-
-  private void proposeAndAccept(CommonKeyword keyword, ContentAssistContext context,
-      ICompletionProposalAcceptor acceptor) {
-    proposeAndAccept(keyword.toString(), context, acceptor);
-  }
-
   @Override public void complete_ID(EObject model, RuleCall ruleCall, ContentAssistContext context,
       ICompletionProposalAcceptor acceptor) {}
 
@@ -138,7 +128,7 @@
       proposeEmptyString(context, acceptor);
       return;
     }
-    if (model instanceof Option || model instanceof Syntax) return;
+    if (model instanceof Option || model instanceof FieldOption || model instanceof Syntax) return;
     for (AbstractElement element : context.getFirstSetGrammarElements()) {
       if (!(element instanceof Assignment)) continue;
       Assignment assignment = (Assignment) element;
@@ -192,7 +182,8 @@
     if (TRUE.hasValue(keyword) || FALSE.hasValue(keyword)) {
       if (!isBoolProposalValid(context)) return true;
     }
-    return false;
+    // remove keyword proposals when current node is "]". At this position we only accept "default" or field options.
+    return context.getCurrentNode().getText().equals(CLOSING_BRACKET.toString());
   }
 
   private void proposeEqualProto2(ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
@@ -203,15 +194,6 @@
     return object instanceof Keyword && keyword.hasValue(((Keyword)object).getValue());
   }
 
-  private boolean isLastWordFromCaretPositionEqualTo(String word, ContentAssistContext context) {
-    StyledText styledText = context.getViewer().getTextWidget();
-    int valueLength = word.length();
-    int start = styledText.getCaretOffset() - valueLength;
-    if (start < 0) return false;
-    String previousWord = styledText.getTextRange(start, valueLength);
-    return word.equals(previousWord);
-  }
-
   private boolean isBoolProposalValid(ContentAssistContext context) {
     EObject model = context.getCurrentModel();
     if (model instanceof Property) return properties.isBool((Property) model);
@@ -301,19 +283,6 @@
     proposeAndAccept(enumType, context, acceptor);
   }
 
-  private void proposeAndAccept(Enum enumType, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
-    Image image = imageHelper.getImage(imageRegistry.imageFor(Literal.class));
-    for (Literal literal : enumType.getLiterals())
-      proposeAndAccept(literal.getName(), image, context, acceptor);
-  }
-
-  private void proposeAndAccept(String proposalText, Image image, ContentAssistContext context,
-      ICompletionProposalAcceptor acceptor) {
-    String s = addSpaceAtBeginning(proposalText, context);
-    ICompletionProposal proposal = createCompletionProposal(s, proposalText, image, context);
-    acceptor.accept(proposal);
-  }
-
   @Override public void completeProperty_Index(EObject model, Assignment assignment, ContentAssistContext context,
       ICompletionProposalAcceptor acceptor) {
     int index = properties.calculateTagNumberOf((Property) model);
@@ -344,22 +313,96 @@
   }
 
   private void proposeAndAccept(String proposalText, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
-    String s = addSpaceAtBeginning(proposalText, context);
-    acceptor.accept(createCompletionProposal(s, context));
-  }
-
-  private String addSpaceAtBeginning(String proposal, ContentAssistContext context) {
-    if (!isLastWordFromCaretPositionEqualTo(SPACE, context)) return SPACE + proposal;
-    return proposal;
+    acceptor.accept(createCompletionProposal(proposalText, context));
   }
 
   @Override protected ICompletionProposal createCompletionProposal(String proposalText, ContentAssistContext context) {
-    String s = addSpaceAtBeginning(proposalText, context);
-    return createCompletionProposal(s, null, defaultImage(), getPriorityHelper().getDefaultPriority(),
+    return createCompletionProposal(proposalText, null, defaultImage(), getPriorityHelper().getDefaultPriority(),
         context.getPrefix(), context);
   }
 
   private Image defaultImage() {
     return imageHelper.getImage(imageRegistry.defaultImage());
   }
+  
+  @Override public void complete_FieldOption(EObject model, RuleCall ruleCall, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    System.out.println("complete_FieldOption");
+  }
+  
+  @Override public void completeFieldOption_Name(EObject model, Assignment assignment, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    Property property = extractPropertyFrom(context);
+    proposeCommonFieldOptions(property, context, acceptor);
+  }
+
+  private void proposeCommonFieldOptions(Property property, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    boolean isPrimitive = properties.isPrimitive(property);
+    List<String> options = existingFieldOptionNames(property);
+    for (Property fieldOption : descriptorProvider.get().fieldOptions()) {
+      String optionName = fieldOption.getName();
+      if (options.contains(optionName) || ("packed".equals(optionName) && !isPrimitive)) continue;
+      String proposalText = optionName + SPACE + EQUAL + SPACE;
+      boolean isBooleanOption = properties.isBool(fieldOption);
+      if (isBooleanOption) proposalText = proposalText + TRUE;
+      ICompletionProposal proposal = createCompletionProposal(proposalText, context);
+      acceptor.accept(proposal);
+    }
+  }
+  
+  private List<String> existingFieldOptionNames(Property property) {
+    List<FieldOption> options = property.getFieldOptions();
+    if (options.isEmpty()) return emptyList();
+    List<String> optionNames = new ArrayList<String>();
+    for (FieldOption option : options) optionNames.add(option.getName());
+    return optionNames;
+  }
+
+  @Override public void completeFieldOption_Value(EObject model, Assignment assignment, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    FieldOption option = (FieldOption) model;
+    Descriptor descriptor = descriptorProvider.get();
+    Property fieldOption = descriptor.lookupFieldOption(option.getName());
+    if (fieldOption == null) return;
+    if (properties.isBool(fieldOption)) {
+      proposeBooleanValues(context, acceptor);
+      return;
+    }
+    if (descriptor.isCTypeOption(option)) {
+      proposeAndAccept(descriptor.cType(), context, acceptor);
+      return;
+    }
+  }
+
+  private void proposeBooleanValues(ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
+    CommonKeyword[] keywords = { FALSE, TRUE };
+    proposeAndAccept(keywords, context, acceptor);
+  }
+
+  private void proposeAndAccept(CommonKeyword[] keywords, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    for (CommonKeyword keyword : keywords) proposeAndAccept(keyword.toString(), context, acceptor);
+  }
+
+  private void proposeAndAccept(Enum enumType, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
+    Image image = imageHelper.getImage(imageRegistry.imageFor(Literal.class));
+    for (Literal literal : enumType.getLiterals())
+      proposeAndAccept(literal.getName(), image, context, acceptor);
+  }
+
+  private void proposeAndAccept(String proposalText, Image image, ContentAssistContext context,
+      ICompletionProposalAcceptor acceptor) {
+    ICompletionProposal proposal = createCompletionProposal(proposalText, proposalText, image, context);
+    acceptor.accept(proposal);
+  }
+
+  private boolean isLastWordFromCaretPositionEqualTo(String word, ContentAssistContext context) {
+    StyledText styledText = context.getViewer().getTextWidget();
+    int valueLength = word.length();
+    int start = styledText.getCaretOffset() - valueLength;
+    if (start < 0) return false;
+    String previousWord = styledText.getTextRange(start, valueLength);
+    return word.equals(previousWord);
+  }
 }
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/outline/ProtobufOutlineTreeProvider.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/outline/ProtobufOutlineTreeProvider.java
index 2c63379..a7c6603 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/outline/ProtobufOutlineTreeProvider.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/outline/ProtobufOutlineTreeProvider.java
@@ -33,6 +33,7 @@
   
   static {
     IGNORED_ELEMENT_TYPES.add(BooleanRef.class);
+    IGNORED_ELEMENT_TYPES.add(FieldOption.class);
     IGNORED_ELEMENT_TYPES.add(MessageReference.class);
   }
   
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/Descriptor.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/Descriptor.java
index e48ed6a..a113563 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/Descriptor.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/Descriptor.java
@@ -8,6 +8,7 @@
  */
 package com.google.eclipse.protobuf.scoping;
 
+import static com.google.eclipse.protobuf.scoping.OptionType.*;
 import static java.util.Collections.unmodifiableCollection;
 import static org.eclipse.emf.common.util.URI.createURI;
 import static org.eclipse.xtext.EcoreUtil2.*;
@@ -16,7 +17,8 @@
 import java.io.*;
 import java.util.*;
 
-import org.eclipse.xtext.parser.*;
+import org.eclipse.xtext.parser.IParseResult;
+import org.eclipse.xtext.parser.IParser;
 import org.eclipse.xtext.resource.XtextResource;
 import org.eclipse.xtext.util.StringInputStream;
 
@@ -33,14 +35,17 @@
 
   private Protobuf root;
 
-  private final Map<String, Property> fileOptions = new LinkedHashMap<String, Property>();
+  private final Map<OptionType, Map<String, Property>> options = new HashMap<OptionType, Map<String, Property>>();
+   
   private Enum optimizedMode;
+  private Enum cType;
 
   /**
    * Creates a new </code>{@link Descriptor}</code>.
    * @param parser the grammar parser.
    */
   @Inject public Descriptor(IParser parser) {
+    addOptionTypes();
     try {
       XtextResource resource = new XtextResource(createURI("descriptor.proto"));
       IParseResult result = parser.parse(new InputStreamReader(globalScopeContents(), "UTF-8"));
@@ -53,6 +58,11 @@
     }
   }
 
+  private void addOptionTypes() {
+    for (OptionType type : OptionType.values())
+      options.put(type, new LinkedHashMap<String, Property>());
+  }
+  
   private static InputStream globalScopeContents() {
     return new StringInputStream(descriptorContents());
   }
@@ -90,35 +100,64 @@
   }
 
   private void initContents() {
-    Message m = fileOptionsMessage();
-    for (MessageElement e : m.getElements()) {
+    for (Message m : getAllContentsOfType(root, Message.class)) {
+      if (isFileOptionsMessage(m)) initFileOptions(m);
+      else if (isFieldOptionsMessage(m)) initFieldOptions(m);
+    }
+  }
+  
+  private boolean isFileOptionsMessage(Message m) {
+    return "FileOptions".equals(m.getName());
+  }
+
+  private boolean isFieldOptionsMessage(Message m) {
+    return "FieldOptions".equals(m.getName());
+  }
+
+  private void initFileOptions(Message fileOptionsMessage) {
+    for (MessageElement e : fileOptionsMessage.getElements()) {
       if (e instanceof Property) {
         addFileOption((Property) e);
         continue;
       }
-      if (isOptimizeModeEnum(e)) {
+      if (isEnumWithName(e, "OptimizeMode")) {
         optimizedMode = (Enum) e;
         continue;
       }
     }
   }
+  
+  private void addFileOption(Property p) {
+    addOption(FILE, p);
+  }
 
-  private boolean isOptimizeModeEnum(MessageElement e) {
+  private void initFieldOptions(Message fieldOptionsMessage) {
+    for (MessageElement e : fieldOptionsMessage.getElements()) {
+      if (e instanceof Property) {
+        addFieldOption((Property) e);
+        continue;
+      }
+      if (isEnumWithName(e, "CType")) {
+        cType = (Enum) e;
+        continue;
+      }
+    }
+  }
+  
+  private void addFieldOption(Property p) {
+    addOption(FIELD, p);
+  }
+  
+  private void addOption(OptionType type, Property p) {
+    options.get(type).put(p.getName(), p);
+  }
+  
+  private boolean isEnumWithName(MessageElement e, String name) {
     if (!(e instanceof Enum)) return false;
     Enum anEnum = (Enum) e;
-    return "OptimizeMode".equals(anEnum.getName());
+    return name.equals(anEnum.getName());
   }
-
-  private Message fileOptionsMessage() {
-    for (Message m : getAllContentsOfType(root, Message.class))
-      if ("FileOptions".equals(m.getName())) return m;
-    throw new IllegalStateException("Unable to find message 'FileOptions'");
-  }
-
-  private void addFileOption(Property p) {
-    fileOptions.put(p.getName(), p);
-  }
-
+  
   /**
    * Returns all the file-level options available. These are the options defined in
    * {@code google/protobuf/descriptor.proto} (more details can be found
@@ -126,7 +165,7 @@
    * @return all the file-level options available.
    */
   public Collection<Property> fileOptions() {
-    return unmodifiableCollection(fileOptions.values());
+    return optionsOfType(FILE);
   }
 
   /**
@@ -139,11 +178,11 @@
   }
 
   /**
-   * Indicates whether the given option is the "OptimizeMode" one (defined in {@code google/protobuf/descriptor.proto}.
+   * Indicates whether the given option is the "optimize_for" one (defined in {@code google/protobuf/descriptor.proto}.
    * More details can be found
    * <a href=http://code.google.com/apis/protocolbuffers/docs/proto.html#options" target="_blank">here</a>.)
    * @param option the given option.
-   * @return {@code true} if the given option is the "OptimizeMode" one, {@code false} otherwise.
+   * @return {@code true} if the given option is the "optimize_for" one, {@code false} otherwise.
    */
   public boolean isOptimizeForOption(Option option) {
     if (option == null) return false;
@@ -158,6 +197,56 @@
    * @return the option whose name matches the given one or {@code null} if a matching option is not found.
    */
   public Property lookupFileOption(String name) {
-    return fileOptions.get(name);
+    return lookupOption(FILE, name);
+  }
+
+  /**
+   * Returns all the field-level options available. These are the options defined in
+   * {@code google/protobuf/descriptor.proto} (more details can be found
+   * <a href=http://code.google.com/apis/protocolbuffers/docs/proto.html#options" target="_blank">here</a>.)
+   * @return all the field-level options available.
+   */
+  public Collection<Property> fieldOptions() {
+    return optionsOfType(FIELD);
+  }
+
+  private Collection<Property> optionsOfType(OptionType type) {
+    return unmodifiableCollection(options.get(type).values());
+  }
+  
+  /**
+   * Looks up a field-level option per name. Field-level options are defined in {@code google/protobuf/descriptor.proto}
+   * (more details can be found <a
+   * href=http://code.google.com/apis/protocolbuffers/docs/proto.html#options" target="_blank">here</a>.)
+   * @param name the name of the option to look for.
+   * @return the option whose name matches the given one or {@code null} if a matching option is not found.
+   */
+  public Property lookupFieldOption(String name) {
+    return lookupOption(FIELD, name);
+  }
+  
+  private Property lookupOption(OptionType type, String name) {
+    return options.get(type).get(name);
+  }
+
+  /**
+   * Indicates whether the given option is the "ctype" one (defined in {@code google/protobuf/descriptor.proto}.
+   * More details can be found
+   * <a href=http://code.google.com/apis/protocolbuffers/docs/proto.html#options" target="_blank">here</a>.)
+   * @param option the given option.
+   * @return {@code true} if the given option is the "ctype" one, {@code false} otherwise.
+   */
+  public boolean isCTypeOption(FieldOption option) {
+    if (option == null) return false;
+    return "ctype".equals(option.getName());
+  }
+  
+  /**
+   * Returns the {@code enum} "CType" (defined in {@code google/protobuf/descriptor.proto}. More details can be
+   * found <a href=http://code.google.com/apis/protocolbuffers/docs/proto.html#options" target="_blank">here</a>.)
+   * @return the {@code enum} "CType."
+   */
+  public Enum cType() {
+    return cType;
   }
 }
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/OptionType.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/OptionType.java
new file mode 100644
index 0000000..ab8ab3a
--- /dev/null
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/OptionType.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2011 Google Inc.
+ *
+ * All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse
+ * Public License v1.0 which accompanies this distribution, and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package com.google.eclipse.protobuf.scoping;
+
+enum OptionType {
+  FILE, FIELD;
+}
\ No newline at end of file
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
index 21bf3b6..f4a09d7 100644
--- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
+++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/scoping/ProtobufScopeProvider.java
@@ -14,10 +14,14 @@
 
 import java.util.*;
 
-import org.eclipse.emf.common.util.*;
-import org.eclipse.emf.ecore.*;
-import org.eclipse.emf.ecore.resource.*;
-import org.eclipse.xtext.naming.*;
+import org.eclipse.emf.common.util.TreeIterator;
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.xtext.naming.IQualifiedNameProvider;
+import org.eclipse.xtext.naming.QualifiedName;
 import org.eclipse.xtext.resource.IEObjectDescription;
 import org.eclipse.xtext.scoping.IScope;
 import org.eclipse.xtext.scoping.impl.*;
@@ -127,6 +131,10 @@
       Enum optimizedMode = descriptor.optimizedMode();
       return scopeForLiterals(optimizedMode);
     }
+    if (container instanceof FieldOption && descriptor.isCTypeOption((FieldOption) container)) {
+      Enum cType = descriptor.cType();
+      return scopeForLiterals(cType);
+    }
     return null;
   }