From a60b390b51098bfc67eba8dfc5dde1803003def9 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Thu, 25 Jul 2024 09:01:55 -0400 Subject: [PATCH 1/9] Add LinkedProposalModel -> snippet conversion to support linked proposals --- .../handlers/CodeActionResolveHandler.java | 132 +++++++++++++++++- .../preferences/ClientPreferences.java | 4 + .../lsp4j/extended/SnippetTextEdit.java | 35 +++++ 3 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 905c2f7ff1..4601e6789e 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -13,21 +13,43 @@ package org.eclipse.jdt.ls.core.internal.handlers; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.manipulation.ChangeCorrectionProposalCore; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.PositionInformation; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.ProposalCore; +import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposalCore; import org.eclipse.jdt.ls.core.internal.ChangeUtil; import org.eclipse.jdt.ls.core.internal.JSONUtility; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jface.text.link.LinkedModeModel; +import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.extended.SnippetTextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; public class CodeActionResolveHandler { public static final String DATA_FIELD_REQUEST_ID = "rid"; public static final String DATA_FIELD_PROPOSAL_ID = "pid"; + public static final String SNIPPET_PREFIX = "${"; + public static final char SNIPPET_CHOICE_INDICATOR = '|'; + public static final String SNIPPET_CHOICE_POSTFIX = "|}"; + public static final String SNIPPET_CHOICE_DELIMITER = ","; public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { Map data = JSONUtility.toModel(params.getData(), Map.class); @@ -46,8 +68,10 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { try { Either proposal = response.getProposals().get(proposalId); - WorkspaceEdit edit = proposal.isLeft() ? ChangeUtil.convertToWorkspaceEdit(proposal.getLeft().getChange()) - : proposal.getRight().resolveEdit(monitor); + WorkspaceEdit edit = proposal.isLeft() ? ChangeUtil.convertToWorkspaceEdit(proposal.getLeft().getChange()) : proposal.getRight().resolveEdit(monitor); + if (JavaLanguageServerPlugin.getPreferencesManager().getClientPreferences().isWorkspaceSnippetEditSupported() && proposal.isLeft() && proposal.getLeft() instanceof LinkedCorrectionProposalCore) { + addSnippetsIfApplicable((LinkedCorrectionProposalCore) proposal.getLeft(), edit); + } if (ChangeUtil.hasChanges(edit)) { params.setEdit(edit); } @@ -57,4 +81,108 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { return params; } + + private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore proposal, WorkspaceEdit edit) throws CoreException { + Object modifiedElement = proposal.getChange().getModifiedElement(); + ICompilationUnit compilationUnit = (ICompilationUnit) ((IJavaElement) modifiedElement).getAncestor(IJavaElement.COMPILATION_UNIT); + IBuffer buffer = compilationUnit.getBuffer(); + LinkedProposalModelCore linkedProposals = proposal.getLinkedProposalModel(); + List snippets = new ArrayList<>(); + Iterator it = linkedProposals.getPositionGroupCoreIterator(); + int snippetNumber = 1; + while (it.hasNext()) { + LinkedProposalPositionGroupCore group = it.next(); + ProposalCore[] proposalList = group.getProposals(); + PositionInformation[] positionList = group.getPositions(); + StringBuilder snippet = new StringBuilder(); + snippet.append(SNIPPET_PREFIX); + snippet.append(snippetNumber); + snippet.append(SNIPPET_CHOICE_INDICATOR); + if (proposalList.length > 1) { + for (int i = 0; i < positionList.length; i++) { + int offset = positionList[i].getOffset(); + int length = positionList[i].getLength(); + // Create snippet on first iteration + if (i == 0) { + LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); + for (int j = 0; j < proposalList.length; j++) { + org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); + if (editWithText != null) { + if (snippet.charAt(snippet.length()-1) != SNIPPET_CHOICE_INDICATOR) { + snippet.append(SNIPPET_CHOICE_DELIMITER); + } + snippet.append(((ReplaceEdit) editWithText).getText()); + } + } + // If snippet is empty or only has one choice, ignore this group + if (snippet.toString().equals(SNIPPET_PREFIX) || snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { + break; + } + snippet.append(SNIPPET_CHOICE_POSTFIX); + snippetNumber++; + } + snippets.add(new Triple(snippet.toString(), offset, length)); + } + } + } + if (!snippets.isEmpty()) { + // Sort snippets in descending order based on offset, so that the edits are applied in an order that does not alter the offset of later edits + snippets.sort(null); + for (int i = 0; i < edit.getDocumentChanges().size(); i++) { + if (edit.getDocumentChanges().get(i).isLeft()) { + List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); + for (int j = 0; j < edits.size(); j++) { + Range editRange = edits.get(j).getRange(); + StringBuilder replacementText = new StringBuilder(edits.get(j).getNewText()); + int rangeStart = JsonRpcHelpers.toOffset(buffer, editRange.getStart().getLine(), editRange.getStart().getCharacter()); + int rangeEnd = rangeStart + replacementText.length(); + for (int k = 0; k < snippets.size(); k++) { + if (snippets.get(k).offset >= rangeStart && snippets.get(k).offset <= rangeEnd) { + int replaceStart = snippets.get(k).offset - rangeStart; + int replaceEnd = replaceStart + snippets.get(k).length; + replacementText.replace(replaceStart, replaceEnd, snippets.get(k).snippet); + } + } + SnippetTextEdit newEdit = new SnippetTextEdit(editRange, replacementText.toString()); + edits.remove(j); + edits.add(j, newEdit); + } + } + } + } + } + + private static final org.eclipse.text.edits.TextEdit findReplaceOrInsertEdit(org.eclipse.text.edits.TextEdit edit) { + if (edit instanceof ReplaceEdit && !((ReplaceEdit) edit).getText().isBlank()) { + return edit; + } + + if (edit instanceof MultiTextEdit) { + org.eclipse.text.edits.TextEdit[] children = edit.getChildren(); + for (int i = 0; i < children.length; i++) { + org.eclipse.text.edits.TextEdit child = findReplaceOrInsertEdit(children[i]); + if (child != null) { + return child; + } + } + } + return null; + } + + private static final class Triple implements Comparable { + public String snippet; + public int offset; + public int length; + + Triple(String snippet, int offset, int length) { + this.snippet = snippet; + this.offset = offset; + this.length = length; + } + + @Override + public int compareTo(Triple other) { + return other.offset - this.offset; + } + } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java index 4ad13ba811..912319173b 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java @@ -195,6 +195,10 @@ public boolean isWorkspaceApplyEditSupported() { return capabilities.getWorkspace() != null && isTrue(capabilities.getWorkspace().getApplyEdit()); } + public boolean isWorkspaceSnippetEditSupported() { + return Boolean.parseBoolean(extendedClientCapabilities.getOrDefault("snippetEditSupport", "false").toString()); + } + public boolean isProgressReportSupported() { return Boolean.parseBoolean(extendedClientCapabilities.getOrDefault("progressReportProvider", "false").toString()); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java b/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java new file mode 100644 index 0000000000..e4dc8faeca --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.lsp4j.extended; + +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; + +public class SnippetTextEdit extends TextEdit { + StringValue snippet; + + public SnippetTextEdit(Range range, String snippet) { + super(range, ""); + this.snippet = new StringValue(snippet); + } + + private static final class StringValue { + public static final String kind = "snippet"; + String value; + + StringValue(String value) { + this.value = value; + } + } +} \ No newline at end of file From 6a6a22ca0227f449512d536e7a643a70dc387a3d Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Fri, 9 Aug 2024 16:47:41 -0400 Subject: [PATCH 2/9] fix order of snippets --- .../internal/handlers/CodeActionResolveHandler.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 4601e6789e..5df9db944a 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.Map; import org.eclipse.core.runtime.CoreException; @@ -95,8 +96,6 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p ProposalCore[] proposalList = group.getProposals(); PositionInformation[] positionList = group.getPositions(); StringBuilder snippet = new StringBuilder(); - snippet.append(SNIPPET_PREFIX); - snippet.append(snippetNumber); snippet.append(SNIPPET_CHOICE_INDICATOR); if (proposalList.length > 1) { for (int i = 0; i < positionList.length; i++) { @@ -115,11 +114,10 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p } } // If snippet is empty or only has one choice, ignore this group - if (snippet.toString().equals(SNIPPET_PREFIX) || snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { + if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR)) || snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { break; } snippet.append(SNIPPET_CHOICE_POSTFIX); - snippetNumber++; } snippets.add(new Triple(snippet.toString(), offset, length)); } @@ -128,6 +126,12 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p if (!snippets.isEmpty()) { // Sort snippets in descending order based on offset, so that the edits are applied in an order that does not alter the offset of later edits snippets.sort(null); + ListIterator li = snippets.listIterator(snippets.size()); + while (li.hasPrevious()) { + Triple element = li.previous(); + element.snippet = SNIPPET_PREFIX + snippetNumber + element.snippet; + snippetNumber++; + } for (int i = 0; i < edit.getDocumentChanges().size(); i++) { if (edit.getDocumentChanges().get(i).isLeft()) { List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); From db5c0336f26de488a3d775e945821b7cecbd3302 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Tue, 20 Aug 2024 13:34:09 -0400 Subject: [PATCH 3/9] support placeholder values for length 1 proposals --- .../internal/handlers/CodeActionResolveHandler.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 5df9db944a..a000cc4709 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -97,7 +97,7 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p PositionInformation[] positionList = group.getPositions(); StringBuilder snippet = new StringBuilder(); snippet.append(SNIPPET_CHOICE_INDICATOR); - if (proposalList.length > 1) { + if (proposalList.length > 0) { for (int i = 0; i < positionList.length; i++) { int offset = positionList[i].getOffset(); int length = positionList[i].getLength(); @@ -113,11 +113,16 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p snippet.append(((ReplaceEdit) editWithText).getText()); } } - // If snippet is empty or only has one choice, ignore this group - if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR)) || snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { + // If snippet is empty, ignore this group + if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR))) { break; } snippet.append(SNIPPET_CHOICE_POSTFIX); + // If snippet only has one choice, remove choice indicators + if (snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { + snippet.setCharAt(0, ':'); + snippet.deleteCharAt(snippet.length() - 2); + } } snippets.add(new Triple(snippet.toString(), offset, length)); } From 3a6bfa0c6ef538e146019d9bbc87357916fc596c Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Thu, 22 Aug 2024 10:59:14 -0400 Subject: [PATCH 4/9] add support for 0 length proposalList --- .../handlers/CodeActionResolveHandler.java | 93 +++++++++++-------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index a000cc4709..7ad3396a11 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -14,9 +14,10 @@ package org.eclipse.jdt.ls.core.internal.handlers; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.Iterator; import java.util.List; -import java.util.ListIterator; import java.util.Map; import org.eclipse.core.runtime.CoreException; @@ -95,48 +96,62 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p LinkedProposalPositionGroupCore group = it.next(); ProposalCore[] proposalList = group.getProposals(); PositionInformation[] positionList = group.getPositions(); + Arrays.sort(positionList, new Comparator() { + @Override + public int compare(PositionInformation p1, PositionInformation p2) { + return p1.getOffset() - p2.getOffset(); + } + }); StringBuilder snippet = new StringBuilder(); snippet.append(SNIPPET_CHOICE_INDICATOR); - if (proposalList.length > 0) { - for (int i = 0; i < positionList.length; i++) { - int offset = positionList[i].getOffset(); - int length = positionList[i].getLength(); - // Create snippet on first iteration - if (i == 0) { - LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); - for (int j = 0; j < proposalList.length; j++) { - org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); - if (editWithText != null) { - if (snippet.charAt(snippet.length()-1) != SNIPPET_CHOICE_INDICATOR) { - snippet.append(SNIPPET_CHOICE_DELIMITER); - } - snippet.append(((ReplaceEdit) editWithText).getText()); + for (int i = 0; i < positionList.length; i++) { + int offset = positionList[i].getOffset(); + int length = positionList[i].getLength(); + // Create snippet on first iteration + if (i == 0) { + LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); + for (int j = 0; j < proposalList.length; j++) { + org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); + if (editWithText != null) { + if (snippet.charAt(snippet.length() - 1) != SNIPPET_CHOICE_INDICATOR) { + snippet.append(SNIPPET_CHOICE_DELIMITER); } - } - // If snippet is empty, ignore this group - if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR))) { - break; - } - snippet.append(SNIPPET_CHOICE_POSTFIX); - // If snippet only has one choice, remove choice indicators - if (snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { - snippet.setCharAt(0, ':'); - snippet.deleteCharAt(snippet.length() - 2); + snippet.append(((ReplaceEdit) editWithText).getText()); } } + // // If snippet is empty, ignore this group + // if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR))) { + // break; + // } + snippet.append(SNIPPET_CHOICE_POSTFIX); + // If snippet only has one choice, remove choice indicators + if (snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { + snippet.setCharAt(0, ':'); + snippet.deleteCharAt(snippet.length() - 2); + } snippets.add(new Triple(snippet.toString(), offset, length)); + } else { + Triple currentSnippet = snippets.get(snippets.size() - 1); + currentSnippet.offset.add(offset); + currentSnippet.length.add(length); } } } if (!snippets.isEmpty()) { // Sort snippets in descending order based on offset, so that the edits are applied in an order that does not alter the offset of later edits snippets.sort(null); - ListIterator li = snippets.listIterator(snippets.size()); - while (li.hasPrevious()) { - Triple element = li.previous(); + // ListIterator li = snippets.listIterator(snippets.size()); + for (int i = snippets.size() - 1; i >= 0; i--) { + Triple element = snippets.get(i); element.snippet = SNIPPET_PREFIX + snippetNumber + element.snippet; snippetNumber++; + for (int j = 1; j < element.offset.size(); j++) { + snippets.add(new Triple(element.snippet.toString(), element.offset.get(j), element.length.get(j))); + element.offset.remove(j); + element.length.remove(j); + } } + snippets.sort(null); for (int i = 0; i < edit.getDocumentChanges().size(); i++) { if (edit.getDocumentChanges().get(i).isLeft()) { List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); @@ -146,10 +161,14 @@ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore p int rangeStart = JsonRpcHelpers.toOffset(buffer, editRange.getStart().getLine(), editRange.getStart().getCharacter()); int rangeEnd = rangeStart + replacementText.length(); for (int k = 0; k < snippets.size(); k++) { - if (snippets.get(k).offset >= rangeStart && snippets.get(k).offset <= rangeEnd) { - int replaceStart = snippets.get(k).offset - rangeStart; - int replaceEnd = replaceStart + snippets.get(k).length; - replacementText.replace(replaceStart, replaceEnd, snippets.get(k).snippet); + Triple snippetHolder = snippets.get(k); + if (snippetHolder.offset.get(0) >= rangeStart && snippetHolder.offset.get(0) <= rangeEnd) { + int replaceStart = snippetHolder.offset.get(0) - rangeStart; + int replaceEnd = replaceStart + snippetHolder.length.get(0); + if (snippetHolder.snippet.endsWith(":}")) { + snippetHolder.snippet = snippetHolder.snippet.replaceFirst(":", ":" + replacementText.substring(replaceStart, replaceEnd)); + } + replacementText.replace(replaceStart, replaceEnd, snippetHolder.snippet); } } SnippetTextEdit newEdit = new SnippetTextEdit(editRange, replacementText.toString()); @@ -180,18 +199,18 @@ private static final org.eclipse.text.edits.TextEdit findReplaceOrInsertEdit(org private static final class Triple implements Comparable { public String snippet; - public int offset; - public int length; + public List offset = new ArrayList<>(); + public List length = new ArrayList<>(); Triple(String snippet, int offset, int length) { this.snippet = snippet; - this.offset = offset; - this.length = length; + this.offset.add(offset); + this.length.add(length); } @Override public int compareTo(Triple other) { - return other.offset - this.offset; + return other.offset.get(0) - this.offset.get(0); } } } From 5c817e70e3977482062c807d63e12ed43b7aae00 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Thu, 22 Aug 2024 12:14:18 -0400 Subject: [PATCH 5/9] additional edits + comments --- .../handlers/CodeActionResolveHandler.java | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 7ad3396a11..5b9738d1ae 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -24,7 +24,6 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.IBuffer; import org.eclipse.jdt.core.ICompilationUnit; -import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.manipulation.ChangeCorrectionProposalCore; import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore; import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore; @@ -84,18 +83,31 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { return params; } + /*** + * Supports linked correction proposals by converting them to snippets. + * Represents a + * {@link org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore} + * with snippet choice syntax if the group has multiple proposals, otherwise + * represents it as a placeholder. + * + * @param proposal + * the proposal to add support for + * @param edit + * the current edit to be returned with the code action + * @throws CoreException + */ private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore proposal, WorkspaceEdit edit) throws CoreException { - Object modifiedElement = proposal.getChange().getModifiedElement(); - ICompilationUnit compilationUnit = (ICompilationUnit) ((IJavaElement) modifiedElement).getAncestor(IJavaElement.COMPILATION_UNIT); + ICompilationUnit compilationUnit = proposal.getCompilationUnit(); IBuffer buffer = compilationUnit.getBuffer(); LinkedProposalModelCore linkedProposals = proposal.getLinkedProposalModel(); List snippets = new ArrayList<>(); Iterator it = linkedProposals.getPositionGroupCoreIterator(); - int snippetNumber = 1; + while (it.hasNext()) { LinkedProposalPositionGroupCore group = it.next(); ProposalCore[] proposalList = group.getProposals(); PositionInformation[] positionList = group.getPositions(); + // Sorts in ascending order to ensure first position in list has the smallest offset Arrays.sort(positionList, new Comparator() { @Override public int compare(PositionInformation p1, PositionInformation p2) { @@ -104,12 +116,16 @@ public int compare(PositionInformation p1, PositionInformation p2) { }); StringBuilder snippet = new StringBuilder(); snippet.append(SNIPPET_CHOICE_INDICATOR); + for (int i = 0; i < positionList.length; i++) { int offset = positionList[i].getOffset(); int length = positionList[i].getLength(); + // Create snippet on first iteration if (i == 0) { LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); + + // Groups with no proposals will have the snippet text added while amending the WorkspaceEdit for (int j = 0; j < proposalList.length; j++) { org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); if (editWithText != null) { @@ -119,18 +135,17 @@ public int compare(PositionInformation p1, PositionInformation p2) { snippet.append(((ReplaceEdit) editWithText).getText()); } } - // // If snippet is empty, ignore this group - // if (snippet.toString().equals(String.valueOf(SNIPPET_CHOICE_INDICATOR))) { - // break; - // } + snippet.append(SNIPPET_CHOICE_POSTFIX); // If snippet only has one choice, remove choice indicators if (snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { snippet.setCharAt(0, ':'); snippet.deleteCharAt(snippet.length() - 2); } + // Snippet is added with smallest offset as 0th element snippets.add(new Triple(snippet.toString(), offset, length)); } else { + // Add offset/length values from additional positions in group to previously created snippet Triple currentSnippet = snippets.get(snippets.size() - 1); currentSnippet.offset.add(offset); currentSnippet.length.add(length); @@ -138,20 +153,24 @@ public int compare(PositionInformation p1, PositionInformation p2) { } } if (!snippets.isEmpty()) { - // Sort snippets in descending order based on offset, so that the edits are applied in an order that does not alter the offset of later edits + // Sort snippets based on offset of earliest occurrence to enable correct numbering snippets.sort(null); - // ListIterator li = snippets.listIterator(snippets.size()); + int snippetNumber = 1; for (int i = snippets.size() - 1; i >= 0; i--) { Triple element = snippets.get(i); element.snippet = SNIPPET_PREFIX + snippetNumber + element.snippet; snippetNumber++; + // Separate snippets with multiple positions into individual instances in list for (int j = 1; j < element.offset.size(); j++) { snippets.add(new Triple(element.snippet.toString(), element.offset.get(j), element.length.get(j))); element.offset.remove(j); element.length.remove(j); } } + // Re-sort snippets (with the added individual instances) by offset in descending order, + // so that the amendments to the text edit are applied in an order that does not alter the offset of later amendments snippets.sort(null); + for (int i = 0; i < edit.getDocumentChanges().size(); i++) { if (edit.getDocumentChanges().get(i).isLeft()) { List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); @@ -160,20 +179,22 @@ public int compare(PositionInformation p1, PositionInformation p2) { StringBuilder replacementText = new StringBuilder(edits.get(j).getNewText()); int rangeStart = JsonRpcHelpers.toOffset(buffer, editRange.getStart().getLine(), editRange.getStart().getCharacter()); int rangeEnd = rangeStart + replacementText.length(); + for (int k = 0; k < snippets.size(); k++) { - Triple snippetHolder = snippets.get(k); - if (snippetHolder.offset.get(0) >= rangeStart && snippetHolder.offset.get(0) <= rangeEnd) { - int replaceStart = snippetHolder.offset.get(0) - rangeStart; - int replaceEnd = replaceStart + snippetHolder.length.get(0); - if (snippetHolder.snippet.endsWith(":}")) { - snippetHolder.snippet = snippetHolder.snippet.replaceFirst(":", ":" + replacementText.substring(replaceStart, replaceEnd)); + Triple currentSnippet = snippets.get(k); + if (currentSnippet.offset.get(0) >= rangeStart && currentSnippet.offset.get(0) <= rangeEnd) { + int replaceStart = currentSnippet.offset.get(0) - rangeStart; + int replaceEnd = replaceStart + currentSnippet.length.get(0); + // If snippet text has not been added due to no elements in the proposal list, create snippet based on the text in the given position range + if (currentSnippet.snippet.endsWith(":}")) { + currentSnippet.snippet = currentSnippet.snippet.replaceFirst(":", ":" + replacementText.substring(replaceStart, replaceEnd)); } - replacementText.replace(replaceStart, replaceEnd, snippetHolder.snippet); + replacementText.replace(replaceStart, replaceEnd, currentSnippet.snippet); } } + SnippetTextEdit newEdit = new SnippetTextEdit(editRange, replacementText.toString()); - edits.remove(j); - edits.add(j, newEdit); + edits.set(j, newEdit); } } } @@ -208,6 +229,7 @@ private static final class Triple implements Comparable { this.length.add(length); } + // Sorts in descending order based on 0th (smallest) element of offset list @Override public int compareTo(Triple other) { return other.offset.get(0) - this.offset.get(0); From b8c25cea8d8bc4d408e90f915b682a2e0fcc5181 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Mon, 26 Aug 2024 15:36:47 -0400 Subject: [PATCH 6/9] move to SnippetUtils + change comma addition and sort logic --- .../internal/contentassist/SnippetUtils.java | 177 +++++++++++++++++ .../handlers/CodeActionResolveHandler.java | 178 +----------------- 2 files changed, 179 insertions(+), 176 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java index c8bea9cd6b..c21425e9d2 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java @@ -13,10 +13,32 @@ package org.eclipse.jdt.ls.core.internal.contentassist; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.PositionInformation; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.ProposalCore; +import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposalCore; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers; +import org.eclipse.jface.text.link.LinkedModeModel; +import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MarkupKind; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.extended.SnippetTextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; public class SnippetUtils { @@ -25,6 +47,11 @@ public class SnippetUtils { private static final String TM_SELECTED_TEXT = "\\$TM_SELECTED_TEXT"; private static final String TM_FILENAME_BASE = "\\$TM_FILENAME_BASE"; + public static final String SNIPPET_PREFIX = "${"; + public static final char SNIPPET_CHOICE_INDICATOR = '|'; + public static final String SNIPPET_CHOICE_POSTFIX = "|}"; + public static final String SNIPPET_CHOICE_SEPARATOR = ","; + private SnippetUtils() { } @@ -64,4 +91,154 @@ public static Either beautifyDocument(String raw) { return Either.forLeft(escapedString); } } + + /*** + * Supports linked correction proposals by converting them to snippets. + * Represents a + * {@link org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore} + * with snippet choice syntax if the group has multiple proposals, otherwise + * represents it as a placeholder. + * + * @param proposal + * the proposal to add support for + * @param edit + * the current edit to be returned with the code action + * @throws CoreException + */ + public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore proposal, WorkspaceEdit edit) throws CoreException { + ICompilationUnit compilationUnit = proposal.getCompilationUnit(); + IBuffer buffer = compilationUnit.getBuffer(); + LinkedProposalModelCore linkedProposals = proposal.getLinkedProposalModel(); + List snippets = new ArrayList<>(); + Iterator it = linkedProposals.getPositionGroupCoreIterator(); + + while (it.hasNext()) { + LinkedProposalPositionGroupCore group = it.next(); + ProposalCore[] proposalList = group.getProposals(); + PositionInformation[] positionList = group.getPositions(); + // Sorts in ascending order to ensure first position in list has the smallest offset + Arrays.sort(positionList, (p1, p2) -> { + return p1.getOffset() - p2.getOffset(); + }); + StringBuilder snippet = new StringBuilder(); + snippet.append(SNIPPET_CHOICE_INDICATOR); + + for (int i = 0; i < positionList.length; i++) { + int offset = positionList[i].getOffset(); + int length = positionList[i].getLength(); + + // Create snippet on first iteration + if (i == 0) { + LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); + + // Groups with no proposals will have the snippet text added while amending the WorkspaceEdit + for (int j = 0; j < proposalList.length; j++) { + org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); + if (editWithText != null) { + snippet.append(((ReplaceEdit) editWithText).getText()); + snippet.append(SNIPPET_CHOICE_SEPARATOR); + } + } + if (String.valueOf(snippet.charAt(snippet.length() - 1)).equals(SNIPPET_CHOICE_SEPARATOR)) { + snippet.deleteCharAt(snippet.length() - 1); + } + snippet.append(SNIPPET_CHOICE_POSTFIX); + // If snippet only has one choice, remove choice indicators + if (snippet.indexOf(SNIPPET_CHOICE_SEPARATOR) == -1) { + snippet.setCharAt(0, ':'); + snippet.deleteCharAt(snippet.length() - 2); + } + // Snippet is added with smallest offset as 0th element + snippets.add(new Triple(snippet.toString(), offset, length)); + } else { + // Add offset/length values from additional positions in group to previously created snippet + Triple currentSnippet = snippets.get(snippets.size() - 1); + currentSnippet.offset.add(offset); + currentSnippet.length.add(length); + } + } + } + if (!snippets.isEmpty()) { + // Sort snippets based on offset of earliest occurrence to enable correct numbering + snippets.sort(null); + int snippetNumber = 1; + for (int i = snippets.size() - 1; i >= 0; i--) { + Triple element = snippets.get(i); + element.snippet = SNIPPET_PREFIX + snippetNumber + element.snippet; + snippetNumber++; + // Separate snippets with multiple positions into individual instances in list + for (int j = 1; j < element.offset.size(); j++) { + snippets.add(new Triple(element.snippet.toString(), element.offset.get(j), element.length.get(j))); + element.offset.remove(j); + element.length.remove(j); + } + } + // Re-sort snippets (with the added individual instances) by offset in descending order, + // so that the amendments to the text edit are applied in an order that does not alter the offset of later amendments + snippets.sort(null); + + for (int i = 0; i < edit.getDocumentChanges().size(); i++) { + if (edit.getDocumentChanges().get(i).isLeft()) { + List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); + for (int j = 0; j < edits.size(); j++) { + Range editRange = edits.get(j).getRange(); + StringBuilder replacementText = new StringBuilder(edits.get(j).getNewText()); + int rangeStart = JsonRpcHelpers.toOffset(buffer, editRange.getStart().getLine(), editRange.getStart().getCharacter()); + int rangeEnd = rangeStart + replacementText.length(); + + for (int k = 0; k < snippets.size(); k++) { + Triple currentSnippet = snippets.get(k); + if (currentSnippet.offset.get(0) >= rangeStart && currentSnippet.offset.get(0) <= rangeEnd) { + int replaceStart = currentSnippet.offset.get(0) - rangeStart; + int replaceEnd = replaceStart + currentSnippet.length.get(0); + // If snippet text has not been added due to no elements in the proposal list, create snippet based on the text in the given position range + if (currentSnippet.snippet.endsWith(":}")) { + currentSnippet.snippet = currentSnippet.snippet.replaceFirst(":", ":" + replacementText.substring(replaceStart, replaceEnd)); + } + replacementText.replace(replaceStart, replaceEnd, currentSnippet.snippet); + } + } + + SnippetTextEdit newEdit = new SnippetTextEdit(editRange, replacementText.toString()); + edits.set(j, newEdit); + } + } + } + } + } + + private static final org.eclipse.text.edits.TextEdit findReplaceOrInsertEdit(org.eclipse.text.edits.TextEdit edit) { + if (edit instanceof ReplaceEdit && !((ReplaceEdit) edit).getText().isBlank()) { + return edit; + } + + if (edit instanceof MultiTextEdit) { + org.eclipse.text.edits.TextEdit[] children = edit.getChildren(); + for (int i = 0; i < children.length; i++) { + org.eclipse.text.edits.TextEdit child = findReplaceOrInsertEdit(children[i]); + if (child != null) { + return child; + } + } + } + return null; + } + + private static final class Triple implements Comparable { + public String snippet; + public List offset = new ArrayList<>(); + public List length = new ArrayList<>(); + + Triple(String snippet, int offset, int length) { + this.snippet = snippet; + this.offset.add(offset); + this.length.add(length); + } + + // Sorts in descending order based on 0th (smallest) element of offset list + @Override + public int compareTo(Triple other) { + return other.offset.get(0) - this.offset.get(0); + } + } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 5b9738d1ae..6f0d8059e1 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -13,44 +13,23 @@ package org.eclipse.jdt.ls.core.internal.handlers; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.jdt.core.IBuffer; -import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.manipulation.ChangeCorrectionProposalCore; -import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore; -import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore; -import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.PositionInformation; -import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.ProposalCore; import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposalCore; import org.eclipse.jdt.ls.core.internal.ChangeUtil; import org.eclipse.jdt.ls.core.internal.JSONUtility; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; -import org.eclipse.jface.text.link.LinkedModeModel; -import org.eclipse.jface.text.link.LinkedPosition; +import org.eclipse.jdt.ls.core.internal.contentassist.SnippetUtils; import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; -import org.eclipse.lsp4j.extended.SnippetTextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.ReplaceEdit; public class CodeActionResolveHandler { public static final String DATA_FIELD_REQUEST_ID = "rid"; public static final String DATA_FIELD_PROPOSAL_ID = "pid"; - public static final String SNIPPET_PREFIX = "${"; - public static final char SNIPPET_CHOICE_INDICATOR = '|'; - public static final String SNIPPET_CHOICE_POSTFIX = "|}"; - public static final String SNIPPET_CHOICE_DELIMITER = ","; public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { Map data = JSONUtility.toModel(params.getData(), Map.class); @@ -71,7 +50,7 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { Either proposal = response.getProposals().get(proposalId); WorkspaceEdit edit = proposal.isLeft() ? ChangeUtil.convertToWorkspaceEdit(proposal.getLeft().getChange()) : proposal.getRight().resolveEdit(monitor); if (JavaLanguageServerPlugin.getPreferencesManager().getClientPreferences().isWorkspaceSnippetEditSupported() && proposal.isLeft() && proposal.getLeft() instanceof LinkedCorrectionProposalCore) { - addSnippetsIfApplicable((LinkedCorrectionProposalCore) proposal.getLeft(), edit); + SnippetUtils.addSnippetsIfApplicable((LinkedCorrectionProposalCore) proposal.getLeft(), edit); } if (ChangeUtil.hasChanges(edit)) { params.setEdit(edit); @@ -82,157 +61,4 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { return params; } - - /*** - * Supports linked correction proposals by converting them to snippets. - * Represents a - * {@link org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore} - * with snippet choice syntax if the group has multiple proposals, otherwise - * represents it as a placeholder. - * - * @param proposal - * the proposal to add support for - * @param edit - * the current edit to be returned with the code action - * @throws CoreException - */ - private static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore proposal, WorkspaceEdit edit) throws CoreException { - ICompilationUnit compilationUnit = proposal.getCompilationUnit(); - IBuffer buffer = compilationUnit.getBuffer(); - LinkedProposalModelCore linkedProposals = proposal.getLinkedProposalModel(); - List snippets = new ArrayList<>(); - Iterator it = linkedProposals.getPositionGroupCoreIterator(); - - while (it.hasNext()) { - LinkedProposalPositionGroupCore group = it.next(); - ProposalCore[] proposalList = group.getProposals(); - PositionInformation[] positionList = group.getPositions(); - // Sorts in ascending order to ensure first position in list has the smallest offset - Arrays.sort(positionList, new Comparator() { - @Override - public int compare(PositionInformation p1, PositionInformation p2) { - return p1.getOffset() - p2.getOffset(); - } - }); - StringBuilder snippet = new StringBuilder(); - snippet.append(SNIPPET_CHOICE_INDICATOR); - - for (int i = 0; i < positionList.length; i++) { - int offset = positionList[i].getOffset(); - int length = positionList[i].getLength(); - - // Create snippet on first iteration - if (i == 0) { - LinkedPosition linkedPosition = new LinkedPosition(JsonRpcHelpers.toDocument(buffer), positionList[i].getOffset(), positionList[i].getLength(), positionList[i].getSequenceRank()); - - // Groups with no proposals will have the snippet text added while amending the WorkspaceEdit - for (int j = 0; j < proposalList.length; j++) { - org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); - if (editWithText != null) { - if (snippet.charAt(snippet.length() - 1) != SNIPPET_CHOICE_INDICATOR) { - snippet.append(SNIPPET_CHOICE_DELIMITER); - } - snippet.append(((ReplaceEdit) editWithText).getText()); - } - } - - snippet.append(SNIPPET_CHOICE_POSTFIX); - // If snippet only has one choice, remove choice indicators - if (snippet.indexOf(SNIPPET_CHOICE_DELIMITER) == -1) { - snippet.setCharAt(0, ':'); - snippet.deleteCharAt(snippet.length() - 2); - } - // Snippet is added with smallest offset as 0th element - snippets.add(new Triple(snippet.toString(), offset, length)); - } else { - // Add offset/length values from additional positions in group to previously created snippet - Triple currentSnippet = snippets.get(snippets.size() - 1); - currentSnippet.offset.add(offset); - currentSnippet.length.add(length); - } - } - } - if (!snippets.isEmpty()) { - // Sort snippets based on offset of earliest occurrence to enable correct numbering - snippets.sort(null); - int snippetNumber = 1; - for (int i = snippets.size() - 1; i >= 0; i--) { - Triple element = snippets.get(i); - element.snippet = SNIPPET_PREFIX + snippetNumber + element.snippet; - snippetNumber++; - // Separate snippets with multiple positions into individual instances in list - for (int j = 1; j < element.offset.size(); j++) { - snippets.add(new Triple(element.snippet.toString(), element.offset.get(j), element.length.get(j))); - element.offset.remove(j); - element.length.remove(j); - } - } - // Re-sort snippets (with the added individual instances) by offset in descending order, - // so that the amendments to the text edit are applied in an order that does not alter the offset of later amendments - snippets.sort(null); - - for (int i = 0; i < edit.getDocumentChanges().size(); i++) { - if (edit.getDocumentChanges().get(i).isLeft()) { - List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); - for (int j = 0; j < edits.size(); j++) { - Range editRange = edits.get(j).getRange(); - StringBuilder replacementText = new StringBuilder(edits.get(j).getNewText()); - int rangeStart = JsonRpcHelpers.toOffset(buffer, editRange.getStart().getLine(), editRange.getStart().getCharacter()); - int rangeEnd = rangeStart + replacementText.length(); - - for (int k = 0; k < snippets.size(); k++) { - Triple currentSnippet = snippets.get(k); - if (currentSnippet.offset.get(0) >= rangeStart && currentSnippet.offset.get(0) <= rangeEnd) { - int replaceStart = currentSnippet.offset.get(0) - rangeStart; - int replaceEnd = replaceStart + currentSnippet.length.get(0); - // If snippet text has not been added due to no elements in the proposal list, create snippet based on the text in the given position range - if (currentSnippet.snippet.endsWith(":}")) { - currentSnippet.snippet = currentSnippet.snippet.replaceFirst(":", ":" + replacementText.substring(replaceStart, replaceEnd)); - } - replacementText.replace(replaceStart, replaceEnd, currentSnippet.snippet); - } - } - - SnippetTextEdit newEdit = new SnippetTextEdit(editRange, replacementText.toString()); - edits.set(j, newEdit); - } - } - } - } - } - - private static final org.eclipse.text.edits.TextEdit findReplaceOrInsertEdit(org.eclipse.text.edits.TextEdit edit) { - if (edit instanceof ReplaceEdit && !((ReplaceEdit) edit).getText().isBlank()) { - return edit; - } - - if (edit instanceof MultiTextEdit) { - org.eclipse.text.edits.TextEdit[] children = edit.getChildren(); - for (int i = 0; i < children.length; i++) { - org.eclipse.text.edits.TextEdit child = findReplaceOrInsertEdit(children[i]); - if (child != null) { - return child; - } - } - } - return null; - } - - private static final class Triple implements Comparable { - public String snippet; - public List offset = new ArrayList<>(); - public List length = new ArrayList<>(); - - Triple(String snippet, int offset, int length) { - this.snippet = snippet; - this.offset.add(offset); - this.length.add(length); - } - - // Sorts in descending order based on 0th (smallest) element of offset list - @Override - public int compareTo(Triple other) { - return other.offset.get(0) - this.offset.get(0); - } - } } From 5c2e354caffa9d3b249aff9bb709b141fd739890 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Tue, 27 Aug 2024 09:48:44 -0400 Subject: [PATCH 7/9] add uri check --- .../jdt/ls/core/internal/contentassist/SnippetUtils.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java index c21425e9d2..0a01974681 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java @@ -26,6 +26,7 @@ import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.PositionInformation; import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore.ProposalCore; import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposalCore; +import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers; import org.eclipse.jface.text.link.LinkedModeModel; @@ -107,6 +108,7 @@ public static Either beautifyDocument(String raw) { */ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore proposal, WorkspaceEdit edit) throws CoreException { ICompilationUnit compilationUnit = proposal.getCompilationUnit(); + String proposalUri = JDTUtils.toURI(compilationUnit); IBuffer buffer = compilationUnit.getBuffer(); LinkedProposalModelCore linkedProposals = proposal.getLinkedProposalModel(); List snippets = new ArrayList<>(); @@ -180,6 +182,7 @@ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore pr for (int i = 0; i < edit.getDocumentChanges().size(); i++) { if (edit.getDocumentChanges().get(i).isLeft()) { List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); + String editUri = edit.getDocumentChanges().get(i).getLeft().getTextDocument().getUri(); for (int j = 0; j < edits.size(); j++) { Range editRange = edits.get(j).getRange(); StringBuilder replacementText = new StringBuilder(edits.get(j).getNewText()); @@ -188,7 +191,7 @@ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore pr for (int k = 0; k < snippets.size(); k++) { Triple currentSnippet = snippets.get(k); - if (currentSnippet.offset.get(0) >= rangeStart && currentSnippet.offset.get(0) <= rangeEnd) { + if (proposalUri.equals(editUri) && currentSnippet.offset.get(0) >= rangeStart && currentSnippet.offset.get(0) <= rangeEnd) { int replaceStart = currentSnippet.offset.get(0) - rangeStart; int replaceEnd = replaceStart + currentSnippet.length.get(0); // If snippet text has not been added due to no elements in the proposal list, create snippet based on the text in the given position range From 26c57922f32e5312cf163285cf41f4515303e67b Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Wed, 28 Aug 2024 10:45:45 -0400 Subject: [PATCH 8/9] change findReplaceEdit and add test case --- .../internal/contentassist/SnippetUtils.java | 14 +++--- .../handlers/CodeActionResolveHandler.java | 3 +- .../CodeActionResolveHandlerTest.java | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java index 0a01974681..57e3ad90e8 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/contentassist/SnippetUtils.java @@ -135,7 +135,7 @@ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore pr // Groups with no proposals will have the snippet text added while amending the WorkspaceEdit for (int j = 0; j < proposalList.length; j++) { - org.eclipse.text.edits.TextEdit editWithText = findReplaceOrInsertEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); + org.eclipse.text.edits.TextEdit editWithText = findReplaceEdit(proposalList[j].computeEdits(0, linkedPosition, '\u0000', 0, new LinkedModeModel())); if (editWithText != null) { snippet.append(((ReplaceEdit) editWithText).getText()); snippet.append(SNIPPET_CHOICE_SEPARATOR); @@ -178,7 +178,6 @@ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore pr // Re-sort snippets (with the added individual instances) by offset in descending order, // so that the amendments to the text edit are applied in an order that does not alter the offset of later amendments snippets.sort(null); - for (int i = 0; i < edit.getDocumentChanges().size(); i++) { if (edit.getDocumentChanges().get(i).isLeft()) { List edits = edit.getDocumentChanges().get(i).getLeft().getEdits(); @@ -210,17 +209,16 @@ public static final void addSnippetsIfApplicable(LinkedCorrectionProposalCore pr } } - private static final org.eclipse.text.edits.TextEdit findReplaceOrInsertEdit(org.eclipse.text.edits.TextEdit edit) { + private static final org.eclipse.text.edits.TextEdit findReplaceEdit(org.eclipse.text.edits.TextEdit edit) { if (edit instanceof ReplaceEdit && !((ReplaceEdit) edit).getText().isBlank()) { return edit; } if (edit instanceof MultiTextEdit) { - org.eclipse.text.edits.TextEdit[] children = edit.getChildren(); - for (int i = 0; i < children.length; i++) { - org.eclipse.text.edits.TextEdit child = findReplaceOrInsertEdit(children[i]); - if (child != null) { - return child; + for (org.eclipse.text.edits.TextEdit child : edit.getChildren()) { + org.eclipse.text.edits.TextEdit replaceEdit = findReplaceEdit(child); + if (replaceEdit != null) { + return replaceEdit; } } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java index 6f0d8059e1..0c4c5405e1 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandler.java @@ -49,7 +49,8 @@ public CodeAction resolve(CodeAction params, IProgressMonitor monitor) { try { Either proposal = response.getProposals().get(proposalId); WorkspaceEdit edit = proposal.isLeft() ? ChangeUtil.convertToWorkspaceEdit(proposal.getLeft().getChange()) : proposal.getRight().resolveEdit(monitor); - if (JavaLanguageServerPlugin.getPreferencesManager().getClientPreferences().isWorkspaceSnippetEditSupported() && proposal.isLeft() && proposal.getLeft() instanceof LinkedCorrectionProposalCore) { + if (JavaLanguageServerPlugin.getPreferencesManager().getClientPreferences().isWorkspaceSnippetEditSupported() && edit.getDocumentChanges() != null + && proposal.isLeft() && proposal.getLeft() instanceof LinkedCorrectionProposalCore) { SnippetUtils.addSnippetsIfApplicable((LinkedCorrectionProposalCore) proposal.getLeft(), edit); } if (ChangeUtil.hasChanges(edit)) { diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java index 6a677573af..915a1d670b 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java @@ -51,6 +51,8 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.extended.SnippetTextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.junit.Assert; import org.junit.Before; @@ -360,6 +362,48 @@ public void testResolveCodeAction_SourceActions() throws Exception { Assert.assertEquals(buf.toString(), actual); } + @Test + public void testResolveCodeAction_LinkedProposals() throws Exception { + when(preferenceManager.getClientPreferences().isResolveCodeActionSupported()).thenReturn(true); + when(preferenceManager.getClientPreferences().isWorkspaceSnippetEditSupported()).thenReturn(true); + when(preferenceManager.getClientPreferences().isResourceOperationSupported()).thenReturn(true); + + StringBuilder buf = new StringBuilder(); + buf.append("import java.util.HashMap;\n"); + buf.append("import java.util.List;\n"); + buf.append("public class E {\n"); + buf.append(" private void hello() {\n"); + buf.append(" List test = new HashMap();\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit unit = defaultPackage.createCompilationUnit("E.java", buf.toString(), false, null); + CodeActionParams params = new CodeActionParams(); + params.setTextDocument(new TextDocumentIdentifier(JDTUtils.toURI(unit))); + final Range range = CodeActionUtil.getRange(unit, "new HashMap()"); + params.setRange(range); + CodeActionContext context = new CodeActionContext(Arrays.asList(getDiagnostic(Integer.toString(IProblem.TypeMismatch), range)), Collections.singletonList(CodeActionKind.QuickFix)); + params.setContext(context); + + List> quickfixActions = server.codeAction(params).join(); + Assert.assertNotNull(quickfixActions); + Assert.assertFalse("No quickfix actions were found", quickfixActions.isEmpty()); + for (Either codeAction : quickfixActions) { + Assert.assertNull("Should defer the edit property to the resolveCodeAction request", codeAction.getRight().getEdit()); + Assert.assertNotNull("Should preserve the data property for the unresolved code action", codeAction.getRight().getData()); + } + + Optional> removeUnusedResponse = quickfixActions.stream().filter(codeAction -> { + return "Change type of 'test' to 'HashMap'".equals(codeAction.getRight().getTitle()); + }).findFirst(); + Assert.assertTrue("Should return the quickfix \"Change type of 'test' to 'HashMap'\"", removeUnusedResponse.isPresent()); + CodeAction unresolvedCodeAction = removeUnusedResponse.get().getRight(); + + CodeAction resolvedCodeAction = server.resolveCodeAction(unresolvedCodeAction).join(); + Assert.assertNotNull("Should resolve the edit property in the resolveCodeAction request", resolvedCodeAction.getEdit()); + List edits = resolvedCodeAction.getEdit().getDocumentChanges().get(0).getLeft().getEdits(); + Assert.assertTrue(edits.get(0) instanceof SnippetTextEdit); + } + private Diagnostic getDiagnostic(String code, Range range){ Diagnostic $ = new Diagnostic(); $.setCode(code); From 798415a70f6ca11df1fbbb8b09242d8202359b59 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Wed, 28 Aug 2024 14:59:45 -0400 Subject: [PATCH 9/9] add getter methods for SnippetTextEdit and check value in test --- .../org/eclipse/lsp4j/extended/SnippetTextEdit.java | 10 +++++++++- .../handlers/CodeActionResolveHandlerTest.java | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java b/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java index e4dc8faeca..fb1efe57b4 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/lsp4j/extended/SnippetTextEdit.java @@ -24,12 +24,20 @@ public SnippetTextEdit(Range range, String snippet) { this.snippet = new StringValue(snippet); } - private static final class StringValue { + public StringValue getSnippet() { + return snippet; + } + + public static final class StringValue { public static final String kind = "snippet"; String value; StringValue(String value) { this.value = value; } + + public String getValue() { + return value; + } } } \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java index 915a1d670b..3faf65c750 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeActionResolveHandlerTest.java @@ -385,7 +385,6 @@ public void testResolveCodeAction_LinkedProposals() throws Exception { params.setContext(context); List> quickfixActions = server.codeAction(params).join(); - Assert.assertNotNull(quickfixActions); Assert.assertFalse("No quickfix actions were found", quickfixActions.isEmpty()); for (Either codeAction : quickfixActions) { Assert.assertNull("Should defer the edit property to the resolveCodeAction request", codeAction.getRight().getEdit()); @@ -402,6 +401,7 @@ public void testResolveCodeAction_LinkedProposals() throws Exception { Assert.assertNotNull("Should resolve the edit property in the resolveCodeAction request", resolvedCodeAction.getEdit()); List edits = resolvedCodeAction.getEdit().getDocumentChanges().get(0).getLeft().getEdits(); Assert.assertTrue(edits.get(0) instanceof SnippetTextEdit); + Assert.assertEquals(((SnippetTextEdit) edits.get(0)).getSnippet().getValue(), "${1|HashMap,Map,Cloneable,Serializable,AbstractMap,Object|}"); } private Diagnostic getDiagnostic(String code, Range range){