| /* |
| * 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.commands; |
| |
| import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.*; |
| import static org.eclipse.xtext.util.Strings.isEmpty; |
| |
| 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; |
| import org.eclipse.xtext.*; |
| import org.eclipse.xtext.nodemodel.INode; |
| import org.eclipse.xtext.resource.XtextResource; |
| import org.eclipse.xtext.ui.editor.XtextEditor; |
| 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 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 |
| * line of code being edited is a property or enum literal and if it does not have an index yet, this handler will |
| * insert an index with a proper value as well. |
| * |
| * @author alruiz@google.com (Alex Ruiz) |
| */ |
| 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 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) { |
| int offset = styledText.getCaretOffset(); |
| int lineAtOffset = styledText.getLineAtOffset(offset); |
| int offsetAtLine = styledText.getOffsetAtLine(lineAtOffset); |
| String line = styledText.getLine(lineAtOffset); |
| ContentToInsert newContent = newContent(editor, styledText, line); |
| if (newContent.equals(ContentToInsert.NONE)) return; |
| if (newContent.location.equals(Location.END)) { |
| offset = offsetAtLine + line.length(); |
| styledText.setCaretOffset(offset); |
| } |
| 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 { |
| 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())) continue; |
| EObject model = c.getCurrentModel(); |
| if (model instanceof FieldOption) { |
| FieldOption option = (FieldOption) model; |
| model = option.eContainer(); |
| } |
| if (model instanceof Literal) { |
| Literal literal = (Literal) model; |
| ContentToInsert content = newContent(literal); |
| if (content.equals(ContentToInsert.NONE)) { |
| int index = literals.calculateIndexOf(literal); |
| literal.setIndex(index); |
| updateIndexInCommentOfParent(literal, index, document); |
| } |
| return content; |
| } |
| if (model instanceof Property) { |
| Property property = (Property) model; |
| ContentToInsert content = newContent(property); |
| if (content.equals(ContentToInsert.NONE)) { |
| int index = fields.calculateTagNumberOf(property); |
| property.setIndex(index); |
| updateIndexInCommentOfParent(property, index, document); |
| } |
| return content; |
| } |
| } |
| return INSERT_SEMICOLON_AT_CURRENT_LOCATION; |
| } |
| }); |
| } 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); |
| } |
| |
| private boolean wasCreatedByString(INode node) { |
| EObject grammarElement = node.getGrammarElement(); |
| if (!(grammarElement instanceof RuleCall)) return false; |
| AbstractRule rule = ((RuleCall) grammarElement).getRule(); |
| if (!(rule instanceof TerminalRule)) return false; |
| TerminalRule terminalRule = (TerminalRule) rule; |
| return "STRING".equals(terminalRule.getName()); |
| } |
| |
| private ContentToInsert newContent(Literal literal) { |
| 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) { |
| 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; |
| 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; |
| } |
| } |