blob: f57ed4481dafb6a46f521eeea5ae48a86be5c366 [file] [log] [blame]
/*
* 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;
}
}