Bug 525590 - apply additional text edits of completion items

Change-Id: I289881e711f04fcc2891018032f1083b8f453e76
Signed-off-by: Martin Lippert <mlippert@gmail.com>
diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/IncompleteCompletionTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/IncompleteCompletionTest.java
index 6ac01c2..0417235 100644
--- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/IncompleteCompletionTest.java
+++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/IncompleteCompletionTest.java
@@ -185,6 +185,68 @@
 	}
 
 	@Test
+	public void testCompletionWithAdditionalEdits() throws CoreException, InvocationTargetException {
+		List<CompletionItem> items = new ArrayList<>();
+		CompletionItem item = new CompletionItem("additionaEditsCompletion");
+		item.setKind(CompletionItemKind.Function);
+		item.setInsertText("MainInsertText");
+
+		List<TextEdit> additionalTextEdits = new ArrayList<>();
+
+		TextEdit additionaEdit1 = new TextEdit(new Range(new Position(0, 6), new Position(0, 6)), "addOnText1");
+		TextEdit additionaEdit2 = new TextEdit(new Range(new Position(0, 12), new Position(0, 12)), "addOnText2");
+		additionalTextEdits.add(additionaEdit1);
+		additionalTextEdits.add(additionaEdit2);
+
+		item.setAdditionalTextEdits(additionalTextEdits);
+		items.add(item);
+		MockLanguageSever.INSTANCE.setCompletionList(new CompletionList(true, items));
+
+		String content = "this <> is <> the main <> content of the file";
+		IFile testFile = TestUtils.createUniqueTestFile(project, content);
+		ITextViewer viewer = TestUtils.openTextViewer(testFile);
+
+		ICompletionProposal[] proposals = contentAssistProcessor.computeCompletionProposals(viewer, 24);
+		assertEquals(items.size(), proposals.length);
+		// TODO compare both structures
+		LSIncompleteCompletionProposal LSIncompleteCompletionProposal = (LSIncompleteCompletionProposal) proposals[0];
+		LSIncompleteCompletionProposal.apply(viewer.getDocument());
+
+		String newContent = viewer.getDocument().get();
+		assertEquals("this <addOnText1> is <addOnText2> the main <MainInsertText> content of the file", newContent);
+	}
+
+	@Test
+	public void testSnippetCompletionWithAdditionalEdits()
+			throws PartInitException, InvocationTargetException, CoreException {
+		CompletionItem item = new CompletionItem("snippet item");
+		item.setInsertText("$1 and ${2:foo}");
+		item.setKind(CompletionItemKind.Class);
+		item.setInsertTextFormat(InsertTextFormat.Snippet);
+		List<TextEdit> additionalTextEdits = new ArrayList<>();
+
+		TextEdit additionaEdit1 = new TextEdit(new Range(new Position(0, 6), new Position(0, 6)), "addOnText1");
+		TextEdit additionaEdit2 = new TextEdit(new Range(new Position(0, 12), new Position(0, 12)), "addOnText2");
+		additionalTextEdits.add(additionaEdit1);
+		additionalTextEdits.add(additionaEdit2);
+
+		item.setAdditionalTextEdits(additionalTextEdits);
+
+		MockLanguageSever.INSTANCE.setCompletionList(new CompletionList(true, Collections.singletonList(item)));
+
+		String content = "this <> is <> the main <> content of the file";
+		ITextViewer viewer = TestUtils.openTextViewer(TestUtils.createUniqueTestFile(project, content));
+
+		ICompletionProposal[] proposals = contentAssistProcessor.computeCompletionProposals(viewer, 24);
+		assertEquals(1, proposals.length);
+		((LSIncompleteCompletionProposal) proposals[0]).apply(viewer.getDocument());
+
+		String newContent = viewer.getDocument().get();
+		assertEquals("this <addOnText1> is <addOnText2> the main < and foo> content of the file", newContent);
+		// TODO check link edit groups
+	}
+
+	@Test
 	public void testApplyCompletionWithPrefix() throws CoreException, InvocationTargetException {
 		Range range = new Range(new Position(0, 0), new Position(0, 5));
 		List<CompletionItem> items = Collections
diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSIncompleteCompletionProposal.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSIncompleteCompletionProposal.java
index 998044e..b45d161 100644
--- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSIncompleteCompletionProposal.java
+++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSIncompleteCompletionProposal.java
@@ -60,6 +60,8 @@
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
 
+import com.google.common.collect.ImmutableList;
+
 public class LSIncompleteCompletionProposal
 		implements ICompletionProposal, ICompletionProposalExtension3, ICompletionProposalExtension4,
 		ICompletionProposalExtension5, ICompletionProposalExtension6, ICompletionProposalExtension7,
@@ -289,8 +291,9 @@
 			}
 			insertText = textEdit.getNewText();
 			LinkedHashMap<String, List<LinkedPosition>> regions = new LinkedHashMap<>();
+			int insertionOffset = LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document);
+			insertionOffset = computeNewOffset(item.getAdditionalTextEdits(), insertionOffset, document);
 			if (item.getInsertTextFormat() == InsertTextFormat.Snippet) {
-				int insertionOffset = LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document);
 				int currentOffset = 0;
 				while ((currentOffset = insertText.indexOf('$', currentOffset)) != -1) {
 					StringBuilder keyBuilder = new StringBuilder();
@@ -335,7 +338,15 @@
 				}
 			}
 			textEdit.setNewText(insertText); // insertText now has placeholder removed
-			LSPEclipseUtils.applyEdit(textEdit, document);
+			List<TextEdit> additionalEdits = item.getAdditionalTextEdits();
+			if (additionalEdits != null && !additionalEdits.isEmpty()) {
+				ImmutableList.Builder<TextEdit> allEdits = ImmutableList.builder();
+				allEdits.add(textEdit);
+				allEdits.addAll(additionalEdits);
+				LSPEclipseUtils.applyEdits(document, allEdits.build());
+			} else {
+				LSPEclipseUtils.applyEdit(textEdit, document);
+			}
 
 			if (viewer != null && !regions.isEmpty()) {
 				LinkedModeModel model = new LinkedModeModel();
@@ -355,13 +366,36 @@
 				ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
 				ui.enter();
 			} else {
-				selection = new Region(LSPEclipseUtils.toOffset(textEdit.getRange().getStart(), document) + textEdit.getNewText().length(), 0);
+				selection = new Region(insertionOffset + textEdit.getNewText().length(), 0);
 			}
 		} catch (BadLocationException ex) {
 			LanguageServerPlugin.logError(ex);
 		}
 	}
 
+	private int computeNewOffset(List<TextEdit> additionalTextEdits, int insertionOffset, IDocument doc) {
+		if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) {
+			int adjustment = 0;
+			for (TextEdit edit : additionalTextEdits) {
+				try {
+					Range rng = edit.getRange();
+					int start = LSPEclipseUtils.toOffset(rng.getStart(), doc);
+					if (start <= insertionOffset) {
+						int end = LSPEclipseUtils.toOffset(rng.getEnd(), doc);
+						int orgLen = end - start;
+						int newLeng = edit.getNewText().length();
+						int editChange = newLeng - orgLen;
+						adjustment += editChange;
+					}
+				} catch (BadLocationException e) {
+					LanguageServerPlugin.logError(e);
+				}
+			}
+			return insertionOffset + adjustment;
+		}
+		return insertionOffset;
+	}
+
 	protected String getInsertText() {
 		String insertText = this.item.getInsertText();
 		if (this.item.getTextEdit() != null) {