|
19 | 19 | import java.text.MessageFormat; |
20 | 20 | import java.util.ArrayList; |
21 | 21 | import java.util.Collections; |
| 22 | +import java.util.HashSet; |
22 | 23 | import java.util.List; |
| 24 | +import java.util.Set; |
23 | 25 | import java.util.concurrent.CompletableFuture; |
24 | 26 | import java.util.logging.Level; |
25 | 27 | import java.util.logging.Logger; |
|
29 | 31 | import org.eclipse.lsp4j.Diagnostic; |
30 | 32 | import org.eclipse.lsp4j.Position; |
31 | 33 | import org.eclipse.lsp4j.Range; |
| 34 | +import org.eclipse.lsp4j.TextEdit; |
32 | 35 |
|
33 | 36 | import com.google.gson.JsonObject; |
34 | 37 | import com.redhat.qute.ls.commons.BadLocationException; |
|
40 | 43 | import com.redhat.qute.parser.template.Expression; |
41 | 44 | import com.redhat.qute.parser.template.Node; |
42 | 45 | import com.redhat.qute.parser.template.NodeKind; |
| 46 | +import com.redhat.qute.parser.template.Parameter; |
43 | 47 | import com.redhat.qute.parser.template.Section; |
| 48 | +import com.redhat.qute.parser.template.SectionKind; |
44 | 49 | import com.redhat.qute.parser.template.Template; |
| 50 | +import com.redhat.qute.parser.template.sections.WithSection; |
45 | 51 | import com.redhat.qute.project.QuteProject; |
46 | 52 | import com.redhat.qute.services.commands.QuteClientCommandConstants; |
47 | 53 | import com.redhat.qute.services.diagnostics.QuteErrorCode; |
@@ -72,6 +78,8 @@ class QuteCodeActions { |
72 | 78 |
|
73 | 79 | private static final String EXCLUDED_VALIDATION_TITLE = "Exclude this file from validation."; |
74 | 80 |
|
| 81 | + private static final String QUTE_DEPRICATED_WITH_SECTION = "Replace `#with` with `#let`."; |
| 82 | + |
75 | 83 | public CompletableFuture<List<CodeAction>> doCodeActions(Template template, CodeActionContext context, Range range, |
76 | 84 | SharedSettings sharedSettings) { |
77 | 85 | List<CodeAction> codeActions = new ArrayList<>(); |
@@ -109,6 +117,15 @@ public CompletableFuture<List<CodeAction>> doCodeActions(Template template, Code |
109 | 117 | // Create `undefinedTag`" |
110 | 118 | doCodeActionsForUndefinedSectionTag(template, diagnostic, codeActions); |
111 | 119 | break; |
| 120 | + case NotRecommendedWithSection: |
| 121 | + // The following Qute template: |
| 122 | + // {#with } |
| 123 | + // |
| 124 | + // will provide a quickfix like: |
| 125 | + // |
| 126 | + // Replace `with` with `let`. |
| 127 | + doCodeActionsForNotRecommendedWithSection(template, diagnostic, codeActions); |
| 128 | + break; |
112 | 129 | default: |
113 | 130 | break; |
114 | 131 | } |
@@ -186,6 +203,127 @@ private static void doCodeActionToDisableValidation(Template template, List<Diag |
186 | 203 | codeActions.add(disableValidationForTemplateQuickFix); |
187 | 204 | } |
188 | 205 |
|
| 206 | + /** |
| 207 | + * Create CodeAction for unrecommended `with` Qute syntax. |
| 208 | + * |
| 209 | + * e.g. <code> |
| 210 | + * {#with item} |
| 211 | + * {name} |
| 212 | + * {/with} |
| 213 | + * </code> becomes <code> |
| 214 | + * {#let name=item.name} |
| 215 | + * {name} |
| 216 | + * {/let} |
| 217 | + * </code> |
| 218 | + * |
| 219 | + * @param template the Qute template. |
| 220 | + * @param diagnostic the diagnostic list that this CodeAction will fix. |
| 221 | + * @param codeActions the list of CodeActions to perform. |
| 222 | + * |
| 223 | + */ |
| 224 | + private static void doCodeActionsForNotRecommendedWithSection(Template template, Diagnostic diagnostic, |
| 225 | + List<CodeAction> codeActions) { |
| 226 | + Range withSectionRange = diagnostic.getRange(); |
| 227 | + try { |
| 228 | + // 1. Retrieve the #with section node using diagnostic range |
| 229 | + int withSectionStart = template.offsetAt(withSectionRange.getStart()); |
| 230 | + WithSection withSection = (WithSection) template.findNodeAt(withSectionStart + 1); |
| 231 | + |
| 232 | + // 2. Initialize TextEdit array |
| 233 | + List<TextEdit> edits = new ArrayList<TextEdit>(); |
| 234 | + |
| 235 | + // 2.1 Create text edit to update section start from #with to #let |
| 236 | + // and collect all object parts from expression for text edit |
| 237 | + // (e.g. {name} -> {#let name=item.name}) |
| 238 | + edits.add(createWithSectionOpenEdit(template, withSection)); |
| 239 | + |
| 240 | + // 2.2 Create text edit to update section end from /with to /let |
| 241 | + edits.add(createWithSectionCloseEdit(template, withSection)); |
| 242 | + |
| 243 | + // 3. Create CodeAction |
| 244 | + CodeAction replaceWithSection = CodeActionFactory.replace(QUTE_DEPRICATED_WITH_SECTION, edits, |
| 245 | + template.getTextDocument(), diagnostic); |
| 246 | + codeActions.add(replaceWithSection); |
| 247 | + } catch (BadLocationException e) { |
| 248 | + LOGGER.log(Level.SEVERE, "Creation of not recommended with section code action failed", e); |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + /** |
| 253 | + * Create text edit to replace unrecommended with section opening tag |
| 254 | + * |
| 255 | + * @param template the Qute template. |
| 256 | + * @param withSection the Qute with section |
| 257 | + * @return |
| 258 | + * @throws BadLocationException |
| 259 | + */ |
| 260 | + private static TextEdit createWithSectionOpenEdit(Template template, WithSection withSection) |
| 261 | + throws BadLocationException { |
| 262 | + String objectPartName = withSection.getObjectParameter() != null ? withSection.getObjectParameter().getName() |
| 263 | + : ""; |
| 264 | + // Use set to avoid duplicate parameters |
| 265 | + Set<String> withObjectParts = new HashSet<String>(); |
| 266 | + |
| 267 | + // Retrieve all expressions in #with section |
| 268 | + findObjectParts(withSection, withObjectParts); |
| 269 | + List<String> letObjectPartParameterList = new ArrayList<String>(); |
| 270 | + for (String objectPart : withObjectParts) { |
| 271 | + letObjectPartParameterList.add(MessageFormat.format("{1}={0}.{1}", objectPartName, objectPart)); |
| 272 | + } |
| 273 | + |
| 274 | + // Build text edit string |
| 275 | + String letObjectPartParameters = String.join(" ", letObjectPartParameterList); |
| 276 | + String withSectionOpenReplacementText = MessageFormat.format("#let {0}", letObjectPartParameters); |
| 277 | + Range withSectionOpen = new Range(template.positionAt(withSection.getStartTagNameOpenOffset()), |
| 278 | + template.positionAt(withSection.getStartTagCloseOffset())); |
| 279 | + |
| 280 | + return new TextEdit(withSectionOpen, withSectionOpenReplacementText); |
| 281 | + } |
| 282 | + |
| 283 | + /** |
| 284 | + * Find all object parts by traversing AST Nodes, while retrieveing Expressions |
| 285 | + * nested in Sections with recursion |
| 286 | + * |
| 287 | + * @param node current node in AST traversal |
| 288 | + * @param objectParts set of found object parts |
| 289 | + */ |
| 290 | + private static void findObjectParts(Node node, Set<String> objectParts) { |
| 291 | + List<Node> children = node.getChildren(); |
| 292 | + // Base case: Node is an expression |
| 293 | + if (children.isEmpty()) { |
| 294 | + if (node.getKind() == NodeKind.Expression) { |
| 295 | + objectParts.add(((Expression) node).getContent()); |
| 296 | + } |
| 297 | + } |
| 298 | + for (Node child : children) { |
| 299 | + if (child.getKind() == NodeKind.Expression) { |
| 300 | + objectParts.add(((Expression) child).getContent()); |
| 301 | + } else if (child.getKind() == NodeKind.Section && ((Section) child).getSectionKind() != SectionKind.WITH) { |
| 302 | + for (Parameter param : ((Section) child).getParameters()) { |
| 303 | + objectParts.add(param.getValue()); |
| 304 | + } |
| 305 | + // Recursive call to traverse nested non-WithSection Sections |
| 306 | + findObjectParts(child, objectParts); |
| 307 | + } |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + /** |
| 312 | + * Create text edit to replace unrecommended with section closing tag |
| 313 | + * |
| 314 | + * @param template the Qute template. |
| 315 | + * @param withSection the Qute with section |
| 316 | + * @return |
| 317 | + * @throws BadLocationException |
| 318 | + */ |
| 319 | + private static TextEdit createWithSectionCloseEdit(Template template, WithSection withSection) |
| 320 | + throws BadLocationException { |
| 321 | + String withSectionCloseReplacementText = "/let"; |
| 322 | + Range withSectionClose = new Range(template.positionAt(withSection.getEndTagNameOpenOffset()), |
| 323 | + template.positionAt(withSection.getEndTagCloseOffset())); |
| 324 | + return new TextEdit(withSectionClose, withSectionCloseReplacementText); |
| 325 | + } |
| 326 | + |
189 | 327 | /** |
190 | 328 | * Create the configuration update (done on client side) quick fix. |
191 | 329 | * |
|
0 commit comments