From 8172067c6b44f382b81b2a74559ddda4d54977c3 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 26 Sep 2024 11:35:48 +0200 Subject: [PATCH] feat: provide LSP API support Signed-off-by: azerr --- docs/LSPApi.md | 93 +++++++ .../lsp4ij/LanguageServerFactory.java | 4 + .../lsp4ij/LanguageServerWrapper.java | 10 + .../client/features/AbstractLSPFeature.java | 41 ++++ .../client/features/LSPCompletionFeature.java | 21 ++ .../client/features/LSPDiagnosticFeature.java | 226 ++++++++++++++++++ .../client/features/LSPFormattingFeature.java | 26 ++ .../client/features/LSPServerFeatures.java | 101 ++++++++ .../quickfix/LSPLazyCodeActions.java | 4 +- .../diagnostics/LSPDiagnosticAnnotator.java | 158 +----------- .../diagnostics/LSPDiagnosticsForServer.java | 8 +- .../AbstractLSPFormattingService.java | 12 +- .../definition/LanguageServerDefinition.java | 23 +- .../ExtensionLanguageServerDefinition.java | 17 +- 14 files changed, 565 insertions(+), 179 deletions(-) create mode 100644 docs/LSPApi.md create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPFeature.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDiagnosticFeature.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPServerFeatures.java diff --git a/docs/LSPApi.md b/docs/LSPApi.md new file mode 100644 index 000000000..0fb6a22ef --- /dev/null +++ b/docs/LSPApi.md @@ -0,0 +1,93 @@ +# LSP server support + +The LSPCustomizedServerSupport API allows customizing the behavior of LSP features to customize: + +* [LSP completion support](#customize-lsp-completion-support) +* [LSP diagnostic support](#customize-lsp-diagnostic-support) +* [LSP formatting support](#customize-lsp-formatting-support) + +These custom supports are done: + +* by extending the default classes `LSPCustomized*Support` (e.g. creating a new class `MyLSPCustomizedFormattingSupport` that extends +[LSPCustomizedFormattingSupport](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/customization/LSPCustomizedFormattingSupport.java) to customize formatting support) +and overriding some methods to customize the behavior. +* registering your custom classes with `LanguageServerFactory#createServerSupport(@NotNull Project)`: + +```java +package my.language.server; + +import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.LanguageServerFactory; +import com.redhat.devtools.lsp4ij.client.features.LSPCustomizedServerSupport; +import org.jetbrains.annotations.NotNull; + +public class MyLanguageServerFactory implements LanguageServerFactory { + + @Override + public @NotNull LSPCustomizedServerSupport createServerSupport(@NotNull Project project) { + return new LSPCustomizedServerSupport(project) + .withCompletionSupport(new MyLSPCustomizedCompletionSupport()) // customize LSP completion support + .withDiagnosticSupport(new MyLSPCustomizedDiagnosticSupport()) // customize LSP diagnostic support + .withFormattingSupport(new MyLSPCustomizedFormattingSupport()); // customize LSP formatting support + } +} +``` + +## Customize LSP completion support + +TODO + +## Customize LSP diagnostic support + +Here is an example of code that avoids creating an IntelliJ annotation when the LSP diagnostic code is equal to `ignore`: + +```java +package my.language.server; + +import com.intellij.lang.annotation.HighlightSeverity; +import com.redhat.devtools.lsp4ij.client.features.LSPCustomizedDiagnosticSupport; +import org.eclipse.lsp4j.Diagnostic; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MyLSPCustomizedDiagnosticSupport extends LSPCustomizedDiagnosticSupport { + + @Override + public @Nullable HighlightSeverity getHighlightSeverity(@NotNull Diagnostic diagnostic) { + if (diagnostic.getCode() != null && + diagnostic.getCode().isLeft() && + "ignore".equals(diagnostic.getCode().getLeft())) { + // return a null HighlightSeverity when LSP diagnostic code is equals + // to 'ignore' to avoid creating an IntelliJ annotation + return null; + } + return super.getHighlightSeverity(diagnostic); + } + +} +``` + +## Customize LSP formatting support + +Here is an example of code that allows to execute the LSP formatter even if there is a specific formatter registered by an IntelliJ plugin + +TODO: revisit this API to manage range formatting too. + +```java +package my.language.server; + +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPCustomizedFormattingSupport; +import com.redhat.devtools.lsp4ij.client.features.LSPCustomizedServerSupport; +import org.jetbrains.annotations.NotNull; + +public class MyLSPCustomizedFormattingSupport extends LSPCustomizedFormattingSupport { + + @Override + public boolean canSupportFormatting(@NotNull PsiFile file, boolean hasCustomFormatter) { + // By default, LSPCustomizedFormattingSupport return false if it has custom formatter with psi + // returns true even if there is custom formatter + return true; + } +} +``` \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java index e7831e973..3024ea3a1 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java @@ -13,6 +13,7 @@ import com.intellij.openapi.project.Project; import com.redhat.devtools.lsp4ij.client.LanguageClientImpl; import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider; +import com.redhat.devtools.lsp4ij.client.features.LSPServerFeatures; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; @@ -50,4 +51,7 @@ public interface LanguageServerFactory { return LanguageServer.class; } + @NotNull default LSPServerFeatures createServerSupport(@NotNull Project project) { + return new LSPServerFeatures(project); + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java index 16f714eb1..75d165b80 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java @@ -31,6 +31,7 @@ import com.redhat.devtools.lsp4ij.lifecycle.LanguageServerLifecycleManager; import com.redhat.devtools.lsp4ij.lifecycle.NullLanguageServerLifecycleManager; import com.redhat.devtools.lsp4ij.server.*; +import com.redhat.devtools.lsp4ij.client.features.LSPServerFeatures; import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -108,6 +109,8 @@ public class LanguageServerWrapper implements Disposable { private FileOperationsManager fileOperationsManager; + private LSPServerFeatures serverSupport; + /* Backwards compatible constructor */ public LanguageServerWrapper(@NotNull Project project, @NotNull LanguageServerDefinition serverDefinition) { this(project, serverDefinition, null); @@ -1174,4 +1177,11 @@ public boolean isSignatureTriggerCharactersSupported(String charTyped) { } return triggerCharacters.contains(charTyped); } + + public LSPServerFeatures getServerSupport() { + if (serverSupport == null) { + serverSupport = getServerDefinition().createServerSupport(getProject()); + } + return serverSupport; + } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPFeature.java new file mode 100644 index 000000000..f003ca572 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPFeature.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Abstract class for any LSP customization feature support. + */ +@ApiStatus.Experimental +public abstract class AbstractLSPFeature { + + private LSPServerFeatures serverSupport; + + /** + * Returns the LSP server support. + * + * @return the LSP server support. + */ + public @NotNull LSPServerFeatures getServerSupport() { + return serverSupport; + } + + /** + * Set the LSP server support. + * + * @param serverSupport the LSP server support. + */ + public void setServerSupport(@NotNull LSPServerFeatures serverSupport) { + this.serverSupport = serverSupport; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java new file mode 100644 index 000000000..8ca690654 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import org.jetbrains.annotations.ApiStatus; + +/** + * LSP customization completion support. + */ +@ApiStatus.Experimental +public class LSPCompletionFeature extends AbstractLSPFeature { + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDiagnosticFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDiagnosticFeature.java new file mode 100644 index 000000000..fb8afb1ba --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDiagnosticFeature.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.lang.annotation.AnnotationBuilder; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import com.redhat.devtools.lsp4ij.features.diagnostics.SeverityMapping; +import com.redhat.devtools.lsp4ij.hint.LSPNavigationLinkHandler; +import com.redhat.devtools.lsp4ij.internal.StringUtils; +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * LSP customization diagnostic support. + */ +@ApiStatus.Experimental +public class LSPDiagnosticFeature extends AbstractLSPFeature { + + /** + * Create an IntelliJ annotation in the given holder by using given LSP diagnostic and fixes. + * @param diagnostic the LSP diagnostic. + * @param document the document. + * @param fixes the fixes coming from LSP CodeAction. + * @param holder the annotation holder where annotation must be registered. + */ + public void createAnnotation(@NotNull Diagnostic diagnostic, + @NotNull Document document, + List fixes, + @NotNull AnnotationHolder holder) { + + // Get the text range from the given LSP diagnostic range. + // Since IJ cannot highlight an error when the start/end range offset are the same + // the method LSPIJUtils.toTextRange is called with adjust, in other words when start/end range offset are the same: + // - when the offset is at the end of the line, the method returns a text range with the same offset, + // and annotation must be created with Annotation#setAfterEndOfLine(true). + // - when the offset is inside the line, the end offset is incremented. + TextRange range = LSPIJUtils.toTextRange(diagnostic.getRange(), document, null, true); + if (range == null) { + // Language server reports invalid diagnostic, ignore it. + return; + } + + HighlightSeverity severity = getHighlightSeverity(diagnostic); + if (severity == null) { + // Ignore the diagnostic + return; + } + + // Collect information required to create Intellij Annotations + String message = getMessage(diagnostic); + + // Create IntelliJ Annotation from the given LSP diagnostic + AnnotationBuilder builder = holder + .newAnnotation(severity, message) + .tooltip(getToolTip(diagnostic)) + .range(range); + if (range.getStartOffset() == range.getEndOffset()) { + // Show the annotation at the end of line. + builder.afterEndOfLine(); + } + + // Update highlight type from the diagnostic tags + ProblemHighlightType highlightType = getProblemHighlightType(diagnostic.getTags()); + if (highlightType != null) { + builder.highlightType(highlightType); + } + + // Register lazy quick fixes + for (IntentionAction fix : fixes) { + builder.withFix(fix); + } + builder.create(); + } + + /** + * Returns the IntelliJ {@link HighlightSeverity} from the given diagnostic and null otherwise. + * + *

+ * If null is returned, the diagnostic will be ignored. + *

+ * + * @param diagnostic the LSP diagnostic. + * @return the IntelliJ {@link HighlightSeverity} from the given diagnostic and null otherwise. + */ + @Nullable + public HighlightSeverity getHighlightSeverity(@NotNull Diagnostic diagnostic) { + return SeverityMapping.toHighlightSeverity(diagnostic.getSeverity()); + } + + /** + * Returns the message of the given diagnostic. + * + * @param diagnostic the LSP diagnostic. + * @return the message of the given diagnostic. + */ + @NotNull + public String getMessage(@NotNull Diagnostic diagnostic) { + return diagnostic.getMessage(); + } + + /** + * Returns the annotation tooltip from the given LSP diagnostic. + * + * @param diagnostic the LSP diagnostic. + * @return the annotation tooltip from the given LSP diagnostic. + */ + @NotNull + public String getToolTip(@NotNull Diagnostic diagnostic) { + // message + StringBuilder tooltip = new StringBuilder(""); + tooltip.append(StringUtil.escapeXmlEntities(diagnostic.getMessage())); + // source + tooltip.append(" "); + String source = diagnostic.getSource(); + if (StringUtils.isNotBlank(source)) { + tooltip.append(source); + } + // error code + Either code = diagnostic.getCode(); + if (code != null) { + String errorCode = code.isLeft() ? code.getLeft() : code.isRight() ? String.valueOf(code.getRight()) : null; + if (StringUtils.isNotBlank(errorCode)) { + tooltip.append(" ("); + String href = diagnostic.getCodeDescription() != null ? diagnostic.getCodeDescription().getHref() : null; + addLink(errorCode, href, tooltip); + tooltip.append(")"); + } + } + // Diagnostic related information + List information = diagnostic.getRelatedInformation(); + if (information != null) { + tooltip.append("
    "); + for (var item : information) { + String message = item.getMessage(); + tooltip.append("
  • "); + Location location = item.getLocation(); + if (location != null) { + String fileName = getFileName(location); + String fileUrl = LSPNavigationLinkHandler.toNavigationUrl(location); + addLink(fileName, fileUrl, tooltip); + tooltip.append(": "); + } + tooltip.append(message); + tooltip.append("
  • "); + } + tooltip.append("
"); + } + tooltip.append("
"); + return tooltip.toString(); + } + + @NotNull + private static String getFileName(Location location) { + String fileUri = location.getUri(); + int index = fileUri.lastIndexOf('/'); + String fileName = fileUri.substring(index + 1); + StringBuilder result = new StringBuilder(fileName); + Range range = location.getRange(); + if (range != null) { + result.append("("); + result.append(range.getStart().getLine()); + result.append(":"); + result.append(range.getStart().getCharacter()); + result.append(", "); + result.append(range.getEnd().getLine()); + result.append(":"); + result.append(range.getEnd().getCharacter()); + result.append(")"); + } + return fileName; + } + + private static void addLink(String text, String href, StringBuilder tooltip) { + boolean hasHref = StringUtils.isNotBlank(href); + if (hasHref) { + tooltip.append(""); + } + tooltip.append(text); + if (hasHref) { + tooltip.append(""); + } + } + + /** + * Returns the {@link ProblemHighlightType} from the given tags and null otherwise. + * + * @param tags the diagnostic tags. + * @return the {@link ProblemHighlightType} from the given tags and null otherwise. + */ + @Nullable + public ProblemHighlightType getProblemHighlightType(@Nullable List tags) { + if (tags == null || tags.isEmpty()) { + return null; + } + if (tags.contains(DiagnosticTag.Unnecessary)) { + return ProblemHighlightType.LIKE_UNUSED_SYMBOL; + } + if (tags.contains(DiagnosticTag.Deprecated)) { + return ProblemHighlightType.LIKE_DEPRECATED; + } + return null; + } + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java new file mode 100644 index 000000000..3cf34bca9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP formatting feature. + */ +@ApiStatus.Experimental +public class LSPFormattingFeature extends AbstractLSPFeature { + + public boolean canSupportFormatting(@NotNull PsiFile file, boolean hasCustomFormatter) { + return !hasCustomFormatter; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPServerFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPServerFeatures.java new file mode 100644 index 000000000..bc0580873 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPServerFeatures.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP server features. + */ +@ApiStatus.Experimental +public class LSPServerFeatures { + + private final @NotNull Project project; + + private LSPCompletionFeature completionSupport; + + private LSPDiagnosticFeature diagnosticSupport; + + private LSPFormattingFeature formattingSupport; + + public LSPServerFeatures(@NotNull Project project) { + this.project = project; + } + + public @NotNull Project getProject() { + return project; + } + + @NotNull + public final LSPCompletionFeature getCompletionSupport() { + if (completionSupport == null) { + withCompletionSupport(new LSPCompletionFeature()); + } + return completionSupport; + } + + /** + * Initialize the LSP customization completion support. + * + * @param completionSupport the LSP customization completion support. + * + * @return the LSP server support. + */ + public LSPServerFeatures withCompletionSupport(@NotNull LSPCompletionFeature completionSupport) { + completionSupport.setServerSupport(this); + this.completionSupport = completionSupport; + return this; + } + + @NotNull + public final LSPDiagnosticFeature getDiagnosticSupport() { + if (diagnosticSupport == null) { + withDiagnosticSupport(new LSPDiagnosticFeature()); + } + return diagnosticSupport; + } + + /** + * Initialize the LSP customization diagnostic support. + * + * @param diagnosticSupport the LSP customization diagnostic support. + * + * @return the LSP server support. + */ + public LSPServerFeatures withDiagnosticSupport(@NotNull LSPDiagnosticFeature diagnosticSupport) { + diagnosticSupport.setServerSupport(this); + this.diagnosticSupport = diagnosticSupport; + return this; + } + + @NotNull + public final LSPFormattingFeature getFormattingSupport() { + if (formattingSupport == null) { + withFormattingSupport(new LSPFormattingFeature()); + } + return formattingSupport; + } + + /** + * Initialize the LSP customization formatting support. + * + * @param formattingSupport the LSP customization formatting support. + * + * @return the LSP server support. + */ + public LSPServerFeatures withFormattingSupport(@NotNull LSPFormattingFeature formattingSupport) { + formattingSupport.setServerSupport(this); + this.formattingSupport = formattingSupport; + return this; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/quickfix/LSPLazyCodeActions.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/quickfix/LSPLazyCodeActions.java index 6466aeccc..c91536c8d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/quickfix/LSPLazyCodeActions.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/quickfix/LSPLazyCodeActions.java @@ -56,7 +56,7 @@ public class LSPLazyCodeActions implements LSPLazyCodeActionProvider { private final LanguageServerItem languageServer; // List of lazy code actions - private final List codeActions; + private final List codeActions; // LSP code actions request used to load code action for the diagnostic. private CompletableFuture> lspCodeActionRequest = null; @@ -181,7 +181,7 @@ private static CodeActionParams createCodeActionParams(List diagnost * * @return the list of lazy code actions. */ - public List getCodeActions() { + public List getCodeActions() { return codeActions; } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticAnnotator.java b/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticAnnotator.java index a1ee8724d..f2880107f 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticAnnotator.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticAnnotator.java @@ -14,25 +14,20 @@ package com.redhat.devtools.lsp4ij.features.diagnostics; import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInspection.ProblemHighlightType; -import com.intellij.lang.annotation.*; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.ExternalAnnotator; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPVirtualFileData; import com.redhat.devtools.lsp4ij.LanguageServersRegistry; import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPExternalAnnotator; -import com.redhat.devtools.lsp4ij.features.codeAction.LSPLazyCodeActionIntentionAction; -import com.redhat.devtools.lsp4ij.hint.LSPNavigationLinkHandler; -import com.redhat.devtools.lsp4ij.internal.StringUtils; -import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.jsonrpc.messages.Either; +import com.redhat.devtools.lsp4ij.client.features.LSPDiagnosticFeature; +import org.eclipse.lsp4j.Diagnostic; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -95,148 +90,9 @@ private static void createAnnotation(@NotNull Diagnostic diagnostic, @Nullable PsiFile file, @NotNull LSPDiagnosticsForServer diagnosticsForServer, @NotNull AnnotationHolder holder) { - // Get the text range from the given LSP diagnostic range. - // Since IJ cannot highlight an error when the start/end range offset are the same - // the method LSPIJUtils.toTextRange is called with adjust, in other words when start/end range offset are the same: - // - when the offset is at the end of the line, the method returns a text range with the same offset, - // and annotation must be created with Annotation#setAfterEndOfLine(true). - // - when the offset is inside the line, the end offset is incremented. - TextRange range = LSPIJUtils.toTextRange(diagnostic.getRange(), document, null, true); - if (range == null) { - // Language server reports invalid diagnostic, ignore it. - return; - } - - // Collect information required to create Intellij Annotations - HighlightSeverity severity = SeverityMapping.toHighlightSeverity(diagnostic.getSeverity()); - String message = diagnostic.getMessage(); - - // Create IntelliJ Annotation from the given LSP diagnostic - AnnotationBuilder builder = holder - .newAnnotation(severity, message) - .tooltip(getToolTip(diagnostic)) - .range(range); - if (range.getStartOffset() == range.getEndOffset()) { - // Show the annotation at the end of line. - builder.afterEndOfLine(); - } - - // Update highlight type from the diagnostic tags - ProblemHighlightType highlightType = getProblemHighlightType(diagnostic.getTags()); - if (highlightType != null) { - builder.highlightType(highlightType); - } - - // Register lazy quick fixes - List fixes = diagnosticsForServer.getQuickFixesFor(diagnostic); - for (IntentionAction fix : fixes) { - builder.withFix(fix); - } - builder.create(); - } - - /** - * Returns the {@link ProblemHighlightType} from the given tags and null otherwise. - * - * @param tags the diagnostic tags. - * @return the {@link ProblemHighlightType} from the given tags and null otherwise. - */ - @Nullable - private static ProblemHighlightType getProblemHighlightType(@Nullable List tags) { - if (tags == null || tags.isEmpty()) { - return null; - } - if (tags.contains(DiagnosticTag.Unnecessary)) { - return ProblemHighlightType.LIKE_UNUSED_SYMBOL; - } - if (tags.contains(DiagnosticTag.Deprecated)) { - return ProblemHighlightType.LIKE_DEPRECATED; - } - return null; - } - - /** - * Returns the annotation tooltip from the given LSP diagnostic. - * - * @param diagnostic the LSP diagnostic. - * @return the annotation tooltip from the given LSP diagnostic. - */ - private static String getToolTip(Diagnostic diagnostic) { - // message - StringBuilder tooltip = new StringBuilder(""); - tooltip.append(StringUtil.escapeXmlEntities(diagnostic.getMessage())); - // source - tooltip.append(" "); - String source = diagnostic.getSource(); - if (StringUtils.isNotBlank(source)) { - tooltip.append(source); - } - // error code - Either code = diagnostic.getCode(); - if (code != null) { - String errorCode = code.isLeft() ? code.getLeft() : code.isRight() ? String.valueOf(code.getRight()) : null; - if (StringUtils.isNotBlank(errorCode)) { - tooltip.append(" ("); - String href = diagnostic.getCodeDescription() != null ? diagnostic.getCodeDescription().getHref() : null; - addLink(errorCode, href, tooltip); - tooltip.append(")"); - } - } - // Diagnostic related information - List informations = diagnostic.getRelatedInformation(); - if (informations != null) { - tooltip.append("
    "); - for (var information : informations) { - String message = information.getMessage(); - tooltip.append("
  • "); - Location location = information.getLocation(); - if (location != null) { - String fileName = getFileName(location); - String fileUrl = LSPNavigationLinkHandler.toNavigationUrl(location); - addLink(fileName, fileUrl, tooltip); - tooltip.append(": "); - } - tooltip.append(message); - tooltip.append("
  • "); - } - tooltip.append("
"); - } - tooltip.append("
"); - return tooltip.toString(); - } - - @NotNull - private static String getFileName(Location location) { - String fileUri = location.getUri(); - int index = fileUri.lastIndexOf('/'); - String fileName = fileUri.substring(index + 1); - StringBuilder result = new StringBuilder(fileName); - Range range = location.getRange(); - if (range != null) { - result.append("("); - result.append(range.getStart().getLine()); - result.append(":"); - result.append(range.getStart().getCharacter()); - result.append(", "); - result.append(range.getEnd().getLine()); - result.append(":"); - result.append(range.getEnd().getCharacter()); - result.append(")"); - } - return fileName; - } - - private static void addLink(String text, String href, StringBuilder tooltip) { - boolean hasHref = StringUtils.isNotBlank(href); - if (hasHref) { - tooltip.append(""); - } - tooltip.append(text); - if (hasHref) { - tooltip.append(""); - } + LSPDiagnosticFeature diagnosticSupport = diagnosticsForServer.getServerSupport().getDiagnosticSupport(); + List fixes = diagnosticsForServer.getQuickFixesFor(diagnostic); + diagnosticSupport.createAnnotation(diagnostic, document, fixes, holder); } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticsForServer.java b/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticsForServer.java index 012c7f179..789b4373a 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticsForServer.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/diagnostics/LSPDiagnosticsForServer.java @@ -13,11 +13,12 @@ *******************************************************************************/ package com.redhat.devtools.lsp4ij.features.diagnostics; +import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.LanguageServerWrapper; -import com.redhat.devtools.lsp4ij.features.codeAction.LSPLazyCodeActionIntentionAction; import com.redhat.devtools.lsp4ij.features.codeAction.quickfix.LSPLazyCodeActions; +import com.redhat.devtools.lsp4ij.client.features.LSPServerFeatures; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; @@ -53,6 +54,9 @@ public LSPDiagnosticsForServer(LanguageServerItem languageServer, VirtualFile fi this.diagnostics = Collections.emptyMap(); } + public LSPServerFeatures getServerSupport() { + return languageServer.getServerWrapper().getServerSupport(); + } /** * Update the new LSP published diagnosics. * @@ -134,7 +138,7 @@ public Set getDiagnostics() { * @param diagnostic the diagnostic. * @return Intellij quickfixes for the given diagnostic if there available. */ - public List getQuickFixesFor(Diagnostic diagnostic) { + public List getQuickFixesFor(Diagnostic diagnostic) { boolean codeActionSupported = isCodeActionSupported(languageServer.getServerWrapper()); if (!codeActionSupported || diagnostics.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/AbstractLSPFormattingService.java b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/AbstractLSPFormattingService.java index bbab2ee5b..2fa2b1e13 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/AbstractLSPFormattingService.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/AbstractLSPFormattingService.java @@ -19,10 +19,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; -import com.redhat.devtools.lsp4ij.LSPFileSupport; -import com.redhat.devtools.lsp4ij.LSPIJUtils; -import com.redhat.devtools.lsp4ij.LanguageServersRegistry; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.*; import org.eclipse.lsp4j.ServerCapabilities; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -118,9 +115,14 @@ public final boolean canFormat(@NotNull PsiFile file) { return false; } // Check if the file can support formatting / range formatting + boolean hasCustomFormatter = !LanguageFormatting.INSTANCE.allForLanguage(file.getLanguage()).isEmpty(); Project project = file.getProject(); return LanguageServiceAccessor.getInstance(project) - .hasAny(file.getVirtualFile(), ls -> canSupportFormatting(ls.getServerCapabilitiesSync())); + .hasAny(file.getVirtualFile(), ls -> canSupportFormatting1(ls, file, hasCustomFormatter) && canSupportFormatting(ls.getServerCapabilitiesSync())); + } + + private boolean canSupportFormatting1(LanguageServerWrapper ls, PsiFile file, boolean hasCustomFormatter) { + return ls.getServerSupport().getFormattingSupport().canSupportFormatting(file, hasCustomFormatter); } private static TextRange getFormattingRange(AsyncFormattingRequest formattingRequest) { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java index 2e3fe61b0..0e0a898a8 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java @@ -22,7 +22,6 @@ import com.intellij.openapi.util.Pair; import com.redhat.devtools.lsp4ij.LanguageServerEnablementSupport; import com.redhat.devtools.lsp4ij.LanguageServerFactory; -import com.redhat.devtools.lsp4ij.client.LanguageClientImpl; import com.redhat.devtools.lsp4ij.features.semanticTokens.DefaultSemanticTokensColorsProvider; import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -43,12 +42,10 @@ public abstract class LanguageServerDefinition implements LanguageServerFactory, private static final int DEFAULT_LAST_DOCUMENTED_DISCONNECTED_TIMEOUT = 5; - private static SemanticTokensColorsProvider DEFAULT_SEMANTIC_TOKENS_COLORS_PROVIDER = new DefaultSemanticTokensColorsProvider(); + private static final SemanticTokensColorsProvider DEFAULT_SEMANTIC_TOKENS_COLORS_PROVIDER = new DefaultSemanticTokensColorsProvider(); - private final @NotNull - String id; - private final @NotNull - String name; + private final @NotNull String id; + private final @NotNull String name; private final boolean isSingleton; private final String description; private final int lastDocumentDisconnectedTimeout; @@ -110,7 +107,7 @@ public String getId() { */ @NotNull public String getDisplayName() { - return name != null ? name : id; + return name; } /** @@ -199,22 +196,12 @@ public List, String>> getFilenameMatcherMappings() { return null; } - @Override - public @NotNull LanguageClientImpl createLanguageClient(@NotNull Project project) { - return new LanguageClientImpl(project); - } - - @Override - public @NotNull Class getServerInterface() { - return LanguageServer.class; - } - public Launcher.Builder createLauncherBuilder() { return new Launcher.Builder<>(); } public boolean supportsCurrentEditMode(@NotNull Project project) { - return project != null && (supportsLightEdit || !LightEdit.owns(project)); + return (supportsLightEdit || !LightEdit.owns(project)); } public Icon getIcon() { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/extension/ExtensionLanguageServerDefinition.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/extension/ExtensionLanguageServerDefinition.java index daf9500a3..18ce8b63d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/extension/ExtensionLanguageServerDefinition.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/extension/ExtensionLanguageServerDefinition.java @@ -21,6 +21,7 @@ import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; import com.redhat.devtools.lsp4ij.internal.StringUtils; import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider; +import com.redhat.devtools.lsp4ij.client.features.LSPServerFeatures; import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; @@ -72,6 +73,20 @@ public ExtensionLanguageServerDefinition(@NotNull ServerExtensionPointBean eleme return languageClient; } + @Override + public @NotNull LSPServerFeatures createServerSupport(@NotNull Project project) { + LSPServerFeatures serverSupport = null; + try { + serverSupport = getFactory().createServerSupport(project); + } catch (Exception e) { + LOGGER.warn("Exception occurred while creating an instance of the LSP server support", e); //$NON-NLS-1$ + } + if (serverSupport == null) { + serverSupport = super.createServerSupport(project); + } + return serverSupport; + } + @SuppressWarnings("unchecked") @Override public @NotNull Class getServerInterface() { @@ -146,7 +161,7 @@ private synchronized Icon findIcon() { } @Override - public SemanticTokensColorsProvider getSemanticTokensColorsProvider() { + public @NotNull SemanticTokensColorsProvider getSemanticTokensColorsProvider() { return super.getSemanticTokensColorsProvider(); } }