/* * 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.ui.contentassist; import static com.google.eclipse.protobuf.grammar.CommonKeyword.CLOSING_BRACKET; import static com.google.eclipse.protobuf.grammar.CommonKeyword.EQUAL; import static com.google.eclipse.protobuf.grammar.CommonKeyword.FALSE; import static com.google.eclipse.protobuf.grammar.CommonKeyword.NAN; import static com.google.eclipse.protobuf.grammar.CommonKeyword.OPENING_BRACKET; import static com.google.eclipse.protobuf.grammar.CommonKeyword.OPENING_CURLY_BRACKET; import static com.google.eclipse.protobuf.grammar.CommonKeyword.SYNTAX; import static com.google.eclipse.protobuf.grammar.CommonKeyword.TRUE; import static com.google.eclipse.protobuf.protobuf.ModifierEnum.OPTIONAL; import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.LITERAL; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.DEFAULT_EQUAL_IN_BRACKETS; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.DEFAULT_EQUAL_STRING_IN_BRACKETS; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.EMPTY_STRING; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.EQUAL_PROTO2_IN_QUOTES; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.EQUAL_PROTO3_IN_QUOTES; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.PROTO2_IN_QUOTES; import static com.google.eclipse.protobuf.ui.grammar.CompoundElement.PROTO3_IN_QUOTES; import static com.google.eclipse.protobuf.util.CommonWords.space; import static java.lang.String.valueOf; import static org.eclipse.xtext.EcoreUtil2.getAllContentsOfType; import static org.eclipse.xtext.util.Strings.toFirstLower; import com.google.eclipse.protobuf.grammar.CommonKeyword; import com.google.eclipse.protobuf.model.util.IndexedElements; import com.google.eclipse.protobuf.model.util.Literals; import com.google.eclipse.protobuf.model.util.MessageFields; import com.google.eclipse.protobuf.model.util.Options; import com.google.eclipse.protobuf.protobuf.AbstractOption; import com.google.eclipse.protobuf.protobuf.ComplexValue; import com.google.eclipse.protobuf.protobuf.CustomFieldOption; import com.google.eclipse.protobuf.protobuf.CustomOption; import com.google.eclipse.protobuf.protobuf.DefaultValueFieldOption; import com.google.eclipse.protobuf.protobuf.Enum; import com.google.eclipse.protobuf.protobuf.FieldName; import com.google.eclipse.protobuf.protobuf.IndexedElement; import com.google.eclipse.protobuf.protobuf.Literal; import com.google.eclipse.protobuf.protobuf.MessageField; import com.google.eclipse.protobuf.protobuf.ModifierEnum; import com.google.eclipse.protobuf.protobuf.Option; import com.google.eclipse.protobuf.protobuf.SimpleValueField; import com.google.eclipse.protobuf.ui.grammar.CompoundElement; import com.google.eclipse.protobuf.ui.labeling.Images; import com.google.inject.Inject; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.viewers.StyledString; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.graphics.Image; import org.eclipse.xtext.Assignment; import org.eclipse.xtext.Keyword; import org.eclipse.xtext.RuleCall; import org.eclipse.xtext.ui.PluginImageHelper; import org.eclipse.xtext.ui.editor.contentassist.ConfigurableCompletionProposal; import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext; import org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor; /** * @author alruiz@google.com (Alex Ruiz) * * @see <a href="http://www.eclipse.org/Xtext/documentation/310_eclipse_support.html#content-assist">Xtext Content Assist</a> */ public class ProtobufProposalProvider extends AbstractProtobufProposalProvider { @Inject private Images images; @Inject private IndexedElements indexedElements; @Inject private PluginImageHelper imageHelper; @Inject private Literals literals; @Inject private MessageFields messageFields; @Inject private Options options; @Override public void completeProtobuf_Syntax(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {} @Override public void completeSyntax_Name(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { proposeAndAccept(PROTO2_IN_QUOTES, context, acceptor); proposeAndAccept(PROTO3_IN_QUOTES, context, acceptor); } @Override public void complete_Syntax(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { String proposal = SYNTAX + space() + EQUAL_PROTO2_IN_QUOTES; proposeAndAccept(proposal, imageHelper.getImage(images.imageFor(SYNTAX)), context, acceptor); proposal = SYNTAX + space() + EQUAL_PROTO3_IN_QUOTES; proposeAndAccept(proposal, imageHelper.getImage(images.imageFor(SYNTAX)), context, acceptor); } @Override public void complete_ID(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {} @Override public void complete_CHUNK(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {} @Override public void completeKeyword(Keyword keyword, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { if (keyword == null) { return; } boolean proposalWasHandledAlready = completeKeyword(keyword.getValue(), context, acceptor); if (proposalWasHandledAlready) { return; } super.completeKeyword(keyword, context, acceptor); } private boolean completeKeyword(String keyword, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { if (isLastWordFromCaretPositionEqualTo(keyword, context)) { return true; } if (EQUAL.hasValue(keyword)) { EObject grammarElement = context.getLastCompleteNode().getGrammarElement(); if (isKeyword(grammarElement, SYNTAX)) { proposeEqualProto2(context, acceptor); proposeEqualProto3(context, acceptor); } return true; } if (OPENING_BRACKET.hasValue(keyword)) { return proposeOpeningBracket(context, acceptor); } if (OPENING_CURLY_BRACKET.hasValue(keyword)) { EObject model = context.getCurrentModel(); return model instanceof Option || model instanceof ComplexValue; } if (TRUE.hasValue(keyword) || FALSE.hasValue(keyword)) { if (isBoolProposalValid(context)) { proposeBooleanValues(context, acceptor); } return true; } if (NAN.hasValue(keyword)) { if (isNanProposalValid(context)) { proposeAndAccept(keyword.toString(), context, acceptor); } return true; } // 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 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 isKeyword(EObject object, CommonKeyword keyword) { return object instanceof Keyword && keyword.hasValue(((Keyword) object).getValue()); } private void proposeEqualProto2(ContentAssistContext context, ICompletionProposalAcceptor acceptor) { proposeAndAccept(EQUAL_PROTO2_IN_QUOTES, context, acceptor); } private void proposeEqualProto3(ContentAssistContext context, ICompletionProposalAcceptor acceptor) { proposeAndAccept(EQUAL_PROTO3_IN_QUOTES, context, acceptor); } private void proposeAndAccept(CompoundElement proposalText, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { proposeAndAccept(proposalText.toString(), context, acceptor); } private boolean isBoolProposalValid(ContentAssistContext context) { MessageField field = fieldFrom(context); return field != null && messageFields.isBool(field); } private boolean isNanProposalValid(ContentAssistContext context) { MessageField field = fieldFrom(context); return field != null && messageFields.isFloatingPointNumber(field); } private MessageField fieldFrom(ContentAssistContext context) { EObject model = context.getCurrentModel(); if (model instanceof MessageField) { return (MessageField) model; } if (model instanceof AbstractOption) { AbstractOption option = (AbstractOption) model; IndexedElement source = options.rootSourceOf(option); if (source instanceof MessageField) { return (MessageField) source; } } return null; } private boolean proposeOpeningBracket(ContentAssistContext context, ICompletionProposalAcceptor acceptor) { EObject model = context.getCurrentModel(); if (model instanceof ComplexValue) { return true; } if (model instanceof MessageField) { MessageField field = (MessageField) model; ModifierEnum modifier = field.getModifier(); if (OPTIONAL.equals(modifier)) { CompoundElement display = DEFAULT_EQUAL_IN_BRACKETS; int cursorPosition = display.indexOf(CLOSING_BRACKET); if (messageFields.isString(field)) { display = DEFAULT_EQUAL_STRING_IN_BRACKETS; cursorPosition++; } createAndAccept(display, cursorPosition, context, acceptor); } return true; } return false; } private <T> T extractElementFromContext(ContentAssistContext context, Class<T> type) { EObject model = context.getCurrentModel(); // this is most likely a bug in Xtext: if (!type.isInstance(model)) { model = context.getPreviousModel(); } if (!type.isInstance(model)) { return null; } return type.cast(model); } private ICompletionProposal createCompletionProposal(CompoundElement proposal, ContentAssistContext context) { return createCompletionProposal(proposal.toString(), context); } @Override public void completeLiteral_Index(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { long index = literals.calculateNewIndexOf((Literal) model); proposeIndex(index, context, acceptor); } @Override public void completeLiteralLink_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { MessageField field = null; if (model instanceof DefaultValueFieldOption) { field = (MessageField) model.eContainer(); } if (field == null || !messageFields.isOptional(field)) { return; } Enum enumType = messageFields.enumTypeOf(field); if (enumType != null) { proposeAndAccept(enumType, context, acceptor); } } @Override public void completeMessageField_Index(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { long index = indexedElements.calculateNewIndexFor((MessageField) model); proposeIndex(index, context, acceptor); } private void proposeIndex(long index, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { proposeAndAccept(valueOf(index), context, acceptor); } @Override public void completeMessageField_Name(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { String typeName = toFirstLower(messageFields.typeNameOf((MessageField) model)); int index = 1; String name = typeName + index; for (EObject o : model.eContainer().eContents()) { if (o == model || !(o instanceof MessageField)) { continue; } MessageField field = (MessageField) o; if (!name.equals(field.getName())) { continue; } name = typeName + (++index); } proposeAndAccept(name, context, acceptor); } private void proposeAndAccept(String proposalText, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { acceptor.accept(createCompletionProposal(proposalText, context)); } @Override protected ICompletionProposal createCompletionProposal(String proposalText, ContentAssistContext context) { return createCompletionProposal(proposalText, null, defaultImage(), getPriorityHelper().getDefaultPriority(), context.getPrefix(), context); } private Image defaultImage() { return imageHelper.getImage(images.defaultImage()); } @Override public void completeDefaultValueFieldOption_Value(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { MessageField field = null; if (model instanceof DefaultValueFieldOption) { field = (MessageField) model.eContainer(); } if (model instanceof MessageField) { field = (MessageField) model; } if (field == null || !messageFields.isOptional(field)) { return; } proposeFieldValue(field, context, acceptor); } private boolean proposePrimitiveValues(MessageField field, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { if (messageFields.isBool(field)) { proposeBooleanValues(context, acceptor); return true; } if (messageFields.isString(field)) { proposeEmptyString(context, acceptor); return true; } return false; } 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 proposeEmptyString(ContentAssistContext context, ICompletionProposalAcceptor acceptor) { createAndAccept(EMPTY_STRING, 1, context, acceptor); } private void createAndAccept(CompoundElement display, int cursorPosition, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { ICompletionProposal proposal = createCompletionProposal(display, context); if (proposal instanceof ConfigurableCompletionProposal) { ConfigurableCompletionProposal configurable = (ConfigurableCompletionProposal) proposal; configurable.setCursorPosition(cursorPosition); } acceptor.accept(proposal); } @Override public void completeOptionSource_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { } @Override public void completeMessageOptionField_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { } @Override public void completeExtensionOptionField_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { } @Override public void complete_MessageOptionField(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {} @Override public void complete_ExtensionOptionField(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {} @Override public void completeCustomOption_Value(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { if (model instanceof CustomOption) { CustomOption option = (CustomOption) model; IndexedElement e = options.sourceOfLastFieldIn(option); if (e == null) { e = options.rootSourceOf(option); } if (e instanceof MessageField) { proposeFieldValue((MessageField) e, context, acceptor); } } } @Override public void completeCustomFieldOption_Value(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { // TODO content assist returns "{" if (model instanceof CustomFieldOption) { // TODO check if this is the same as sourceOf CustomFieldOption option = (CustomFieldOption) model; IndexedElement e = options.sourceOfLastFieldIn(option); if (e == null) { e = options.rootSourceOf(option); } if (e instanceof MessageField) { proposeFieldValue((MessageField) e, context, acceptor); } } } private void proposeFieldValue(MessageField field, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { if (field == null || proposePrimitiveValues(field, context, acceptor)) { return; } Enum enumType = messageFields.enumTypeOf(field); if (enumType != null) { proposeAndAccept(enumType, context, acceptor); } } private void proposeAndAccept(Enum enumType, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { Image image = imageHelper.getImage(images.imageFor(LITERAL)); for (Literal literal : getAllContentsOfType(enumType, Literal.class)) { proposeAndAccept(literal.getName(), image, context, acceptor); } } @Override public void completeNormalFieldName_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { } @Override public void completeExtensionFieldName_Target(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { } @Override public void complete_SimpleValueLink(EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { SimpleValueField field = extractElementFromContext(context, SimpleValueField.class); if (field != null) { FieldName name = field.getName(); if (name != null) { IndexedElement target = name.getTarget(); if (target instanceof MessageField) { proposeFieldValue((MessageField) target, context, acceptor); } } } } private void proposeAndAccept(String proposalText, Image image, ContentAssistContext context, ICompletionProposalAcceptor acceptor) { ICompletionProposal proposal = createCompletionProposal(proposalText, proposalText, image, context); acceptor.accept(proposal); } @Override public ICompletionProposal createCompletionProposal(String proposal, String displayString, Image image, ContentAssistContext contentAssistContext) { StyledString styled = null; if (displayString != null) { styled = new StyledString(displayString); } int priority = getPriorityHelper().getDefaultPriority(); String prefix = contentAssistContext.getPrefix(); return createCompletionProposal(proposal, styled, image, priority, prefix, contentAssistContext); } }