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;