1313
1414package org .eclipse .jdt .ls .core .internal .contentassist ;
1515
16+ import java .util .ArrayList ;
17+ import java .util .Arrays ;
18+ import java .util .Iterator ;
19+ import java .util .List ;
20+
21+ import org .eclipse .core .runtime .CoreException ;
22+ import org .eclipse .jdt .core .IBuffer ;
23+ import org .eclipse .jdt .core .ICompilationUnit ;
24+ import org .eclipse .jdt .internal .corext .fix .LinkedProposalModelCore ;
25+ import org .eclipse .jdt .internal .corext .fix .LinkedProposalPositionGroupCore ;
26+ import org .eclipse .jdt .internal .corext .fix .LinkedProposalPositionGroupCore .PositionInformation ;
27+ import org .eclipse .jdt .internal .corext .fix .LinkedProposalPositionGroupCore .ProposalCore ;
28+ import org .eclipse .jdt .internal .ui .text .correction .proposals .LinkedCorrectionProposalCore ;
29+ import org .eclipse .jdt .ls .core .internal .JDTUtils ;
1630import org .eclipse .jdt .ls .core .internal .JavaLanguageServerPlugin ;
31+ import org .eclipse .jdt .ls .core .internal .handlers .JsonRpcHelpers ;
32+ import org .eclipse .jface .text .link .LinkedModeModel ;
33+ import org .eclipse .jface .text .link .LinkedPosition ;
1734import org .eclipse .lsp4j .MarkupContent ;
1835import org .eclipse .lsp4j .MarkupKind ;
36+ import org .eclipse .lsp4j .Range ;
37+ import org .eclipse .lsp4j .TextEdit ;
38+ import org .eclipse .lsp4j .WorkspaceEdit ;
39+ import org .eclipse .lsp4j .extended .SnippetTextEdit ;
1940import org .eclipse .lsp4j .jsonrpc .messages .Either ;
41+ import org .eclipse .text .edits .MultiTextEdit ;
42+ import org .eclipse .text .edits .ReplaceEdit ;
2043
2144public class SnippetUtils {
2245
@@ -25,6 +48,11 @@ public class SnippetUtils {
2548 private static final String TM_SELECTED_TEXT = "\\ $TM_SELECTED_TEXT" ;
2649 private static final String TM_FILENAME_BASE = "\\ $TM_FILENAME_BASE" ;
2750
51+ public static final String SNIPPET_PREFIX = "${" ;
52+ public static final char SNIPPET_CHOICE_INDICATOR = '|' ;
53+ public static final String SNIPPET_CHOICE_POSTFIX = "|}" ;
54+ public static final String SNIPPET_CHOICE_SEPARATOR = "," ;
55+
2856 private SnippetUtils () {
2957 }
3058
@@ -64,4 +92,154 @@ public static Either<String, MarkupContent> beautifyDocument(String raw) {
6492 return Either .forLeft (escapedString );
6593 }
6694 }
95+
96+ /***
97+ * Supports linked correction proposals by converting them to snippets.
98+ * Represents a
99+ * {@link org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroupCore}
100+ * with snippet choice syntax if the group has multiple proposals, otherwise
101+ * represents it as a placeholder.
102+ *
103+ * @param proposal
104+ * the proposal to add support for
105+ * @param edit
106+ * the current edit to be returned with the code action
107+ * @throws CoreException
108+ */
109+ public static final void addSnippetsIfApplicable (LinkedCorrectionProposalCore proposal , WorkspaceEdit edit ) throws CoreException {
110+ ICompilationUnit compilationUnit = proposal .getCompilationUnit ();
111+ String proposalUri = JDTUtils .toURI (compilationUnit );
112+ IBuffer buffer = compilationUnit .getBuffer ();
113+ LinkedProposalModelCore linkedProposals = proposal .getLinkedProposalModel ();
114+ List <Triple > snippets = new ArrayList <>();
115+ Iterator <LinkedProposalPositionGroupCore > it = linkedProposals .getPositionGroupCoreIterator ();
116+
117+ while (it .hasNext ()) {
118+ LinkedProposalPositionGroupCore group = it .next ();
119+ ProposalCore [] proposalList = group .getProposals ();
120+ PositionInformation [] positionList = group .getPositions ();
121+ // Sorts in ascending order to ensure first position in list has the smallest offset
122+ Arrays .sort (positionList , (p1 , p2 ) -> {
123+ return p1 .getOffset () - p2 .getOffset ();
124+ });
125+ StringBuilder snippet = new StringBuilder ();
126+ snippet .append (SNIPPET_CHOICE_INDICATOR );
127+
128+ for (int i = 0 ; i < positionList .length ; i ++) {
129+ int offset = positionList [i ].getOffset ();
130+ int length = positionList [i ].getLength ();
131+
132+ // Create snippet on first iteration
133+ if (i == 0 ) {
134+ LinkedPosition linkedPosition = new LinkedPosition (JsonRpcHelpers .toDocument (buffer ), positionList [i ].getOffset (), positionList [i ].getLength (), positionList [i ].getSequenceRank ());
135+
136+ // Groups with no proposals will have the snippet text added while amending the WorkspaceEdit
137+ for (int j = 0 ; j < proposalList .length ; j ++) {
138+ org .eclipse .text .edits .TextEdit editWithText = findReplaceEdit (proposalList [j ].computeEdits (0 , linkedPosition , '\u0000' , 0 , new LinkedModeModel ()));
139+ if (editWithText != null ) {
140+ snippet .append (((ReplaceEdit ) editWithText ).getText ());
141+ snippet .append (SNIPPET_CHOICE_SEPARATOR );
142+ }
143+ }
144+ if (String .valueOf (snippet .charAt (snippet .length () - 1 )).equals (SNIPPET_CHOICE_SEPARATOR )) {
145+ snippet .deleteCharAt (snippet .length () - 1 );
146+ }
147+ snippet .append (SNIPPET_CHOICE_POSTFIX );
148+ // If snippet only has one choice, remove choice indicators
149+ if (snippet .indexOf (SNIPPET_CHOICE_SEPARATOR ) == -1 ) {
150+ snippet .setCharAt (0 , ':' );
151+ snippet .deleteCharAt (snippet .length () - 2 );
152+ }
153+ // Snippet is added with smallest offset as 0th element
154+ snippets .add (new Triple (snippet .toString (), offset , length ));
155+ } else {
156+ // Add offset/length values from additional positions in group to previously created snippet
157+ Triple currentSnippet = snippets .get (snippets .size () - 1 );
158+ currentSnippet .offset .add (offset );
159+ currentSnippet .length .add (length );
160+ }
161+ }
162+ }
163+ if (!snippets .isEmpty ()) {
164+ // Sort snippets based on offset of earliest occurrence to enable correct numbering
165+ snippets .sort (null );
166+ int snippetNumber = 1 ;
167+ for (int i = snippets .size () - 1 ; i >= 0 ; i --) {
168+ Triple element = snippets .get (i );
169+ element .snippet = SNIPPET_PREFIX + snippetNumber + element .snippet ;
170+ snippetNumber ++;
171+ // Separate snippets with multiple positions into individual instances in list
172+ for (int j = 1 ; j < element .offset .size (); j ++) {
173+ snippets .add (new Triple (element .snippet .toString (), element .offset .get (j ), element .length .get (j )));
174+ element .offset .remove (j );
175+ element .length .remove (j );
176+ }
177+ }
178+ // Re-sort snippets (with the added individual instances) by offset in descending order,
179+ // so that the amendments to the text edit are applied in an order that does not alter the offset of later amendments
180+ snippets .sort (null );
181+ for (int i = 0 ; i < edit .getDocumentChanges ().size (); i ++) {
182+ if (edit .getDocumentChanges ().get (i ).isLeft ()) {
183+ List <TextEdit > edits = edit .getDocumentChanges ().get (i ).getLeft ().getEdits ();
184+ String editUri = edit .getDocumentChanges ().get (i ).getLeft ().getTextDocument ().getUri ();
185+ for (int j = 0 ; j < edits .size (); j ++) {
186+ Range editRange = edits .get (j ).getRange ();
187+ StringBuilder replacementText = new StringBuilder (edits .get (j ).getNewText ());
188+ int rangeStart = JsonRpcHelpers .toOffset (buffer , editRange .getStart ().getLine (), editRange .getStart ().getCharacter ());
189+ int rangeEnd = rangeStart + replacementText .length ();
190+
191+ for (int k = 0 ; k < snippets .size (); k ++) {
192+ Triple currentSnippet = snippets .get (k );
193+ if (proposalUri .equals (editUri ) && currentSnippet .offset .get (0 ) >= rangeStart && currentSnippet .offset .get (0 ) <= rangeEnd ) {
194+ int replaceStart = currentSnippet .offset .get (0 ) - rangeStart ;
195+ int replaceEnd = replaceStart + currentSnippet .length .get (0 );
196+ // 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
197+ if (currentSnippet .snippet .endsWith (":}" )) {
198+ currentSnippet .snippet = currentSnippet .snippet .replaceFirst (":" , ":" + replacementText .substring (replaceStart , replaceEnd ));
199+ }
200+ replacementText .replace (replaceStart , replaceEnd , currentSnippet .snippet );
201+ }
202+ }
203+
204+ SnippetTextEdit newEdit = new SnippetTextEdit (editRange , replacementText .toString ());
205+ edits .set (j , newEdit );
206+ }
207+ }
208+ }
209+ }
210+ }
211+
212+ private static final org .eclipse .text .edits .TextEdit findReplaceEdit (org .eclipse .text .edits .TextEdit edit ) {
213+ if (edit instanceof ReplaceEdit && !((ReplaceEdit ) edit ).getText ().isBlank ()) {
214+ return edit ;
215+ }
216+
217+ if (edit instanceof MultiTextEdit ) {
218+ for (org .eclipse .text .edits .TextEdit child : edit .getChildren ()) {
219+ org .eclipse .text .edits .TextEdit replaceEdit = findReplaceEdit (child );
220+ if (replaceEdit != null ) {
221+ return replaceEdit ;
222+ }
223+ }
224+ }
225+ return null ;
226+ }
227+
228+ private static final class Triple implements Comparable <Triple > {
229+ public String snippet ;
230+ public List <Integer > offset = new ArrayList <>();
231+ public List <Integer > length = new ArrayList <>();
232+
233+ Triple (String snippet , int offset , int length ) {
234+ this .snippet = snippet ;
235+ this .offset .add (offset );
236+ this .length .add (length );
237+ }
238+
239+ // Sorts in descending order based on 0th (smallest) element of offset list
240+ @ Override
241+ public int compareTo (Triple other ) {
242+ return other .offset .get (0 ) - this .offset .get (0 );
243+ }
244+ }
67245}
0 commit comments