/* * Copyright (c) 2015 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 com.google.eclipse.protobuf.protobuf.ProtobufPackage.Literals.LITERAL__INDEX; import static java.util.regex.Pattern.compile; import static org.eclipse.xtext.util.Strings.isEmpty; import com.google.common.annotations.VisibleForTesting; 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.Enum; import com.google.eclipse.protobuf.protobuf.FieldOption; import com.google.eclipse.protobuf.protobuf.Group; import com.google.eclipse.protobuf.protobuf.IndexedElement; import com.google.eclipse.protobuf.protobuf.Literal; import com.google.eclipse.protobuf.protobuf.Message; import com.google.eclipse.protobuf.protobuf.MessageElement; import com.google.eclipse.protobuf.protobuf.MessageField; import com.google.eclipse.protobuf.ui.commands.SmartInsertHandler; import com.google.eclipse.protobuf.ui.preferences.editor.numerictag.NumericTagPreferences; import com.google.inject.Inject; import org.apache.log4j.Logger; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.StyledTextContent; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.RuleCall; import org.eclipse.xtext.nodemodel.ILeafNode; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; 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.concurrent.IUnitOfWork; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Handles a semicolon keypress either by completing the element at the cursor position with a tag * number if that element is a message field, group, or enum literal lacking a tag number, or * otherwise by inserting a semicolon at the cursor position. */ public class SmartSemicolonHandler extends SmartInsertHandler { private static final String SEMICOLON = ";"; private static final Pattern NUMBERS_PATTERN = compile("[\\d]+"); private static Logger logger = Logger.getLogger(SmartSemicolonHandler.class); @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; @Override protected void insertContent(final XtextEditor editor, final StyledText styledText) { final IXtextDocument document = editor.getDocument(); document.modify(new IUnitOfWork.Void<XtextResource>() { @Override public void process(XtextResource resource) { if (!protobufs.hasKnownSyntax(resources.rootOf(resource))) { return; } EObject completableElement = findCompletableElement(editor, styledText.getCaretOffset(), resource); long newIndex = determineNewIndex(completableElement); if (newIndex != -1) { final TextEdit edit = new MultiTextEdit(); TextEdit indexEdit = completeWithIndex(NodeModelUtils.getNode(completableElement), newIndex); if (indexEdit != null) { edit.addChild(indexEdit); TextEdit trailingWhitespaceEdit = deleteTrailingWhitespace(styledText.getContent(), indexEdit.getOffset()); if (trailingWhitespaceEdit != null) { edit.addChild(trailingWhitespaceEdit); } long newNextIndex = newIndex + 1; TextEdit commentEdit = updateNextIndexComment(completableElement, newNextIndex); if (commentEdit != null) { edit.addChild(commentEdit); } try { edit.apply(document); // Move the cursor to the end of the inserted completion text. styledText.setCaretOffset(indexEdit.getExclusiveEnd()); } catch (BadLocationException e) { logger.error("Failed to complete element with new tag number", e); } } } else { styledText.insert(SEMICOLON); styledText.setCaretOffset(styledText.getCaretOffset() + SEMICOLON.length()); } } }); // Refresh syntax highlighting etc. editor.getInternalSourceViewer().invalidateTextPresentation(); } private EObject findCompletableElement(XtextEditor editor, int offset, XtextResource resource) { ContentAssistContext[] contexts = contextFactory.create(editor.getInternalSourceViewer(), offset, resource); for (ContentAssistContext context : contexts) { if (nodes.isCommentOrString(context.getCurrentNode())) { continue; } for (EObject model : Arrays.asList(context.getCurrentModel(), context.getPreviousModel())) { if (model instanceof FieldOption) { model = model.eContainer(); } if (model instanceof MessageField || model instanceof Group || model instanceof Literal) { return model; } } } return null; } @VisibleForTesting long determineNewIndex(EObject model) { if (model instanceof IndexedElement) { IndexedElement indexedElement = (IndexedElement) model; if (indexedElements.indexOf(indexedElement) <= 0) { return indexedElements.calculateNewIndexFor(indexedElement); } } else if (model instanceof Literal) { Literal literal = (Literal) model; INode node = nodes.firstNodeForFeature(literal, LITERAL__INDEX); if (node == null || isEmpty(node.getText())) { return literals.calculateNewIndexOf(literal); } } return -1; } @VisibleForTesting ReplaceEdit completeWithIndex(INode elementNode, long newIndex) { INode nameNode = null; INode equalsNode = null; INode optionsBracketNode = null; INode groupBraceNode = null; for (INode leafNode : elementNode.getAsTreeIterable()) { if (leafNode.getGrammarElement() instanceof RuleCall && ((RuleCall) leafNode.getGrammarElement()).getRule().getName().equals("ID")) { nameNode = leafNode; } else { String text = leafNode.getText(); if (text.equals("=")) { equalsNode = leafNode; } else if (text.equals("[")) { optionsBracketNode = leafNode; } else if (text.equals("{")) { groupBraceNode = leafNode; } } } if (nameNode == null) { return null; } StringBuilder replacement = new StringBuilder(); int start; if (equalsNode != null) { start = equalsNode.getTotalEndOffset(); } else { start = nameNode.getTotalEndOffset(); replacement.append(" ="); } replacement.append(" "); replacement.append(newIndex); int end; if (optionsBracketNode != null) { end = optionsBracketNode.getTotalOffset(); replacement.append(" "); } else if (groupBraceNode != null) { end = groupBraceNode.getTotalOffset(); replacement.append(" "); } else { end = elementNode.getTotalEndOffset(); if (elementNode.getGrammarElement() instanceof RuleCall && ((RuleCall) elementNode.getGrammarElement()).getRule().getName().equals("Group")) { // Insert a space after the index of a new group // so that the user can easily continue typing { or [. replacement.append(" "); } else { replacement.append(SEMICOLON); } } return new ReplaceEdit(start, end - start, replacement.toString()); } @VisibleForTesting TextEdit deleteTrailingWhitespace(StyledTextContent content, int offset) { int lineAtOffset = content.getLineAtOffset(offset); int offsetWithinLine = offset - content.getOffsetAtLine(lineAtOffset); String lineText = content.getLine(lineAtOffset); String trailingText = lineText.substring(offsetWithinLine); int trailingTextLength = trailingText.length(); if (trailingText.trim().length() == 0) { return new DeleteEdit(offset, trailingTextLength); } return null; } @VisibleForTesting ReplaceEdit updateNextIndexComment( EObject completedElement, long newNextIndex) { Class<? extends EObject> containingClass = completedElement instanceof IndexedElement ? Message.class : Enum.class; EObject containingElement = EcoreUtil2.getContainerOfType(completedElement, containingClass); Iterable<ILeafNode> topLevelCommentNodes = findTopLevelCommentNodes(containingElement); Collection<Pattern> patterns = compileIndexCommentPatterns(); IRegion indexLocation = findNextIndexInComments(topLevelCommentNodes, patterns); if (indexLocation != null) { return new ReplaceEdit( indexLocation.getOffset(), indexLocation.getLength(), String.valueOf(newNextIndex)); } return null; } private Iterable<ILeafNode> findTopLevelCommentNodes(EObject containingElement) { Set<ILeafNode> nestedLeafNodes = new HashSet<>(); if (containingElement instanceof Message) { Collection<MessageElement> nestedContainers = new ArrayList<>(); nestedContainers.addAll(EcoreUtil2.getAllContentsOfType(containingElement, Message.class)); nestedContainers.addAll(EcoreUtil2.getAllContentsOfType(containingElement, Enum.class)); for (MessageElement nestedContainer : nestedContainers) { for (ILeafNode nestedLeafNode : NodeModelUtils.getNode(nestedContainer).getLeafNodes()) { nestedLeafNodes.add(nestedLeafNode); } } } Collection<ILeafNode> topLevelCommentNodes = new ArrayList<>(); for (ILeafNode leafNode : NodeModelUtils.getNode(containingElement).getLeafNodes()) { if (!nestedLeafNodes.contains(leafNode) && nodes.isComment(leafNode)) { topLevelCommentNodes.add(leafNode); } } return topLevelCommentNodes; } private Collection<Pattern> compileIndexCommentPatterns() { List<String> regexes = new NumericTagPreferences(storeAccess).patterns(); Collection<Pattern> patterns = new ArrayList<>(regexes.size()); for (String regex : regexes) { patterns.add(Pattern.compile(regex)); } return patterns; } private IRegion findNextIndexInComments( Iterable<ILeafNode> commentNodes, Collection<Pattern> patterns) { for (ILeafNode commentNode : commentNodes) { for (Pattern pattern : patterns) { Matcher patternMatcher = pattern.matcher(commentNode.getText()); if (patternMatcher.find()) { Matcher numberMatcher = NUMBERS_PATTERN.matcher(patternMatcher.group()); if (numberMatcher.find()) { int matchStartPosition = commentNode.getTotalOffset() + patternMatcher.start() + numberMatcher.start(); return new Region(matchStartPosition, numberMatcher.end() - numberMatcher.start()); } } } } return null; } }