diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java index 7a6e199c3..7218b0bf3 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Angelo ZERR + * Copyright (c) 2018, 2023 Angelo ZERR * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -45,35 +45,54 @@ public XMLCodeActions(XMLExtensionsRegistry extensionsRegistry) { } public List doCodeActions(CodeActionContext context, Range range, DOMDocument document, - SharedSettings sharedSettings, CancelChecker cancelChecker) { + SharedSettings sharedSettings, CancelChecker cancelChecker) throws CancellationException { cancelChecker.checkCanceled(); List codeActions = new ArrayList<>(); List diagnostics = context.getDiagnostics(); + + // The first pass is for CodeAction participants that have to react on a certain diagnostic code if (diagnostics != null) { - for (Diagnostic diagnostic : context.getDiagnostics()) { - for (ICodeActionParticipant codeActionParticipant : extensionsRegistry.getCodeActionsParticipants()) { - try { + for (Diagnostic diagnostic : diagnostics) { + if (diagnostic != null) { // Never run this cycle if diagnostic is null + for (ICodeActionParticipant codeActionParticipant : extensionsRegistry.getCodeActionsParticipants()) { cancelChecker.checkCanceled(); - CodeActionRequest request = new CodeActionRequest(diagnostic, range, document, - extensionsRegistry, sharedSettings); - codeActionParticipant.doCodeAction(request, codeActions, cancelChecker); - } catch (CancellationException e) { - throw e; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error while processing code actions for the participant '" - + codeActionParticipant.getClass().getName() + "'.", e); + try { + CodeActionRequest request = new CodeActionRequest(diagnostic, range, document, + extensionsRegistry, sharedSettings); + codeActionParticipant.doCodeAction(request, codeActions, cancelChecker); + } catch (CancellationException e) { + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing code actions for the participant '" + + codeActionParticipant.getClass().getName() + "'.", e); + } } } } } + // The second pass is for CodeAction participants that have to create CodeActions independently of diagnostics + for (ICodeActionParticipant codeActionParticipant : extensionsRegistry.getCodeActionsParticipants()) { + cancelChecker.checkCanceled(); + try { + CodeActionRequest request = new CodeActionRequest(null, range, document, + extensionsRegistry, sharedSettings); + codeActionParticipant.doCodeActionUnconditional(request, codeActions, cancelChecker); + } catch (CancellationException e) { + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing code actions for the participant '" + + codeActionParticipant.getClass().getName() + "'.", e); + } + } + cancelChecker.checkCanceled(); return codeActions; } public CodeAction resolveCodeAction(CodeAction unresolved, DOMDocument document, SharedSettings sharedSettings, - CancelChecker cancelChecker) { + CancelChecker cancelChecker) throws CancellationException { ResolveCodeActionRequest request = new ResolveCodeActionRequest(unresolved, document, extensionsRegistry, sharedSettings); String participantId = request.getParticipantId(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/codeaction/ICodeActionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/codeaction/ICodeActionParticipant.java index 4d5b6fe6c..76dbc9331 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/codeaction/ICodeActionParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/codeaction/ICodeActionParticipant.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Angelo ZERR. + * Copyright (c) 2018, 2023 Angelo ZERR. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -35,6 +35,17 @@ public interface ICodeActionParticipant { */ void doCodeAction(ICodeActionRequest request, List codeActions, CancelChecker cancelChecker); + /** + * Collect the code action in the given codeActions for the given + * code action request request independently of diagnostic provided. + * + * @param request the code action request. + * @param codeActions list of code actions to fill. + * @param cancelChecker the cancel checker. + */ + default void doCodeActionUnconditional(ICodeActionRequest request, List codeActions, CancelChecker cancelChecker) { + } + /** * Returns the codeAction resolver participant identified by the given * participantId and null otherwise. diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java index f81539059..473941fd4 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018, 2022 Angelo ZERR + * Copyright (c) 2018, 2023 Angelo ZERR * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -878,7 +878,7 @@ public static List testCodeActionsFor(String xml, Diagnostic diagnos SharedSettings settings = new SharedSettings(); settings.getFormattingSettings().setTabSize(4); settings.getFormattingSettings().setInsertSpaces(false); - return testCodeActionsFor(xml, diagnostic, null, null, settings, null, index, expected); + return testCodeActionsFor(xml, diagnostic, null, null, null, settings, null, index, expected); } public static List testCodeActionsFor(String xml, String fileURI, Diagnostic diagnostic, @@ -901,7 +901,7 @@ public static List testCodeActionsFor(String xml, String fileURI, Di SharedSettings settings = new SharedSettings(); settings.getFormattingSettings().setTabSize(4); settings.getFormattingSettings().setInsertSpaces(false); - return testCodeActionsFor(xml, diagnostic, catalogPath, fileURI, settings, null, -1, expected); + return testCodeActionsFor(xml, diagnostic, null, catalogPath, fileURI, settings, null, -1, expected); } public static List testCodeActionsFor(String xml, Diagnostic diagnostic, String catalogPath, @@ -912,26 +912,33 @@ public static List testCodeActionsFor(String xml, Diagnostic diagnos public static List testCodeActionsFor(String xml, Diagnostic diagnostic, String catalogPath, SharedSettings sharedSettings, XMLLanguageService xmlLanguageService, CodeAction... expected) throws BadLocationException { - return testCodeActionsFor(xml, diagnostic, catalogPath, null, sharedSettings, xmlLanguageService, -1, expected); + return testCodeActionsFor(xml, diagnostic, null, catalogPath, null, sharedSettings, xmlLanguageService, -1, expected); } - public static List testCodeActionsFor(String xml, Diagnostic diagnostic, String catalogPath, + public static List testCodeActionsFor(String xml, Range range, String catalogPath, + SharedSettings sharedSettings, XMLLanguageService xmlLanguageService, CodeAction... expected) + throws BadLocationException { + return testCodeActionsFor(xml, null, range, catalogPath, null, sharedSettings, xmlLanguageService, -1, expected); + } + public static List testCodeActionsFor(String xml, Diagnostic diagnostic, Range range, String catalogPath, String fileURI, SharedSettings sharedSettings, XMLLanguageService xmlLanguageService, int index, CodeAction... expected) throws BadLocationException { int offset = xml.indexOf('|'); - Range range = null; - if (offset != -1) { xml = xml.substring(0, offset) + xml.substring(offset + 1); } TextDocument document = new TextDocument(xml.toString(), fileURI != null ? fileURI : FILE_URI); + // Use range from the text (if marked by "|"-char or from diagnostics if (offset != -1) { Position position = document.positionAt(offset); range = new Range(position, position); - } else { + } else if (range == null && diagnostic != null) { range = diagnostic.getRange(); } + + // Otherwise, range is to be specified in parameters + assertNotNull(range, "Range cannot be null"); if (xmlLanguageService == null) { xmlLanguageService = new XMLLanguageService(); @@ -979,9 +986,11 @@ public static void assertCodeActions(List actual, CodeAction... expe ca.setTitle(""); if (ca.getDiagnostics() != null) { ca.getDiagnostics().forEach(d -> { - d.setSeverity(null); - d.setMessage(""); - d.setSource(null); + if (d != null) { + d.setSeverity(null); + d.setMessage(""); + d.setSource(null); + } }); } }); @@ -1114,6 +1123,10 @@ public static Either teOp(String uri, int s Collections.singletonList(te(startLine, startChar, endLine, endChar, newText)))); } + public static Either teOp(String uri, TextEdit... te) { + return Either.forLeft(new TextDocumentEdit(new VersionedTextDocumentIdentifier(uri, 0), Arrays.asList(te))); + } + // ------------------- Hover assert public static void assertHover(String value) throws BadLocationException { diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java index e4fad35d8..4d8841322 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2020 Red Hat Inc. and others. +* Copyright (c) 2020, 2023 Red Hat Inc. and others. * All rights reserved. This program and the accompanying materials * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v20.html @@ -50,6 +50,8 @@ import org.eclipse.lemminx.services.DocumentSymbolsResult; import org.eclipse.lemminx.services.SymbolInformationResult; import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant; +import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest; import org.eclipse.lemminx.services.extensions.completion.ICompletionParticipant; import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest; import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse; @@ -60,6 +62,7 @@ import org.eclipse.lemminx.settings.XMLSymbolFilter; import org.eclipse.lemminx.settings.XMLSymbolSettings; import org.eclipse.lemminx.utils.XMLBuilder; +import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; @@ -114,8 +117,21 @@ public ErrorParticipantLanguageService() { throw new RuntimeException("This participant is broken"); }); this.registerCodeActionParticipant((request, codeActions, cancelChecker) -> { + // This Code Action Participant should add its code actions based on diagnostic code Diagnostic diagnostic = request.getDiagnostic(); - codeActions.add(ca(diagnostic, te(0, 0, 0, 0, "a"))); + if (diagnostic != null) { + codeActions.add(ca(diagnostic, te(0, 0, 0, 0, "a"))); + } + }); + this.registerCodeActionParticipant(new ICodeActionParticipant () { + public void doCodeAction(ICodeActionRequest request, List codeActions, CancelChecker cancelChecker) { + // Nothing to do for a diagnostic, even if provided + } + public void doCodeActionUnconditional(ICodeActionRequest request, List codeActions, CancelChecker cancelChecker) { + // This Code Action Participant should add its code actions independently of + // diagnostic provided + codeActions.add(ca(null, te(0, 0, 0, 0, "b"))); + } }); this.registerCodeLensParticipant((request, lenses, cancelChecker) -> { @@ -367,7 +383,13 @@ public void findDocumentSymbols(DOMDocument document, DocumentSymbolsResult symb public void testCodeAction() throws BadLocationException { Diagnostic diagnostic = d(0, 0, 2, XMLSyntaxErrorCode.ElementUnterminated); testCodeActionsFor("", diagnostic, (String) null, null, new ErrorParticipantLanguageService(), - ca(diagnostic, te(0, 0, 0, 0, "a"))); + ca(diagnostic, te(0, 0, 0, 0, "a")), ca(null, te(0, 0, 0, 0, "b"))); + } + + @Test + public void testCodeActionNullDiagnostic() throws BadLocationException { + testCodeActionsFor("", r(0,0, 0, 0), (String) null, null, new ErrorParticipantLanguageService(), + ca(null, te(0, 0, 0, 0, "b"))); } @Test