| /* |
| * 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.semicolon; |
| |
| import static java.util.regex.Pattern.compile; |
| |
| import static org.eclipse.xtext.util.Strings.isEmpty; |
| |
| import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.LITERAL__INDEX; |
| import static com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.MESSAGE_FIELD__INDEX; |
| |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.log4j.Logger; |
| import org.eclipse.emf.ecore.EAttribute; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.swt.custom.StyledText; |
| 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.ui.editor.preferences.IPreferenceStoreAccess; |
| 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.model.util.INodes; |
| import com.google.eclipse.protobuf.model.util.IndexedElements; |
| import com.google.eclipse.protobuf.model.util.Literals; |
| import com.google.eclipse.protobuf.model.util.Protobufs; |
| import com.google.eclipse.protobuf.model.util.Resources; |
| import com.google.eclipse.protobuf.protobuf.FieldOption; |
| 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.Protobuf; |
| import com.google.eclipse.protobuf.ui.commands.SmartInsertHandler; |
| import com.google.eclipse.protobuf.ui.preferences.editor.numerictag.NumericTagPreferences; |
| 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 field 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 final Pattern NUMBERS_PATTERN = compile("[\\d]+"); |
| |
| private static final IUnitOfWork.Void<XtextResource> NULL_UNIT_OF_WORK = new IUnitOfWork.Void<XtextResource>() { |
| @Override public void process(XtextResource resource) {} |
| }; |
| |
| private static Logger logger = Logger.getLogger(SmartSemicolonHandler.class); |
| |
| @Inject private CommentNodesFinder commentNodesFinder; |
| @Inject private ParserBasedContentAssistContextFactory contextFactory; |
| @Inject private IndexedElements indexedElements; |
| @Inject private Literals literals; |
| @Inject private INodes nodes; |
| @Inject private Protobufs protobufs; |
| @Inject private Resources resources; |
| @Inject private IPreferenceStoreAccess storeAccess; |
| |
| private static final String SEMICOLON = CommonKeyword.SEMICOLON.toString(); |
| |
| @Override protected void insertContent(XtextEditor editor, StyledText styledText) { |
| StyledTextAccess styledTextAccess = new StyledTextAccess(styledText); |
| String line = styledTextAccess.lineAtCaretOffset(); |
| if (line.endsWith(SEMICOLON)) { |
| styledTextAccess.insert(SEMICOLON); |
| return; |
| } |
| insertContent(editor, styledTextAccess); |
| refreshHighlighting(editor); |
| } |
| |
| private void insertContent(final XtextEditor editor, final StyledTextAccess styledTextAccess) { |
| final AtomicBoolean shouldInsertSemicolon = new AtomicBoolean(true); |
| final IXtextDocument document = editor.getDocument(); |
| document.readOnly(NULL_UNIT_OF_WORK); // wait for reconciler to finish its work. |
| try { |
| document.modify(new IUnitOfWork.Void<XtextResource>() { |
| @Override public void process(XtextResource resource) { |
| Protobuf root = resources.rootOf(resource); |
| if (!protobufs.isProto2(root) /*|| !resource.getErrors().isEmpty()*/) { |
| return; |
| } |
| int offset = styledTextAccess.caretOffset(); |
| ContentAssistContext[] context = contextFactory.create(editor.getInternalSourceViewer(), offset, resource); |
| for (ContentAssistContext c : context) { |
| if (nodes.isCommentOrString(c.getCurrentNode())) { |
| continue; |
| } |
| EObject model = modelFrom(c); |
| if (model instanceof FieldOption) { |
| FieldOption option = (FieldOption) model; |
| model = option.eContainer(); |
| } |
| if (model instanceof Literal) { |
| Literal literal = (Literal) model; |
| if (shouldCalculateIndex(literal, LITERAL__INDEX)) { |
| long index = literals.calculateNewIndexOf(literal); |
| literal.setIndex(index); |
| updateIndexInCommentOfParent(literal, index, document); |
| shouldInsertSemicolon.set(false); |
| } |
| } |
| if (model instanceof MessageField) { |
| MessageField field = (MessageField) model; |
| if (shouldCalculateIndex(field)) { |
| long index = indexedElements.calculateNewIndexFor(field); |
| field.setIndex(index); |
| updateIndexInCommentOfParent(field, index, document); |
| shouldInsertSemicolon.set(false); |
| } |
| } |
| } |
| } |
| }); |
| } catch (Throwable t) { |
| shouldInsertSemicolon.set(true); |
| logger.error("Unable to generate tag number", t); |
| } |
| if (shouldInsertSemicolon.get()) { |
| styledTextAccess.insert(SEMICOLON); |
| } |
| } |
| |
| private boolean shouldCalculateIndex(EObject target, EAttribute indexAttribute) { |
| INode node = nodes.firstNodeForFeature(target, indexAttribute); |
| return node == null || isEmpty(node.getText()); |
| } |
| |
| private boolean shouldCalculateIndex(IndexedElement target) { |
| return indexedElements.indexOf(target) <= 0; |
| } |
| |
| private EObject modelFrom(ContentAssistContext c) { |
| EObject current = c.getCurrentModel(); |
| boolean isIndexed = current instanceof MessageField || current instanceof Literal; |
| return (isIndexed) ? current : c.getPreviousModel(); |
| } |
| |
| private void updateIndexInCommentOfParent(EObject target, long index, IXtextDocument document) { |
| EObject parent = target.eContainer(); |
| if (parent == null) { |
| return; |
| } |
| NumericTagPreferences preferences = new NumericTagPreferences(storeAccess); |
| for (String pattern : preferences.patterns()) { |
| Pair<INode, Matcher> match = commentNodesFinder.matchingCommentNode(parent, pattern); |
| if (match == null) { |
| return; |
| } |
| String original = match.getSecond().group(); |
| String replacement = NUMBERS_PATTERN.matcher(original).replaceFirst(String.valueOf(index + 1)); |
| INode node = match.getFirst(); |
| int offset = node.getTotalOffset() + node.getText().indexOf(original); |
| try { |
| document.replace(offset, original.length(), replacement); |
| } catch (BadLocationException e) { |
| String format = "Unable to update comment tracking next tag number using pattern '%s'"; |
| logger.error(String.format(format, pattern), e); |
| } |
| } |
| } |
| |
| private void refreshHighlighting(final XtextEditor editor) { |
| editor.getDocument().readOnly(new IUnitOfWork.Void<XtextResource>() { |
| @Override public void process(XtextResource resource) { |
| editor.getInternalSourceViewer().invalidateTextPresentation(); |
| } |
| }); |
| } |
| } |