Changes to SmartSemicolonHandler: - Use text edits rather than a semantic update to insert a tag number, to avoid duplication of surrounding whitespace. - Correctly determine the next tag number within a message containing groups. - Detect and update a next-tag-number comment located at the end of a message. Change-Id: Ie8967f3393ef6f9b2e1fbb0118916f7e82c8f8a0
diff --git a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/junit/core/XtextRule.java b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/junit/core/XtextRule.java index b95bb21..e20c5c6 100644 --- a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/junit/core/XtextRule.java +++ b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/junit/core/XtextRule.java
@@ -113,6 +113,10 @@ return root; } + public String text() { + return resource.getParseResult().getRootNode().getText(); + } + public <T extends EObject> T find(String name, String extra, Class<T> type, SearchOption...options) { return find(name + extra, name.length(), type, options); }
diff --git a/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder_matchingCommentNode_Test.java b/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder_matchingCommentNode_Test.java deleted file mode 100644 index 24bb3d0..0000000 --- a/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder_matchingCommentNode_Test.java +++ /dev/null
@@ -1,79 +0,0 @@ -/* - * 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 org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; - -import static com.google.eclipse.protobuf.junit.core.UnitTestModule.unitTestModule; -import static com.google.eclipse.protobuf.junit.core.XtextRule.overrideRuntimeModuleWith; - -import java.util.regex.Matcher; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.util.Pair; -import org.junit.Rule; -import org.junit.Test; - -import com.google.eclipse.protobuf.junit.core.XtextRule; -import com.google.eclipse.protobuf.protobuf.MessageField; -import com.google.inject.Inject; - -/** - * Tests for <code>{@link CommentNodesFinder#matchingCommentNode(EObject, String...)}</code>. - * - * @author alruiz@google.com (Alex Ruiz) - */ -public class CommentNodesFinder_matchingCommentNode_Test { - @Rule public XtextRule xtext = overrideRuntimeModuleWith(unitTestModule()); - - @Inject private CommentNodesFinder finder; - - // syntax = "proto2"; - // - // message Person { - // // Next Id: 6 - // optional bool active = 1; - // } - @Test public void should_return_matching_single_line_comment_of_element() { - MessageField field = xtext.find("active", MessageField.class); - Pair<INode, Matcher> match = finder.matchingCommentNode(field, "next id: [\\d]+"); - INode node = match.getFirst(); - assertThat(node.getText().trim(), equalTo("// Next Id: 6")); - } - - // syntax = "proto2"; - // - // message Person { - // /* - // * Next Id: 6 - // */ - // optional bool active = 1; - // } - @Test public void should_return_matching_multi_line_comment_of_element() { - MessageField field = xtext.find("active", MessageField.class); - Pair<INode, Matcher> match = finder.matchingCommentNode(field, "NEXT ID: [\\d]+"); - assertNotNull(match.getFirst()); - } - - // syntax = "proto2"; - // - // message Person { - // // Next Id: 6 - // optional bool active = 1; - // } - @Test public void should_return_null_if_no_matching_node_found() { - MessageField active = xtext.find("active", MessageField.class); - Pair<INode, Matcher> match = finder.matchingCommentNode(active, "Hello"); - assertNull(match); - } -}
diff --git a/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandlerTest.java b/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandlerTest.java new file mode 100644 index 0000000..1956883 --- /dev/null +++ b/com.google.eclipse.protobuf.ui.test/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandlerTest.java
@@ -0,0 +1,275 @@ +/* + * 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 org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.custom.StyledTextContent; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import com.google.eclipse.protobuf.junit.core.XtextRule; +import com.google.eclipse.protobuf.protobuf.Group; +import com.google.eclipse.protobuf.protobuf.Literal; +import com.google.eclipse.protobuf.protobuf.MessageField; +import com.google.eclipse.protobuf.ui.plugin.ProtobufEditorPlugIn; +import com.google.inject.Inject; + +public class SmartSemicolonHandlerTest { + @Rule + public XtextRule xtext = XtextRule.createWith(ProtobufEditorPlugIn.injector()); + + @Inject + private SmartSemicolonHandler handler; + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool incomplete + // } + @Test public void shouldDetermineFirstIndexToBe1() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + assertThat(handler.determineNewIndex(incomplete), is(1L)); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool in_message = 2; + // optional group outer_group = 4 { + // optional bool in_outer_group = 5; + // optional group inner_group = 3 { + // optional bool in_inner_group = 1; + // optional bool incomplete + // } + // } + // } + @Test public void shouldDetermineCorrectIndexInsideOfGroups() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + assertThat(handler.determineNewIndex(incomplete), is(6L)); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool in_message = 2; + // optional group outer_group = 4 { + // optional bool in_outer_group = 5; + // optional group inner_group = 3 { + // optional bool in_inner_group = 1; + // } + // } + // optional bool incomplete + // } + @Test public void shouldDetermineCorrectIndexOutsideOfGroups() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + assertThat(handler.determineNewIndex(incomplete), is(6L)); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool in_message = 2; + // message InnerMessage { + // optional bool in_inner_message = 4; + // optional bool incomplete + // } + // } + @Test public void shouldDetermineCorrectIndexInsideOfNestedMessage() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + assertThat(handler.determineNewIndex(incomplete), is(5L)); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool in_message = 2; + // message InnerMessage { + // optional bool in_inner_message = 4; + // } + // optional bool incomplete + // } + @Test public void shouldDetermineCorrectIndexOutsideOfNestedMessage() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + assertThat(handler.determineNewIndex(incomplete), is(3L)); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool incomplete + // } + @Test public void shouldComplete() { + String incompleteFieldName = "incomplete"; + + MessageField incomplete = xtext.find(incompleteFieldName, MessageField.class); + ICompositeNode node = NodeModelUtils.getNode(incomplete); + ReplaceEdit indexEdit = handler.completeWithIndex(node, 1); + + assertThat(indexEdit.getOffset(), + is(xtext.text().indexOf(incompleteFieldName) + incompleteFieldName.length())); + assertThat(indexEdit.getText(), is(" = 1;")); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool incomplete + // = + // } + @Test public void shouldCompleteAfterExistingEquals() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + ICompositeNode node = NodeModelUtils.getNode(incomplete); + ReplaceEdit indexEdit = handler.completeWithIndex(node, 1); + + String equalsAtStartOfLine = " ="; + assertThat(indexEdit.getOffset(), + is(xtext.text().indexOf(equalsAtStartOfLine) + equalsAtStartOfLine.length())); + assertThat(indexEdit.getText(), is(" 1;")); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional bool incomplete [ default = true; ]; + // } + @Test public void shouldCompleteWithoutSemicolonBeforeOptionBracket() { + String incompleteFieldName = "incomplete"; + + MessageField incomplete = xtext.find(incompleteFieldName, MessageField.class); + ICompositeNode node = NodeModelUtils.getNode(incomplete); + ReplaceEdit indexEdit = handler.completeWithIndex(node, 1); + + assertThat(indexEdit.getOffset(), + is(xtext.text().indexOf(incompleteFieldName) + incompleteFieldName.length())); + assertThat(indexEdit.getText(), is(" = 1 ")); + } + + // // ignore errors + // syntax = "proto2"; + // + // message Message { + // optional group incomplete { + // } + // } + @Test public void shouldCompleteWithoutSemicolonBeforeGroupBrace() { + String incompleteGroupName = "incomplete"; + + Group incomplete = xtext.find(incompleteGroupName, Group.class); + ICompositeNode node = NodeModelUtils.getNode(incomplete); + ReplaceEdit indexEdit = handler.completeWithIndex(node, 1); + + assertThat(indexEdit.getOffset(), + is(xtext.text().indexOf(incompleteGroupName) + incompleteGroupName.length())); + assertThat(indexEdit.getText(), is(" = 1 ")); + } + + @Test public void shouldDeleteTrailingWhitespace() { + String trailingWhitespace = " "; + String lineContent = " optional bool foo" + trailingWhitespace; + int lineNumber = 10; + int lineStartOffset = 100; + int insertionOffset = lineStartOffset + lineContent.lastIndexOf(trailingWhitespace); + + StyledTextContent content = Mockito.mock(StyledTextContent.class); + Mockito.when(content.getLineAtOffset(insertionOffset)).thenReturn(lineNumber); + Mockito.when(content.getOffsetAtLine(lineNumber)).thenReturn(lineStartOffset); + Mockito.when(content.getLine(lineNumber)).thenReturn(lineContent); + + TextEdit trailingWhitespaceEdit = handler.deleteTrailingWhitespace(content, insertionOffset); + assertThat(trailingWhitespaceEdit.getOffset(), is(insertionOffset)); + assertThat(trailingWhitespaceEdit.getLength(), is(trailingWhitespace.length())); + } + + // // ignore errors + // syntax = "proto2"; + // + // // Next Id: 2 + // message Message { + // optional bool field = 1; + // optional bool incomplete + // } + @Test public void shouldUpdateNextIndexComment() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + ReplaceEdit commentEdit = handler.updateNextIndexComment(incomplete, 3); + + String pattern = "Next Id: "; + assertThat(commentEdit.getOffset(), is(xtext.text().indexOf(pattern) + pattern.length())); + assertThat(commentEdit.getText(), is("3")); + } + + // // ignore errors + // syntax = "proto2"; + // + // /* + // * Next Id: 2 + // */ + // message Message { + // optional bool field = 1; + // optional bool incomplete + // } + @Test public void shouldUpdateMultilineComment() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + ReplaceEdit commentEdit = handler.updateNextIndexComment(incomplete, 3); + + String pattern = "Next Id: "; + assertThat(commentEdit.getOffset(), is(xtext.text().indexOf(pattern) + pattern.length())); + assertThat(commentEdit.getText(), is("3")); + } + + // // ignore errors + // syntax = "proto2"; + // + // enum Enum { + // ONE = 1; + // TWO = 2; + // THREE = 3; + // FOUR + // // Next Id: 4 + // } + @Test public void shouldUpdateNextIndexCommentAtEndOfEnum() { + Literal incomplete = xtext.find("FOUR", Literal.class); + ReplaceEdit commentEdit = handler.updateNextIndexComment(incomplete, 5); + + String pattern = "Next Id: "; + assertThat(commentEdit.getOffset(), is(xtext.text().indexOf(pattern) + pattern.length())); + assertThat(commentEdit.getText(), is("5")); + } + + // // ignore errors + // syntax = "proto2"; + // + // // My Favorite Number: 200 + // message Message { + // optional bool field = 1; + // optional bool incomplete + // } + @Test public void shouldNotUpdateOtherComment() { + MessageField incomplete = xtext.find("incomplete", MessageField.class); + ReplaceEdit commentEdit = handler.updateNextIndexComment(incomplete, 3); + + assertThat(commentEdit, is((ReplaceEdit) null)); + } +}
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder.java deleted file mode 100644 index 296f715..0000000 --- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/CommentNodesFinder.java +++ /dev/null
@@ -1,98 +0,0 @@ -/* - * 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 com.google.common.cache.CacheBuilder.newBuilder; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.eclipse.protobuf.util.Strings.quote; -import static com.google.eclipse.protobuf.util.SystemProperties.lineSeparator; -import static java.util.regex.Pattern.CASE_INSENSITIVE; -import static org.eclipse.xtext.nodemodel.util.NodeModelUtils.getNode; -import static org.eclipse.xtext.util.Strings.isEmpty; -import static org.eclipse.xtext.util.Tuples.pair; - -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.eclipse.protobuf.model.util.INodes; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import org.apache.log4j.Logger; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.nodemodel.ICompositeNode; -import org.eclipse.xtext.nodemodel.ILeafNode; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.util.Pair; - -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @author alruiz@google.com (Alex Ruiz) - */ -@Singleton class CommentNodesFinder { - private static final String MATCH_ANYTHING = ".*"; - - private static Logger logger = Logger.getLogger(CommentNodesFinder.class); - - @Inject private INodes nodes; - - private final LoadingCache<String, Pattern> patternCache = - newBuilder().maximumSize(20).build(new PatternCacheLoader()); - - Pair<INode, Matcher> matchingCommentNode(EObject target, String... patternsToMatch) { - ICompositeNode node = getNode(target); - for (INode currentNode : node.getAsTreeIterable()) { - if (!nodes.isHiddenLeafNode(currentNode) || !nodes.isComment(currentNode)) { - continue; - } - String rawComment = ((ILeafNode) currentNode).getText(); - if (isEmpty(rawComment)) { - continue; - } - String[] comment = rawComment.split(lineSeparator()); - for (String line : comment) { - for (Pattern pattern : compile(patternsToMatch)) { - Matcher matcher = pattern.matcher(line); - if (matcher.matches()) { - return pair(currentNode, matcher); - } - } - } - } - return null; - } - - private List<Pattern> compile(String[] patterns) { - List<Pattern> compiled = newArrayList(); - for (final String s : patterns) { - Pattern p = null; - try { - p = patternCache.get(s); - } catch (ExecutionException e) { - logger.error("Unable to obtain pattern from cache for " + quote(s), e); - p = PatternCacheLoader.compile(s); - } - compiled.add(p); - } - return compiled; - } - - private static class PatternCacheLoader extends CacheLoader<String, Pattern> { - @Override public Pattern load(String key) throws Exception { - return compile(key); - } - - static Pattern compile(String regex) { - return Pattern.compile(MATCH_ANYTHING + regex + MATCH_ANYTHING, CASE_INSENSITIVE); - } - } -}
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandler.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandler.java index 149425c..005cca6 100644 --- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandler.java +++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/SmartSemicolonHandler.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Google Inc. + * 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 @@ -12,58 +12,68 @@ import static java.util.regex.Pattern.compile; import static org.eclipse.xtext.util.Strings.isEmpty; -import com.google.common.collect.Lists; -import com.google.eclipse.protobuf.grammar.CommonKeyword; +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.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; 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.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.Pair; -import org.eclipse.xtext.util.Tuples; 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.concurrent.atomic.AtomicBoolean; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * 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. + * 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 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; @@ -72,132 +82,240 @@ @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); + @Override protected void insertContent(final XtextEditor editor, final StyledText styledText) { final IXtextDocument document = editor.getDocument(); - final List<Pair<EObject, Long>> commentsToUpdate = Lists.newLinkedList(); - document.readOnly(NULL_UNIT_OF_WORK); // wait for reconciler to finish its work. - try { - /* - * Textual and semantic updates cannot be done in the same IUnitOfWork (throws an - * IllegalStateException), so index updates (semantic) are done first and tracked in the - * commentsToUpdate list, then a 2nd IUnitOfWork processes the comment updates (textual). - */ - document.modify(new IUnitOfWork.Void<XtextResource>() { - @Override public void process(XtextResource resource) { - Protobuf root = resources.rootOf(resource); - if (!protobufs.hasKnownSyntax(root)) { - 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); - commentsToUpdate.add(Tuples.create(model, index)); - shouldInsertSemicolon.set(false); - } - } - if (model instanceof MessageField) { - MessageField field = (MessageField) model; - if (shouldCalculateIndex(field)) { - long index = indexedElements.calculateNewIndexFor(field); - field.setIndex(index); - commentsToUpdate.add(Tuples.create(model, index)); - shouldInsertSemicolon.set(false); - } - } - } - } - }); - - if (!commentsToUpdate.isEmpty()) { - document.modify(new IUnitOfWork.Void<XtextResource>() { - @Override public void process(XtextResource resource) { - for (Pair<EObject, Long> updateInfo : commentsToUpdate) { - updateIndexInCommentOfParent(updateInfo.getFirst(), updateInfo.getSecond(), document); - } - } - }); - } - } 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>() { + document.modify(new IUnitOfWork.Void<XtextResource>() { @Override public void process(XtextResource resource) { - editor.getInternalSourceViewer().invalidateTextPresentation(); + 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; } }
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/StyledTextAccess.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/StyledTextAccess.java deleted file mode 100644 index 2f6bfda..0000000 --- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/commands/semicolon/StyledTextAccess.java +++ /dev/null
@@ -1,46 +0,0 @@ -/* - * Copyright (c) 2012 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 org.eclipse.swt.custom.StyledText; - -/** - * @author alruiz@google.com (Alex Ruiz) - */ -class StyledTextAccess { - private final StyledText styledText; - - StyledTextAccess(StyledText styledText) { - this.styledText = styledText; - } - - String lineAtCaretOffset() { - int offset = caretOffset(); - int lineAtOffset = styledText.getLineAtOffset(offset); - return styledText.getLine(lineAtOffset); - } - - void setCaretOffsetToEndOfLine() { - int offset = caretOffset(); - int lineAtOffset = styledText.getLineAtOffset(offset); - String line = styledText.getLine(lineAtOffset); - int offsetAtLine = styledText.getOffsetAtLine(lineAtOffset); - offset = offsetAtLine + line.length(); - styledText.setCaretOffset(offset); - } - - void insert(String text) { - styledText.insert(text); - styledText.setCaretOffset(caretOffset() + text.length()); - } - - int caretOffset() { - return styledText.getCaretOffset(); - } -}
diff --git a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/IndexedElements.java b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/IndexedElements.java index 6a8208f..3061b1f 100644 --- a/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/IndexedElements.java +++ b/com.google.eclipse.protobuf/src/com/google/eclipse/protobuf/model/util/IndexedElements.java
@@ -10,18 +10,21 @@ import static java.lang.Math.max; import static java.util.Collections.emptyList; - import static org.eclipse.xtext.util.SimpleAttributeResolver.newResolver; import java.util.List; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.util.SimpleAttributeResolver; 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.Message; import com.google.eclipse.protobuf.protobuf.MessageElement; +import com.google.eclipse.protobuf.protobuf.MessageField; import com.google.eclipse.protobuf.protobuf.OneOf; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -53,11 +56,11 @@ * @return the calculated value for the index of the given element. */ public long calculateNewIndexFor(IndexedElement e) { - EObject type = e.eContainer(); - long index = findMaxIndex(type.eContents()); - return ++index; + EObject containingMessage = EcoreUtil2.getContainerOfType(e, Message.class); + long index = findMaxIndex(containingMessage.eContents()); + return index + 1; } - + private long findMaxIndex(Iterable<? extends EObject> elements) { long maxIndex = 0; @@ -66,17 +69,18 @@ maxIndex = max(maxIndex, findMaxIndex(((OneOf) e).getElements())); } else if (e instanceof IndexedElement) { maxIndex = max(maxIndex, indexOf((IndexedElement) e)); + if (e instanceof Group) { + maxIndex = max(maxIndex, findMaxIndex(((Group) e).getElements())); + } } } - + return maxIndex; } /** - * Returns the name of the given <code>{@link IndexedElement}</code>. - * @param e the given {@code IndexedElement}. - * @return the name of the given {@code IndexedElement}, or {@code Long.MIN_VALUE} if the given {@code IndexedElement} - * is {@code null}. + * Returns the index of the given {@link IndexedElement}, or {@code Long.MIN_VALUE} if the given + * {@code IndexedElement} is {@code null}. */ public long indexOf(IndexedElement e) { long index = Long.MIN_VALUE;