diff --git a/docs/LSPApi.md b/docs/LSPApi.md new file mode 100644 index 000000000..d532a2b88 --- /dev/null +++ b/docs/LSPApi.md @@ -0,0 +1,496 @@ +## LSP Client Features + +The [LSPClientFeatures](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java) API allows you to customize the behavior of LSP features, including: + +- [LSP codeAction feature](#lsp-codeAction-feature) +- [LSP codeLens feature](#lsp-codeLens-feature) +- [LSP color feature](#lsp-color-feature) +- [LSP completion feature](#lsp-completion-feature) +- [LSP declaration feature](#lsp-declaration-feature) +- [LSP definition feature](#lsp-definition-feature) +- [LSP diagnostic feature](#lsp-diagnostic-feature) +- [LSP documentHighlight feature](#lsp-documentHighlight-feature) +- [LSP documentLink feature](#lsp-documentLink-feature) +- [LSP documentSymbol feature](#lsp-documentSymbol-feature) +- [LSP foldingRange feature](#lsp-foldingRange-feature) +- [LSP formatting feature](#lsp-formatting-feature) +- [LSP hover feature](#lsp-hover-feature) +- [LSP implementation feature](#lsp-implementation-feature) +- [LSP inlayHint feature](#lsp-inlayHint-feature) +- [LSP references feature](#lsp-references-feature) +- [LSP rename feature](#lsp-rename-feature) +- [LSP semanticTokens feature](#lsp-semanticTokens-feature) +- [LSP signatureHelp feature](#lsp-signatureHelp-feature) +- [LSP typeDefinition feature](#lsp-typeDefinition-feature) +- [LSP usage feature](#lsp-usage-feature) +- [LSP workspace symbol feature](#lsp-workspace-symbol-feature) + +You can extend these default features by: + +- Creating custom classes that extend the `LSP*Feature` classes (e.g., creating a class `MyLSPFormattingFeature` that extends the [LSPFormattingFeature](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java) to customize formatting support) and overriding specific methods to modify behavior. +- Registering your custom classes using `LanguageServerFactory#createClientFeatures()`. + +```java +package my.language.server; + +import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.LanguageServerFactory; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; +import org.jetbrains.annotations.NotNull; + +public class MyLanguageServerFactory implements LanguageServerFactory { + + @Override + public @NotNull LSPClientFeatures createClientFeatures() { + return new LSPClientFeatures() + .setCompletionFeature(new MyLSPCompletionFeature()) // customize LSP completion feature + .setDiagnosticFeature(new MyLSPDiagnosticFeature()) // customize LSP diagnostic feature + .setFormattingFeature(new MyLSPFormattingFeature()); // customize LSP formatting feature + } +} +``` + +| API | Description | Default Behaviour | +|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-------------------| +| Project getProject() | Returns the project. | | +| LanguageServerDefinition getServerDefinition() | Returns the language server definition. | | +| boolean isServerDefinition(@NotNull String languageServerId) | Returns true if the given language server id matches the server definition and false otherwise. | | +| ServerStatus getServerStatus() | Returns the server status. | | +| LanguageServer getLanguageServer() | Returns the LSP4J language server. | | +| boolean isServerStoppingRequiredWhenFilesClosed() | Returns `true` if server stopping required when files closed and `false` otherwise. | `true` | + +```java +package my.language.server; + +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; + +public class MyLSPClientFeatures extends LSPClientFeatures { + + @Override + public boolean isServerStoppingRequiredWhenFilesClosed() { + // Don't stop the language server when all files closed + return false; + } +} +``` + +```java +package my.language.server; + +import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.LanguageServerFactory; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; +import org.jetbrains.annotations.NotNull; + +public class MyLanguageServerFactory implements LanguageServerFactory { + + @Override + public @NotNull LSPClientFeatures createClientFeatures() { + return new MyLSPClientFeatures() // customize LSP general features + .setCompletionFeature(new MyLSPCompletionFeature()) // customize LSP completion feature + .setDiagnosticFeature(new MyLSPDiagnosticFeature()) // customize LSP diagnostic feature + .setFormattingFeature(new MyLSPFormattingFeature()); // customize LSP formatting feature + } +} +``` + +## LSP Base Feature + +| API | Description | Default Behaviour | +|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-------------------| +| Project getProject() | Returns the project. | | +| LanguageServerDefinition getServerDefinition() | Returns the language server definition. | | +| boolean isServerDefinition(@NotNull String languageServerId) | Returns true if the given language server id matches the server definition and false otherwise. | | +| ServerStatus getServerStatus() | Returns the server status. | | +| LanguageServer getLanguageServer() | Returns the LSP4J language server. | | + +### Disable a given LSP Feature + +All `LSP*Feature` classes extend [AbstractLSPDocumentFeature](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPAbstractFeature.java), which declares the `isEnabled` method that returns `true` by default: + +```java +public boolean isEnabled(@NotNull PsiFile file) { + return true; +} +``` + +By overriding this method, you can return `false` to disable a given LSP feature for your language server. This method is called before starting the language server. + +### Supported LSP Feature + +All `LSP*Feature` classes extend [AbstractLSPDocumentFeature](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPAbstractFeature.java), which declares the `isSupported` method that uses the server capabilities: + +```java +public boolean isSupported(@NotNull PsiFile file) { + +} +``` + +This method is called after starting the language server to collect the LSP server capabilities. + +### Project Access + +If you need the `Project` instance in your LSP feature to retrieve the settings of the current project, you can use the `getProject()` getter method. + +```java +public class MyLSPCodeLensFeature extends LSPCodeLensFeature { + + @Override + public boolean isEnabled(@NotNull PsiFile file) { + return MySettings.getInstance(super.getProject()).isCodeLensEnabled(); + } + +} +``` + +### Server Status + +If you need to check the server status, you can use the `getServerStatus()` getter method. + +```java +public class MyLSPCodeLensFeature extends LSPCodeLensFeature { + + @Override + public boolean isEnabled(@NotNull PsiFile file) { + // Here, code lens will be disabled if the language server is not started + // (the LSP CodeLens will not force the start of the language server) + var serverStatus = super.getServerStatus(); + return serverStatus == ServerStatus.starting || serverStatus == ServerStatus.started; + } + +} +``` + +## LSP CodeAction Feature + +| API | Description | Default Behaviour | +|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| String getText(CodeAction codeAction) | Returns the IntelliJ intention action text from the given LSP code action and `null` to ignore the code action. | `codeAction.getTitle()` | +| String getFamilyName(CodeAction codeAction) | Returns the IntelliJ intention action family name from the given LSP code action. | Label of `CodeActionKind` | +| String getText(Command command) | Returns the IntelliJ intention action text from the given LSP command and `null` to ignore the command. | `command.getTitle()` | +| String getFamilyName(Command command) | Returns the IntelliJ intention action family name from the given LSP command. | "LSP Command" | + +## LSP CodeLens Feature + +| API | Description | Default Behaviour | +|-----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| CodeVisionEntry createCodeVisionEntry(CodeLens codeLens, String providerId, LSPCodeLensContext codeLensContext) | Creates an IntelliJ `CodeVisionEntry` from the given LSP `CodeLens` and `null` otherwise (to ignore the LSP `CodeLens`). | | +| String getText(CodeLens codeLens) | Returns the code vision entry text from the LSP `CodeLens` and `null` otherwise (to ignore the LSP `CodeLens`). | `codeLens.getCommand().getTitle()` | + +Here is an example of code that avoids creating an IntelliJ `CodeVisionEntry` when the LSP `CodeLens` command is equal to `Run`: + +```java +package my.language.server; + +import com.redhat.devtools.lsp4ij.client.features.LSPCodeLensFeature; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MyLSPCodeLensFeature extends LSPCodeLensFeature { + + @Override + @Nullable + public String getText(CodeLens codeLens) { + Command command = codeLens.getCommand(); + if ("Run".equals(command)) { + return null; + } + return super.getText(codeLens); + } + +} +``` + +## LSP Color Feature + +| API | Description | Default Behaviour | +|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Completion Feature + +| API | Description | Default Behaviour | +|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| LookupElement createLookupElement(CompletionItem item, LSPCompletionContext context) | Create a completion lookup element from the given LSP completion item and context and null otherwise. | | +| void renderLookupElement(LookupElementPresentation presentation, CompletionItem item) | Update the given IntelliJ lookup element presentation with the given LSP completion item. | | +| String getItemText(CompletionItem item) | Returns the IntelliJ lookup item text from the given LSP completion item and null otherwise. | `item.getLabel()` | +| String getTypeText(CompletionItem item) | Returns the IntelliJ lookup type text from the given LSP completion item and null otherwise. | `item.getDetail()` | +| Icon getIcon(CompletionItem item) | Returns the IntelliJ lookup icon from the given LSP completion item and null otherwise. | default icon from `item.getKind()` | +| boolean isStrikeout(CompletionItem item) | Returns true if the IntelliJ lookup is strike out and false otherwise. | use `item.getDeprecated()` or `item.getTags().contains(CompletionItemTag.Deprecated)` | +| String getTailText(CompletionItem item) | Returns the IntelliJ lookup tail text from the given LSP completion item and null otherwise. | `item.getLabelDetails().getDetail()` | +| boolean isItemTextBold(CompletionItem item) | Returns the IntelliJ lookup item text bold from the given LSP completion item and null otherwise. | `item.getKind() == CompletionItemKind.Keyword` | + +## LSP Declaration Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Definition Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Diagnostic Feature + +| API | Description | Default Behaviour | +|-----------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| void createAnnotation(Diagnostic diagnostic, Document document, List fixes, AnnotationHolder holder) | Creates an IntelliJ annotation in the given holder using the provided LSP diagnostic and fixes. | | +| HighlightSeverity getHighlightSeverity(Diagnostic diagnostic) | Returns the IntelliJ `HighlightSeverity` from the given diagnostic and `null` otherwise. | | +| String getMessage(Diagnostic diagnostic) | Returns the message of the given diagnostic. | | +| String getToolTip(Diagnostic diagnostic) | Returns the annotation tooltip from the given LSP diagnostic. | | +| ProblemHighlightType getProblemHighlightType(List tags) | Returns the `ProblemHighlightType` from the given tags and `null` otherwise. | | + +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.LSPDiagnosticFeature; +import org.eclipse.lsp4j.Diagnostic; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MyLSPDiagnosticFeature extends LSPDiagnosticFeature { + + @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); + } + +} +``` + +## LSP DocumentHighlight Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP DocumentLink Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP DocumentSymbol Feature + +| API | Description | Default Behaviour | +|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| StructureViewTreeElement getStructureViewTreeElement(DocumentSymbolData documentSymbol) | | | +| String getPresentableText(DocumentSymbol documentSymbol, PsiFile psiFile) | | | +| Icon getIcon(DocumentSymbol documentSymbol, PsiFile psiFile) | | | +| String getPresentableText(DocumentSymbol documentSymbol, PsiFile psiFile, boolean unused) | | | +| void navigate(DocumentSymbol documentSymbol, PsiFile psiFile, boolean requestFocus) | | | | | | +| boolean canNavigate(DocumentSymbol documentSymbol, PsiFile psiFile) | | | + +Here is an example of code to customize the document symbol used in `Structure`: + +```java +package com.redhat.devtools.lsp4ij.client; + +import com.intellij.ide.structureView.StructureViewTreeElement; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPDocumentSymbolFeature; +import com.redhat.devtools.lsp4ij.features.documentSymbol.DocumentSymbolData; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.SymbolKind; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public class MyLSPDocumentSymbolFeature extends LSPDocumentSymbolFeature { + + @Override + public @Nullable StructureViewTreeElement getStructureViewTreeElement(DocumentSymbolData documentSymbol) { + if ("ignore".equals(documentSymbol.getDocumentSymbol().getName())) { + // Ignore document symbol with "ignore" name. + return null; + } + return super.getStructureViewTreeElement(documentSymbol); + } + + @Override + public @Nullable Icon getIcon(@NotNull DocumentSymbol documentSymbol, @NotNull PsiFile psiFile, boolean unused) { + if (documentSymbol.getKind() == SymbolKind.Class) { + // Returns custom icon for 'Class' symbol kind + return ...; + } + return super.getIcon(documentSymbol, psiFile, unused); + } + + @Override + public @Nullable String getPresentableText(@NotNull DocumentSymbol documentSymbol, @NotNull PsiFile psiFile) { + if (documentSymbol.getKind() == SymbolKind.Class) { + // Returns custom presentable text for 'Class' symbol kind + return ...; + } + return super.getPresentableText(documentSymbol, psiFile); + } + + @Override + public @Nullable String getLocationString(@NotNull DocumentSymbol documentSymbol, @NotNull PsiFile psiFile) { + if (documentSymbol.getKind() == SymbolKind.Class) { + // Returns custom location string for 'Class' symbol kind + return ...; + } + return super.getLocationString(documentSymbol, psiFile); + } +} +``` + +## LSP FoldingRange Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Formatting Feature + +| API | Description | Default Behaviour | +|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| boolean isExistingFormatterOverrideable(PsiFile file) | Returns `true` if existing formatters are overrideable and `false` otherwise. | `false` | + +Here is an example of code that allows executing the LSP formatter even if there is a specific formatter registered by an IntelliJ plugin: + +```java +package my.language.server; + +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature; +import org.jetbrains.annotations.NotNull; + +public class MyLSPFormattingFeature extends LSPFormattingFeature { + + @Override + protected boolean isExistingFormatterOverrideable(@NotNull PsiFile file) { + // By default, isExistingFormatterOverrideable returns false if it has a custom formatter with psi + // returns true even if there is a custom formatter + return true; + } +} +``` + +## LSP Hover Feature + +| API | Description | Default Behaviour | +|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| String getContent(MarkupContent content, PsiFile file) | Returns the HTML content from the given LSP Markup content and null otherwise. | | + +## LSP Implementation Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP InlayHint Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP References Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Rename Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP SemanticTokens Feature + +| API | Description | Default Behaviour | +|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | +| TextAttributesKey getTextAttributesKey(@NotNull String tokenType, List tokenModifiers, PsiFile file) | Returns the TextAttributesKey to use for colorization for the given token type and given token modifiers and null otherwise. | | + +```java +package my.language.server; + +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPSemanticTokensFeature; +import org.jetbrains.annotations.NotNull; + +public class MyLSPSemanticTokensFeature extends LSPSemanticTokensFeature { + + @Override + public @Nullable TextAttributesKey getTextAttributesKey(@NotNull String tokenType, + @NotNull List tokenModifiers, + @NotNull PsiFile file) { + if ("myClass".equals(tokenType)) { + TextAttributesKey myClass = ... + return myClass; + } + if ("ignore".equals(tokenType)) { + return null; + } + return super.getTextAttributesKey(tokenType, tokenModifiers, file); + } +} +``` + +## LSP SignatureHelp Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP TypeDefinition Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Usage Feature + +| API | Description | Default Behaviour | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` | +| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise.
This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability | + +## LSP Workspace Symbol Feature + +| API | Description | Default Behaviour | +|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| boolean isEnabled() | Returns `true` if the LSP feature is enabled and `false` otherwise. | `true` when server is starting/started | +| boolean isSupported() | Returns `true` if the LSP feature is supported and `false` otherwise.
This supported state is called after starting the language server, which matches the LSP server capabilities. | Check the server capability | diff --git a/src/main/java/com/redhat/devtools/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java b/src/main/java/com/redhat/devtools/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java index d75d67d0b..517224353 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/ConnectDocumentToLanguageServerSetupParticipant.java @@ -80,7 +80,7 @@ private static void connectToLanguageServer(@NotNull VirtualFile file, @NotNull // Server capabilities filter is set to null to avoid waiting // for the start of the server when server capabilities are checked LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, null); + .getLanguageServers(file, null, null); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerFactory.java index e7831e973..320f7daef 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.LSPClientFeatures; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; @@ -50,4 +51,7 @@ public interface LanguageServerFactory { return LanguageServer.class; } + @NotNull default LSPClientFeatures createClientFeatures() { + return new LSPClientFeatures(); + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java index 3310c7526..a885e1f39 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java @@ -11,6 +11,7 @@ package com.redhat.devtools.lsp4ij; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; @@ -52,6 +53,15 @@ public LanguageServerWrapper getServerWrapper() { return serverWrapper; } + /** + * Returns the LSP client features. + * + * @return the LSP client features. + */ + public LSPClientFeatures getClientFeatures() { + return getServerWrapper().getClientFeatures(); + } + /** * Returns the server capabilities of the language server. * @@ -340,12 +350,12 @@ public static boolean isImplementationSupported(@Nullable ServerCapabilities ser } /** - * Returns true if the language server can support folding and false otherwise. + * Returns true if the language server can support folding range and false otherwise. * * @param serverCapabilities the server capabilities. - * @return true if the language server can support folding and false otherwise. + * @return true if the language server can support folding range and false otherwise. */ - public static boolean isFoldingSupported(@Nullable ServerCapabilities serverCapabilities) { + public static boolean isFoldingRangeSupported(@Nullable ServerCapabilities serverCapabilities) { return serverCapabilities != null && hasCapability(serverCapabilities.getFoldingRangeProvider()); } @@ -510,7 +520,7 @@ private static boolean hasCapability(Boolean capability) { } public SemanticTokensColorsProvider getSemanticTokensColorsProvider() { - return getServerWrapper().getServerDefinition().getSemanticTokensColorsProvider(); + return getClientFeatures().getSemanticTokensFeature(); } /** @@ -542,4 +552,12 @@ public static boolean isWorkspaceSymbolSupported(@Nullable ServerCapabilities se return serverCapabilities != null && hasCapability(serverCapabilities.getWorkspaceSymbolProvider()); } + + public static boolean isUsageSupported(ServerCapabilities serverCapabilities) { + return LanguageServerItem.isDeclarationSupported(serverCapabilities) || + LanguageServerItem.isTypeDefinitionSupported(serverCapabilities) || + LanguageServerItem.isDefinitionSupported(serverCapabilities) || + LanguageServerItem.isReferencesSupported(serverCapabilities) || + LanguageServerItem.isImplementationSupported(serverCapabilities); + } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerManager.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerManager.java index 9a12c5b0c..035c8d552 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerManager.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerManager.java @@ -16,6 +16,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + public class LanguageServerManager { private final Project project; @@ -276,4 +279,20 @@ public void stop(@NotNull LanguageServerDefinition serverDefinition, } } + public CompletableFuture<@Nullable LanguageServerItem> getLanguageServer(@NotNull String languageServerId) { + LanguageServerDefinition serverDefinition = LanguageServersRegistry.getInstance().getServerDefinition(languageServerId); + if (serverDefinition == null) { + return CompletableFuture.completedFuture(null); + } + return LanguageServiceAccessor.getInstance(project) + .getLanguageServers(Set.of(serverDefinition), null, null) + .thenApply(servers -> { + if (servers.isEmpty()) { + return null; + } + return servers.get(0); + }); + + } + } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java index 16f714eb1..31760f244 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.LSPClientFeatures; 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 LSPClientFeatures clientFeatures; + /* Backwards compatible constructor */ public LanguageServerWrapper(@NotNull Project project, @NotNull LanguageServerDefinition serverDefinition) { this(project, serverDefinition, null); @@ -226,7 +229,7 @@ public synchronized void start() throws LanguageServerException { // Add error log provider.addLogErrorHandler(error -> { - ServerMessageHandler.logMessage(this, new MessageParams(MessageType.Error, error)); + ServerMessageHandler.logMessage(this.getServerDefinition(), new MessageParams(MessageType.Error, error), getProject()); }); // Starting process... @@ -408,6 +411,9 @@ public void dispose() { this.disposed = true; stop(); stopDispatcher(); + if (clientFeatures != null) { + clientFeatures.dispose(); + } } public boolean isDisposed() { @@ -708,7 +714,7 @@ public void disconnect(URI path, boolean stopIfNoOpenedFiles) { synchronizer.getDocument().removeDocumentListener(synchronizer); synchronizer.documentClosed(); } - if (stopIfNoOpenedFiles && this.connectedDocuments.isEmpty()) { + if (getClientFeatures().isServerStoppingRequiredWhenFilesClosed() && stopIfNoOpenedFiles && this.connectedDocuments.isEmpty()) { if (this.serverDefinition.getLastDocumentDisconnectedTimeout() != 0 && !ApplicationManager.getApplication().isUnitTestMode()) { removeStopTimer(true); startStopTimer(); @@ -763,6 +769,10 @@ public LanguageServer getServer() { } } + public LanguageServer getLanguageServer() { + return languageServer; + } + /** * Starts the language server and returns a CompletableFuture waiting for the * server to be initialized. If done in the UI stream, a job will be created @@ -1174,4 +1184,20 @@ public boolean isSignatureTriggerCharactersSupported(String charTyped) { } return triggerCharacters.contains(charTyped); } + + public LSPClientFeatures getClientFeatures() { + if (clientFeatures == null) { + clientFeatures = getOrCreateClientFeatures(); + } + return clientFeatures; + } + + private synchronized LSPClientFeatures getOrCreateClientFeatures() { + if (clientFeatures != null) { + return clientFeatures; + } + LSPClientFeatures clientFeatures = getServerDefinition().createClientFeatures(); + clientFeatures.setServerWrapper(this); + return clientFeatures; + } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java index 8f30de70a..adef01f58 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java @@ -21,21 +21,19 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.vfs.VirtualFile; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinitionListener; import com.redhat.devtools.lsp4ij.server.definition.LanguageServerFileAssociation; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.services.LanguageServer; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URI; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -import java.util.stream.Collectors; /** * Language server accessor. @@ -44,6 +42,7 @@ *

* If adopters need to collect some language servers, LSP4IJ must provide an API for that. */ +@ApiStatus.Internal public class LanguageServiceAccessor implements Disposable { private static final Logger LOGGER = LoggerFactory.getLogger(LanguageServiceAccessor.class); @@ -63,11 +62,11 @@ public void handleRemoved(@NotNull LanguageServerDefinitionListener.LanguageServ List serversToDispose = startedServers .stream() .filter(server -> event.serverDefinitions.contains(server.getServerDefinition())) - .collect(Collectors.toList()); + .toList(); serversToDispose.forEach(LanguageServerWrapper::dispose); // Remove all servers which are removed from the cache synchronized (startedServers) { - startedServers.removeAll(serversToDispose); + serversToDispose.forEach(startedServers::remove); } } @@ -81,7 +80,7 @@ public void handleChanged(@NotNull LanguageServerChangedEvent event) { List serversToRestart = startedServers .stream() .filter(server -> event.serverDefinition.equals(server.getServerDefinition())) - .collect(Collectors.toList()); + .toList(); serversToRestart.forEach(LanguageServerWrapper::restart); } } @@ -123,7 +122,7 @@ public void findAndStartLanguageServerIfNeeded(LanguageServerDefinition definiti if (forceStart) { // The language server must be started even if there is no open file corresponding to it. LinkedHashSet matchedServers = new LinkedHashSet<>(); - collectLanguageServersFromDefinition(null, project, Set.of(definition), matchedServers); + collectLanguageServersFromDefinition(null, Set.of(definition), matchedServers, null); for (var ls : matchedServers) { ls.restart(); } @@ -144,7 +143,7 @@ public void findAndStartLanguageServerIfNeeded(LanguageServerDefinition definiti */ private void findAndStartLsForFile(@NotNull VirtualFile file, @NotNull LanguageServerDefinition definition) { - getLanguageServers(file, null, definition); + getLanguageServers(file, null, null, definition); } /** @@ -154,7 +153,6 @@ private void findAndStartLsForFile(@NotNull VirtualFile file, * @param filter the filter. * @return true if the given file matches one of started language server with the given filter and false otherwise. */ - @NotNull public boolean hasAny(@NotNull VirtualFile file, @NotNull Predicate filter) { var startedServers = getStartedServers(); @@ -176,22 +174,56 @@ public boolean hasAny(@NotNull VirtualFile file, } @NotNull - public CompletableFuture> getLanguageServers(@NotNull VirtualFile file, - @Nullable Predicate filter) { - return getLanguageServers(file, filter, null); + public CompletableFuture> getLanguageServers(@Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter) { + Set serverDefinitions = new HashSet<>(LanguageServersRegistry.getInstance().getServerDefinitions()); + return getLanguageServers(serverDefinitions, beforeStartingServerFilter, afterStartingServerFilter); } @NotNull - public CompletableFuture> getLanguageServers(@NotNull VirtualFile file, - @Nullable Predicate filter, - @Nullable LanguageServerDefinition matchServerDefinition) { - URI uri = LSPIJUtils.toUri(file); - if (uri == null) { + CompletableFuture> getLanguageServers(@NotNull Set serverDefinitions, + @Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter) { + Set matchedServers = new LinkedHashSet<>(); + collectLanguageServersFromDefinition(null, serverDefinitions, matchedServers, beforeStartingServerFilter); + final List servers = Collections.synchronizedList(new ArrayList<>()); + try { + return CompletableFuture.allOf(matchedServers + .stream() + .filter(LanguageServerWrapper::isEnabled) + .map(wrapper -> + wrapper.getInitializedServer() + .thenComposeAsync(server -> { + if (server != null && + (afterStartingServerFilter == null || afterStartingServerFilter.test(wrapper.getClientFeatures()))) { + servers.add(new LanguageServerItem(server, wrapper)); + } + return CompletableFuture.completedFuture(null); + })) + .toArray(CompletableFuture[]::new)) + .thenApply(theVoid -> servers); + } catch (ProcessCanceledException cancellation) { + throw cancellation; + } catch (Exception e) { + LOGGER.warn(e.getLocalizedMessage(), e); return CompletableFuture.completedFuture(Collections.emptyList()); } + } + + @NotNull + public CompletableFuture> getLanguageServers(@NotNull VirtualFile file, + @Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter) { + return getLanguageServers(file, beforeStartingServerFilter, afterStartingServerFilter, null); + } + @NotNull + private CompletableFuture> getLanguageServers(@NotNull VirtualFile file, + @Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter, + @Nullable LanguageServerDefinition matchServerDefinition) { // Collect started (or not) language servers which matches the given file. - CompletableFuture> matchedServers = getMatchedLanguageServersWrappers(file, matchServerDefinition); + CompletableFuture> matchedServers = getMatchedLanguageServersWrappers(file, matchServerDefinition, beforeStartingServerFilter); if (matchedServers.isDone() && matchedServers.getNow(Collections.emptyList()).isEmpty()) { // None language servers matches the given file return CompletableFuture.completedFuture(Collections.emptyList()); @@ -209,12 +241,12 @@ public CompletableFuture> getLanguageServers(@NotNull V return matchedServers .thenComposeAsync(result -> CompletableFuture.allOf(result .stream() + .filter(LanguageServerWrapper::isEnabled) .map(wrapper -> wrapper.getInitializedServer() .thenComposeAsync(server -> { if (server != null && - wrapper.isEnabled() && - (filter == null || filter.test(wrapper.getServerCapabilities()))) { + (afterStartingServerFilter == null || afterStartingServerFilter.test(wrapper.getClientFeatures()))) { return wrapper.connect(file, document); } return CompletableFuture.completedFuture(null); @@ -249,14 +281,15 @@ public void projectClosing(Project project) { @NotNull private CompletableFuture> getMatchedLanguageServersWrappers( @NotNull VirtualFile file, - @Nullable LanguageServerDefinition matchServerDefinition) { + @Nullable LanguageServerDefinition matchServerDefinition, + @Nullable Predicate beforeStartingServerFilter) { MatchedLanguageServerDefinitions mappings = getMatchedLanguageServerDefinitions(file, project, false); if (mappings == MatchedLanguageServerDefinitions.NO_MATCH) { // There are no mapping for the given file return CompletableFuture.completedFuture(Collections.emptyList()); } - LinkedHashSet matchedServers = new LinkedHashSet<>(); + Set matchedServers = new LinkedHashSet<>(); // Collect sync server definitions var serverDefinitions = mappings.getMatched(); @@ -264,9 +297,9 @@ private CompletableFuture> getMatchedLanguageS if (!serverDefinitions.contains(matchServerDefinition)) { return CompletableFuture.completedFuture(Collections.emptyList()); } - collectLanguageServersFromDefinition(file, project, Set.of(matchServerDefinition), matchedServers); + collectLanguageServersFromDefinition(file, Set.of(matchServerDefinition), matchedServers, beforeStartingServerFilter); } else { - collectLanguageServersFromDefinition(file, project, serverDefinitions, matchedServers); + collectLanguageServersFromDefinition(file, serverDefinitions, matchedServers, beforeStartingServerFilter); } CompletableFuture> async = mappings.getAsyncMatched(); @@ -274,7 +307,7 @@ private CompletableFuture> getMatchedLanguageS // Collect async server definitions return async .thenApply(asyncServerDefinitions -> { - collectLanguageServersFromDefinition(file, project, asyncServerDefinitions, matchedServers); + collectLanguageServersFromDefinition(file, asyncServerDefinitions, matchedServers, beforeStartingServerFilter); return matchedServers; }); } @@ -284,19 +317,23 @@ private CompletableFuture> getMatchedLanguageS /** * Get or create a language server wrapper for the given server definitions and add then to the given matched servers. * - * @param file the file. - * @param fileProject the file project. - * @param serverDefinitions the server definitions. - * @param matchedServers the list to update with get/created language server. + * @param file the file. + * @param serverDefinitions the server definitions. + * @param matchedServers the list to update with get/created language server. + * @param beforeStartingServerFilter */ - private void collectLanguageServersFromDefinition(@Nullable VirtualFile file, @NotNull Project fileProject, @NotNull Set serverDefinitions, @NotNull Set matchedServers) { + private void collectLanguageServersFromDefinition(@Nullable VirtualFile file, + @NotNull Set serverDefinitions, + @NotNull Set matchedServers, + @Nullable Predicate beforeStartingServerFilter) { synchronized (startedServers) { for (var serverDefinition : serverDefinitions) { boolean useExistingServer = false; // Loop for started language servers for (var startedServer : startedServers) { if (startedServer.getServerDefinition().equals(serverDefinition) - && (file == null || startedServer.canOperate(file))) { + && (file == null || startedServer.canOperate(file)) + && (beforeStartingServerFilter == null || beforeStartingServerFilter.test(startedServer.getClientFeatures()))) { // A started language server match the file, use it matchedServers.add(startedServer); useExistingServer = true; @@ -305,9 +342,11 @@ private void collectLanguageServersFromDefinition(@Nullable VirtualFile file, @N } if (!useExistingServer) { // There are none started servers which matches the file, create and add it. - LanguageServerWrapper wrapper = new LanguageServerWrapper(fileProject, serverDefinition); - startedServers.add(wrapper); - matchedServers.add(wrapper); + LanguageServerWrapper wrapper = new LanguageServerWrapper(project, serverDefinition); + if (beforeStartingServerFilter == null || beforeStartingServerFilter.test(wrapper.getClientFeatures())) { + startedServers.add(wrapper); + matchedServers.add(wrapper); + } } } } @@ -356,9 +395,9 @@ public CompletableFuture> getAsyncMatched() { * @param ignoreMatch true if {@link DocumentMatcher} must be ignored when mapping matches the given file and false otherwise. * @return the matched language server definitions for the given file. */ - public MatchedLanguageServerDefinitions getMatchedLanguageServerDefinitions(@NotNull VirtualFile file, - @NotNull Project fileProject, - boolean ignoreMatch) { + private MatchedLanguageServerDefinitions getMatchedLanguageServerDefinitions(@NotNull VirtualFile file, + @NotNull Project fileProject, + boolean ignoreMatch) { Set syncMatchedDefinitions = null; Set asyncMatchedDefinitions = null; @@ -371,9 +410,7 @@ public MatchedLanguageServerDefinitions getMatchedLanguageServerDefinitions(@Not languages.add(language); } FileType fileType = file.getFileType(); - if (fileType != null) { - languages.add(fileType); - } + languages.add(fileType); while (!languages.isEmpty()) { Object contentType = languages.poll(); @@ -429,16 +466,14 @@ public MatchedLanguageServerDefinitions getMatchedLanguageServerDefinitions(@Not final Set serverDefinitions = Collections.synchronizedSet(new HashSet<>()); async = CompletableFuture.allOf(asyncMatchedDefinitions .stream() - .map(mapping -> { - return mapping - .matchAsync(file, fileProject) - .thenApply(result -> { - if (result) { - serverDefinitions.add(mapping.getServerDefinition()); - } - return null; - }); - } + .map(mapping -> mapping + .matchAsync(file, fileProject) + .thenApply(result -> { + if (result) { + serverDefinitions.add(mapping.getServerDefinition()); + } + return null; + }) ) .toArray(CompletableFuture[]::new)) .thenApply(theVoid -> serverDefinitions); @@ -456,48 +491,6 @@ private static boolean match(VirtualFile file, Project fileProject, LanguageServ return mapping.match(file, fileProject); } - - /** - * Gets list of running LS satisfying a capability predicate. This does not - * start any matching language servers, it returns the already running ones. - * - * @param request - * @return list of Language Servers - */ - @NotNull - public List getActiveLanguageServers(Predicate request) { - return getLanguageServers(null, request, true); - } - - /** - * Gets list of LS initialized for given project - * - * @param onlyActiveLS true if this method should return only the already running - * language servers, otherwise previously started language servers - * will be re-activated - * @return list of Language Servers - */ - @NotNull - public List getLanguageServers(@Nullable Project project, - Predicate request, boolean onlyActiveLS) { - List serverInfos = new ArrayList<>(); - for (LanguageServerWrapper wrapper : startedServers) { - if ((!onlyActiveLS || wrapper.isActive()) && (project == null || wrapper.canOperate(project))) { - @Nullable - LanguageServer server = wrapper.getServer(); - if (server == null) { - continue; - } - if (request == null - || wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */ - || request.test(wrapper.getServerCapabilities())) { - serverInfos.add(server); - } - } - } - return serverInfos; - } - @Override public void dispose() { LanguageServersRegistry.getInstance().removeLanguageServerDefinitionListener(serverDefinitionListener); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/ServerMessageHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/ServerMessageHandler.java index 358d28a38..34d4f6ad1 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/ServerMessageHandler.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/ServerMessageHandler.java @@ -51,20 +51,13 @@ private ServerMessageHandler() { /** * Implements the LSP window/logMessage specification. * - * @param serverWrapper the language server wrapper - * @param params the message request parameters - */ - public static void logMessage(LanguageServerWrapper serverWrapper, MessageParams params) { - logMessage(serverWrapper.getServerDefinition(), params, serverWrapper.getProject() ); - } - - /** - * Implements the LSP window/logMessage specification. - * - * @param serverWrapper the language server wrapper - * @param params the message request parameters + * @param serverDefinition the language server definition. + * @param params the message request parameters. + * @param project the project. */ - public static void logMessage(LanguageServerDefinition serverDefinition, MessageParams params, Project project) { + public static void logMessage(@NotNull LanguageServerDefinition serverDefinition, + @NotNull MessageParams params, + @NotNull Project project) { LSPConsoleToolWindowPanel.showLog(serverDefinition, params, project ); } @@ -107,17 +100,16 @@ public static void showMessage(@NotNull String title, /** * Implements the LSP window/showMessageRequest specification. * - * @param serverWrapper the language server wrapper - * @param params the message request parameters + * @param languageServerName the language server name. + * @param params the message request parameters. + * @param project the project. */ - public static CompletableFuture showMessageRequest(@NotNull LanguageServerWrapper serverWrapper, + public static CompletableFuture showMessageRequest(@NotNull String languageServerName, @NotNull ShowMessageRequestParams params, @NotNull Project project) { CompletableFuture future = new CompletableFuture<>(); ApplicationManager.getApplication() .invokeLater(() -> { - String languageServerName = serverWrapper.getServerDefinition().getDisplayName(); - String title = params.getMessage(); String content = MarkdownConverter.getInstance(project).toHtml(params.getMessage()); final Notification notification = new Notification( LSP_WINDOW_SHOW_MESSAGE_REQUEST_GROUP_ID, @@ -147,7 +139,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { } }); - Notifications.Bus.notify(notification, serverWrapper.getProject()); + Notifications.Bus.notify(notification, project); var balloon= notification.getBalloon(); if (balloon != null) { balloon.addListener(new JBPopupListener() { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java index 9d9c5d5fa..e40d7eb4b 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java @@ -24,6 +24,7 @@ import com.redhat.devtools.lsp4ij.features.diagnostics.LSPDiagnosticHandler; import com.redhat.devtools.lsp4ij.features.progress.LSPProgressManager; import com.redhat.devtools.lsp4ij.internal.InlayHintsFactoryBridge; +import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageServer; @@ -62,7 +63,7 @@ public Project getProject() { return project; } - public final void connect(LanguageServer server, LanguageServerWrapper wrapper) { + public final void connect(@NotNull LanguageServer server, @NotNull LanguageServerWrapper wrapper) { this.server = server; this.wrapper = wrapper; this.diagnosticHandler = new LSPDiagnosticHandler(wrapper); @@ -73,19 +74,23 @@ protected final LanguageServer getLanguageServer() { return server; } + public LanguageServerDefinition getServerDefinition() { + return wrapper.getServerDefinition(); + } + @Override public void telemetryEvent(Object object) { // TODO } @Override - public final CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { - return ServerMessageHandler.showMessageRequest(wrapper, requestParams, getProject()); + public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { + return ServerMessageHandler.showMessageRequest(getServerDefinition().getDisplayName(), requestParams, getProject()); } @Override - public final void showMessage(MessageParams messageParams) { - ServerMessageHandler.showMessage(wrapper.getServerDefinition().getDisplayName(), messageParams, getProject()); + public void showMessage(MessageParams messageParams) { + ServerMessageHandler.showMessage(getServerDefinition().getDisplayName(), messageParams, getProject()); } @Override @@ -94,17 +99,17 @@ public CompletableFuture showDocument(ShowDocumentParams par } @Override - public final void publishDiagnostics(PublishDiagnosticsParams diagnostics) { + public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { this.diagnosticHandler.accept(diagnostics); } @Override - public final void logMessage(MessageParams message) { - CompletableFuture.runAsync(() -> ServerMessageHandler.logMessage(wrapper, message)); + public void logMessage(MessageParams message) { + CompletableFuture.runAsync(() -> ServerMessageHandler.logMessage(getServerDefinition(), message, getProject())); } @Override - public final CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { + public CompletableFuture applyEdit(ApplyWorkspaceEditParams params) { CompletableFuture future = new CompletableFuture<>(); WriteCommandAction.runWriteCommandAction(getProject(), () -> { LSPIJUtils.applyWorkspaceEdit(params.getEdit()); @@ -125,9 +130,7 @@ public CompletableFuture unregisterCapability(UnregistrationParams params) @Override public CompletableFuture> workspaceFolders() { - return CompletableFuture.supplyAsync(() -> { - return LSPIJUtils.toWorkspaceFolders(project); - }); + return CompletableFuture.supplyAsync(() -> LSPIJUtils.toWorkspaceFolders(project)); } @Override @@ -144,7 +147,7 @@ private void refreshInlayHintsForAllOpenedFiles() { ReadAction.nonBlocking((Callable) () -> { for (var fileData : wrapper.getConnectedFiles()) { VirtualFile file = fileData.getFile(); - final PsiFile psiFile = LSPIJUtils.getPsiFile(file, project); + PsiFile psiFile = LSPIJUtils.getPsiFile(file, project); if (psiFile != null) { Editor[] editors = LSPIJUtils.editorsForFile(file, getProject()); InlayHintsFactoryBridge.refreshInlayHints(psiFile, editors, true); @@ -170,7 +173,7 @@ private void refreshSemanticTokensForAllOpenedFiles() { ReadAction.nonBlocking((Callable) () -> { for (var fileData : wrapper.getConnectedFiles()) { VirtualFile file = fileData.getFile(); - final PsiFile psiFile = LSPIJUtils.getPsiFile(file, project); + PsiFile psiFile = LSPIJUtils.getPsiFile(file, project); if (psiFile != null) { // Evict the semantic tokens cache LSPFileSupport.getSupport(psiFile).getSemanticTokensSupport().cancel(); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPDocumentFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPDocumentFeature.java new file mode 100644 index 000000000..026931eda --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPDocumentFeature.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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; + +/** + * Abstract class for any LSP document feature. + */ +@ApiStatus.Experimental +public abstract class AbstractLSPDocumentFeature extends AbstractLSPFeature { + + /** + * Returns true if the LSP feature is enabled for the given file and false otherwise. + * + *

+ * This enable state is called before starting the language server which matches the file. + *

+ * + * @param file the file. + * + * @return true if the LSP feature is enabled for the given file and false otherwise. + */ + public boolean isEnabled(@NotNull PsiFile file) { + return true; + } + + /** + * Returns true if the LSP feature is supported for the given file and false otherwise. + * + *

+ * This supported state is called after starting the language server which matches the file and user the LSP server capabilities. + *

+ * + * @param file the file. + * + * @return true if the LSP feature is supported for the given file and false otherwise. + */ + public abstract boolean isSupported(@NotNull PsiFile 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..953546c34 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPFeature.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.Disposable; +import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.ServerStatus; +import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; +import org.eclipse.lsp4j.services.LanguageServer; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Abstract class for any LSP feature. + */ +@ApiStatus.Experimental +public abstract class AbstractLSPFeature implements Disposable { + + private LSPClientFeatures clientFeatures; + + /** + * Returns the LSP server support. + * + * @return the LSP server support. + */ + public final @NotNull LSPClientFeatures getClientFeatures() { + return clientFeatures; + } + + /** + * Returns the project. + * + * @return the project. + */ + @NotNull + public final Project getProject() { + return clientFeatures.getProject(); + } + + /** + * Returns the server status. + * + * @return the server status. + */ + @NotNull + public final ServerStatus getServerStatus() { + return getClientFeatures().getServerStatus(); + } + + /** + * Returns the language server definition. + * + * @return the language server definition. + */ + @NotNull + public final LanguageServerDefinition getServerDefinition() { + return getClientFeatures().getServerDefinition(); + } + + /** + * Returns true if the given language server id matches the server definition and false otherwise. + * + * @param languageServerId the language server id. + * @return true if the given language server id matches the server definition and false otherwise. + */ + public boolean isServerDefinition(@NotNull String languageServerId) { + return getClientFeatures().isServerDefinition(languageServerId); + } + + /** + * Returns the LSP4J language server. + * + * @return the LSP4J language server. + */ + @Nullable + public final LanguageServer getLanguageServer() { + return getClientFeatures().getLanguageServer(); + } + + /** + * Set the LSP server support. + * + * @param clientFeatures the LSP client features. + */ + @ApiStatus.Internal + public final void setClientFeatures(@NotNull LSPClientFeatures clientFeatures) { + this.clientFeatures = clientFeatures; + } + + @Override + public void dispose() { + clientFeatures = null; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPWorkspaceFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPWorkspaceFeature.java new file mode 100644 index 000000000..561b06867 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/AbstractLSPWorkspaceFeature.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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; + +/** + * Abstract class for any LSP workspace feature. + */ +@ApiStatus.Experimental +public abstract class AbstractLSPWorkspaceFeature extends AbstractLSPFeature { + + /** + * Returns true if the LSP feature is enabled and false otherwise. + * + *

+ * This enable state is called before starting the language server. + *

+ * + * @return true if the LSP feature is enabled and false otherwise. + */ + public boolean isEnabled() { + return true; + } + + /** + * Returns true if the LSP feature is supported false otherwise. + * + *

+ * This supported state is called after starting the language server which matches the file and user the LSP server capabilities. + *

+ * + * + * @return true if the LSP feature is supported and false otherwise. + */ + public abstract boolean isSupported(); + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java new file mode 100644 index 000000000..3add1b123 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java @@ -0,0 +1,783 @@ +/******************************************************************************* + * 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.Disposable; +import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.lsp4ij.ServerStatus; +import com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition; +import org.eclipse.lsp4j.services.LanguageServer; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * LSP client features. + */ +@ApiStatus.Experimental +public class LSPClientFeatures implements Disposable { + + private LanguageServerWrapper serverWrapper; + + private LSPCodeActionFeature codeActionFeature; + + private LSPCodeLensFeature codeLensFeature; + + private LSPColorFeature colorFeature; + + private LSPCompletionFeature completionFeature; + + private LSPDeclarationFeature declarationFeature; + + private LSPDefinitionFeature definitionFeature; + + private LSPDocumentLinkFeature documentLinkFeature; + + private LSPDocumentHighlightFeature documentHighlightFeature; + + private LSPDocumentSymbolFeature documentSymbolFeature; + + private LSPDiagnosticFeature diagnosticFeature; + + private LSPFoldingRangeFeature foldingRangeFeature; + + private LSPFormattingFeature formattingFeature; + + private LSPImplementationFeature implementationFeature; + + private LSPInlayHintFeature inlayHintFeature; + + private LSPHoverFeature hoverFeature; + + private LSPReferencesFeature referencesFeature; + + private LSPRenameFeature renameFeature; + + private LSPSemanticTokensFeature semanticTokensFeature; + + private LSPSignatureHelpFeature signatureHelpFeature; + + private LSPTypeDefinitionFeature typeDefinitionFeature; + + private LSPUsageFeature usageFeature; + + private LSPWorkspaceSymbolFeature workspaceSymbolFeature; + + /** + * Returns the project. + * + * @return the project. + */ + @NotNull + public final Project getProject() { + return getServerWrapper().getProject(); + } + + /** + * Returns the language server definition. + * + * @return the language server definition. + */ + @NotNull + public final LanguageServerDefinition getServerDefinition() { + return getServerWrapper().getServerDefinition(); + } + + /** + * Returns true if the given language server id matches the server definition and false otherwise. + * + * @param languageServerId the language server id. + * @return true if the given language server id matches the server definition and false otherwise. + */ + public boolean isServerDefinition(@NotNull String languageServerId) { + return languageServerId.equals(getServerDefinition().getId()); + } + + /** + * Returns the server status. + * + * @return the server status. + */ + @NotNull + public final ServerStatus getServerStatus() { + return getServerWrapper().getServerStatus(); + } + + /** + * Returns the LSP4J language server. + * + * @return the LSP4J language server. + */ + @Nullable + public final LanguageServer getLanguageServer() { + return getServerWrapper().getLanguageServer(); + } + + /** + * Returns true if server stopping required when files closed and false otherwise. + * + * @return true if server stopping required when files closed and false otherwise. + */ + public boolean isServerStoppingRequiredWhenFilesClosed() { + return true; + } + + /** + * Returns the LSP codeAction feature. + * + * @return the LSP codeAction feature. + */ + @NotNull + public final LSPCodeActionFeature getCodeActionFeature() { + if (codeActionFeature == null) { + setCodeActionFeature(new LSPCodeActionFeature()); + } + return codeActionFeature; + } + + /** + * Initialize the LSP codeAction feature. + * + * @param codeActionFeature the LSP codeAction feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setCodeActionFeature(@NotNull LSPCodeActionFeature codeActionFeature) { + codeActionFeature.setClientFeatures(this); + this.codeActionFeature = codeActionFeature; + return this; + } + + /** + * Returns the LSP codeLens feature. + * + * @return the LSP codeLens feature. + */ + @NotNull + public final LSPCodeLensFeature getCodeLensFeature() { + if (codeLensFeature == null) { + setCodeLensFeature(new LSPCodeLensFeature()); + } + return codeLensFeature; + } + + /** + * Initialize the LSP codeLens feature. + * + * @param codeLensFeature the LSP codeLens feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setCodeLensFeature(@NotNull LSPCodeLensFeature codeLensFeature) { + codeLensFeature.setClientFeatures(this); + this.codeLensFeature = codeLensFeature; + return this; + } + + /** + * Returns the LSP color feature. + * + * @return the LSP color feature. + */ + @NotNull + public final LSPColorFeature getColorFeature() { + if (colorFeature == null) { + setColorFeature(new LSPColorFeature()); + } + return colorFeature; + } + + /** + * Initialize the LSP color feature. + * + * @param colorFeature the LSP color feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setColorFeature(@NotNull LSPColorFeature colorFeature) { + colorFeature.setClientFeatures(this); + this.colorFeature = colorFeature; + return this; + } + + /** + * Returns the LSP completion feature. + * + * @return the LSP completion feature. + */ + @NotNull + public final LSPCompletionFeature getCompletionFeature() { + if (completionFeature == null) { + setCompletionFeature(new LSPCompletionFeature()); + } + return completionFeature; + } + + /** + * Initialize the LSP completion feature. + * + * @param completionFeature the LSP completion feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setCompletionFeature(@NotNull LSPCompletionFeature completionFeature) { + completionFeature.setClientFeatures(this); + this.completionFeature = completionFeature; + return this; + } + + /** + * Returns the LSP declaration feature. + * + * @return the LSP declaration feature. + */ + @NotNull + public final LSPDeclarationFeature getDeclarationFeature() { + if (declarationFeature == null) { + setDeclarationFeature(new LSPDeclarationFeature()); + } + return declarationFeature; + } + + /** + * Initialize the LSP declaration feature. + * + * @param declarationFeature the LSP declaration feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setDeclarationFeature(@NotNull LSPDeclarationFeature declarationFeature) { + declarationFeature.setClientFeatures(this); + this.declarationFeature = declarationFeature; + return this; + } + + /** + * Returns the LSP definition feature. + * + * @return the LSP definition feature. + */ + @NotNull + public final LSPDefinitionFeature getDefinitionFeature() { + if (definitionFeature == null) { + setDefinitionFeature(new LSPDefinitionFeature()); + } + return definitionFeature; + } + + /** + * Initialize the LSP definition feature. + * + * @param definitionFeature the LSP definition feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setDefinitionFeature(@NotNull LSPDefinitionFeature definitionFeature) { + definitionFeature.setClientFeatures(this); + this.definitionFeature = definitionFeature; + return this; + } + + /** + * Returns the LSP documentHighlight feature. + * + * @return the LSP documentHighlight feature. + */ + @NotNull + public final LSPDocumentHighlightFeature getDocumentHighlightFeature() { + if (documentHighlightFeature == null) { + setDocumentHighlightFeature(new LSPDocumentHighlightFeature()); + } + return documentHighlightFeature; + } + + /** + * Initialize the LSP documentHighlight feature. + * + * @param documentHighlightFeature the LSP documentHighlight feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setDocumentHighlightFeature(@NotNull LSPDocumentHighlightFeature documentHighlightFeature) { + documentHighlightFeature.setClientFeatures(this); + this.documentHighlightFeature = documentHighlightFeature; + return this; + } + + /** + * Returns the LSP documentLink feature. + * + * @return the LSP documentLink feature. + */ + @NotNull + public final LSPDocumentLinkFeature getDocumentLinkFeature() { + if (documentLinkFeature == null) { + setDocumentLinkFeature(new LSPDocumentLinkFeature()); + } + return documentLinkFeature; + } + + /** + * Initialize the LSP documentLink feature. + * + * @param documentLinkFeature the LSP documentLink feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setDocumentLinkFeature(@NotNull LSPDocumentLinkFeature documentLinkFeature) { + documentLinkFeature.setClientFeatures(this); + this.documentLinkFeature = documentLinkFeature; + return this; + } + + /** + * Returns the LSP documentSymbol feature. + * + * @return the LSP documentSymbol feature. + */ + @NotNull + public final LSPDocumentSymbolFeature getDocumentSymbolFeature() { + if (documentSymbolFeature == null) { + setDocumentSymbolFeature(new LSPDocumentSymbolFeature()); + } + return documentSymbolFeature; + } + + /** + * Initialize the LSP documentSymbol feature. + * + * @param documentSymbolFeature the LSP documentSymbol feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setDocumentSymbolFeature(@NotNull LSPDocumentSymbolFeature documentSymbolFeature) { + documentSymbolFeature.setClientFeatures(this); + this.documentSymbolFeature = documentSymbolFeature; + return this; + } + + /** + * Returns the LSP diagnostic feature. + * + * @return the LSP diagnostic feature. + */ + @NotNull + public final LSPDiagnosticFeature getDiagnosticFeature() { + if (diagnosticFeature == null) { + setDiagnosticFeature(new LSPDiagnosticFeature()); + } + return diagnosticFeature; + } + + /** + * Initialize the LSP diagnostic feature. + * + * @param diagnosticFeature the LSP diagnostic feature. + * @return the LSP client feature. + */ + public LSPClientFeatures setDiagnosticFeature(@NotNull LSPDiagnosticFeature diagnosticFeature) { + diagnosticFeature.setClientFeatures(this); + this.diagnosticFeature = diagnosticFeature; + return this; + } + + /** + * Returns the LSP foldingRange feature. + * + * @return the LSP foldingRange feature. + */ + @NotNull + public final LSPFoldingRangeFeature getFoldingRangeFeature() { + if (foldingRangeFeature == null) { + setFoldingRangeFeature(new LSPFoldingRangeFeature()); + } + return foldingRangeFeature; + } + + /** + * Initialize the LSP foldingRange feature. + * + * @param foldingRangeFeature the LSP foldingRange feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setFoldingRangeFeature(@NotNull LSPFoldingRangeFeature foldingRangeFeature) { + foldingRangeFeature.setClientFeatures(this); + this.foldingRangeFeature = foldingRangeFeature; + return this; + } + + /** + * Returns the LSP formatting feature. + * + * @return the LSP formatting feature. + */ + @NotNull + public final LSPFormattingFeature getFormattingFeature() { + if (formattingFeature == null) { + setFormattingFeature(new LSPFormattingFeature()); + } + return formattingFeature; + } + + /** + * Initialize the LSP formatting feature. + * + * @param formattingFeature the LSP formatting feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setFormattingFeature(@NotNull LSPFormattingFeature formattingFeature) { + formattingFeature.setClientFeatures(this); + this.formattingFeature = formattingFeature; + return this; + } + + /** + * Returns the LSP implementation feature. + * + * @return the LSP implementation feature. + */ + @NotNull + public final LSPImplementationFeature getImplementationFeature() { + if (implementationFeature == null) { + setImplementationFeature(new LSPImplementationFeature()); + } + return implementationFeature; + } + + /** + * Initialize the LSP implementation feature. + * + * @param implementationFeature the LSP implementation feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setImplementationFeature(@NotNull LSPImplementationFeature implementationFeature) { + implementationFeature.setClientFeatures(this); + this.implementationFeature = implementationFeature; + return this; + } + + /** + * Returns the LSP inlayHint feature. + * + * @return the LSP inlayHint feature. + */ + @NotNull + public final LSPInlayHintFeature getInlayHintFeature() { + if (inlayHintFeature == null) { + setInlayHintFeature(new LSPInlayHintFeature()); + } + return inlayHintFeature; + } + + /** + * Initialize the LSP inlayHint feature. + * + * @param inlayHintFeature the LSP inlayHint feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setInlayHintFeature(@NotNull LSPInlayHintFeature inlayHintFeature) { + inlayHintFeature.setClientFeatures(this); + this.inlayHintFeature = inlayHintFeature; + return this; + } + + /** + * Returns the LSP hover feature. + * + * @return the LSP hover feature. + */ + @NotNull + public final LSPHoverFeature getHoverFeature() { + if (hoverFeature == null) { + setHoverFeature(new LSPHoverFeature()); + } + return hoverFeature; + } + + /** + * Initialize the LSP hover feature. + * + * @param hoverFeature the LSP hover feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setHoverFeature(@NotNull LSPHoverFeature hoverFeature) { + hoverFeature.setClientFeatures(this); + this.hoverFeature = hoverFeature; + return this; + } + + /** + * Returns the LSP references feature. + * + * @return the LSP references feature. + */ + @NotNull + public final LSPReferencesFeature getReferencesFeature() { + if (referencesFeature == null) { + setReferencesFeature(new LSPReferencesFeature()); + } + return referencesFeature; + } + + /** + * Initialize the LSP references feature. + * + * @param referencesFeature the LSP references feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setReferencesFeature(@NotNull LSPReferencesFeature referencesFeature) { + referencesFeature.setClientFeatures(this); + this.referencesFeature = referencesFeature; + return this; + } + + /** + * Returns the LSP rename feature. + * + * @return the LSP rename feature. + */ + @NotNull + public final LSPRenameFeature getRenameFeature() { + if (renameFeature == null) { + setRenameFeature(new LSPRenameFeature()); + } + return renameFeature; + } + + /** + * Initialize the LSP rename feature. + * + * @param renameFeature the LSP rename feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setRenameFeature(@NotNull LSPRenameFeature renameFeature) { + renameFeature.setClientFeatures(this); + this.renameFeature = renameFeature; + return this; + } + + /** + * Returns the LSP semanticTokens feature. + * + * @return the LSP semanticTokens feature. + */ + @NotNull + public final LSPSemanticTokensFeature getSemanticTokensFeature() { + if (semanticTokensFeature == null) { + setSemanticTokensFeature(new LSPSemanticTokensFeature()); + } + return semanticTokensFeature; + } + + /** + * Initialize the LSP semanticTokens feature. + * + * @param semanticTokensFeature the LSP semanticTokens feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setSemanticTokensFeature(@NotNull LSPSemanticTokensFeature semanticTokensFeature) { + semanticTokensFeature.setClientFeatures(this); + this.semanticTokensFeature = semanticTokensFeature; + return this; + } + + /** + * Returns the LSP signatureHelp feature. + * + * @return the LSP signatureHelp feature. + */ + @NotNull + public final LSPSignatureHelpFeature getSignatureHelpFeature() { + if (signatureHelpFeature == null) { + setSignatureHelpFeature(new LSPSignatureHelpFeature()); + } + return signatureHelpFeature; + } + + /** + * Initialize the LSP signatureHelp feature. + * + * @param signatureHelpFeature the LSP signatureHelp feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setSignatureHelpFeature(@NotNull LSPSignatureHelpFeature signatureHelpFeature) { + signatureHelpFeature.setClientFeatures(this); + this.signatureHelpFeature = signatureHelpFeature; + return this; + } + + /** + * Returns the LSP typeDefinition feature. + * + * @return the LSP typeDefinition feature. + */ + @NotNull + public final LSPTypeDefinitionFeature getTypeDefinitionFeature() { + if (typeDefinitionFeature == null) { + setTypeDefinitionFeature(new LSPTypeDefinitionFeature()); + } + return typeDefinitionFeature; + } + + /** + * Initialize the LSP typeDefinition feature. + * + * @param typeDefinitionFeature the LSP typeDefinition feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setTypeDefinitionFeature(@NotNull LSPTypeDefinitionFeature typeDefinitionFeature) { + typeDefinitionFeature.setClientFeatures(this); + this.typeDefinitionFeature = typeDefinitionFeature; + return this; + } + + /** + * Returns the LSP usage feature. + * + * @return the LSP usage feature. + */ + @NotNull + public final LSPUsageFeature getUsageFeature() { + if (usageFeature == null) { + setUsageFeature(new LSPUsageFeature()); + } + return usageFeature; + } + + /** + * Initialize the LSP usage feature. + * + * @param usageFeature the LSP usage feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setUsageFeature(@NotNull LSPUsageFeature usageFeature) { + usageFeature.setClientFeatures(this); + this.usageFeature = usageFeature; + return this; + } + + /** + * Returns the LSP workspaceSymbol feature. + * + * @return the LSP workspaceSymbol feature. + */ + @NotNull + public final LSPWorkspaceSymbolFeature getWorkspaceSymbolFeature() { + if (workspaceSymbolFeature == null) { + setWorkspaceSymbolFeature(new LSPWorkspaceSymbolFeature()); + } + return workspaceSymbolFeature; + } + + /** + * Initialize the LSP workspaceSymbol feature. + * + * @param workspaceSymbolFeature the LSP workspaceSymbol feature. + * @return the LSP client features. + */ + public final LSPClientFeatures setWorkspaceSymbolFeature(@NotNull LSPWorkspaceSymbolFeature workspaceSymbolFeature) { + workspaceSymbolFeature.setClientFeatures(this); + this.workspaceSymbolFeature = workspaceSymbolFeature; + return this; + } + + /** + * Set the language server wrapper. + * + * @param serverWrapper the language server wrapper. + */ + @ApiStatus.Internal + public final void setServerWrapper(LanguageServerWrapper serverWrapper) { + this.serverWrapper = serverWrapper; + } + + /** + * Returns the language server wrapper. + * + * @return the language server wrapper. + */ + @ApiStatus.Internal + final LanguageServerWrapper getServerWrapper() { + return serverWrapper; + } + + /** + * Dispose client features called when the language server is disposed. + */ + @Override + public void dispose() { + serverWrapper = null; + if (codeActionFeature != null) { + codeActionFeature.dispose(); + } + if (codeLensFeature != null) { + codeLensFeature.dispose(); + } + if (colorFeature != null) { + colorFeature.dispose(); + } + if (completionFeature != null) { + completionFeature.dispose(); + } + if (codeActionFeature != null) { + codeActionFeature.dispose(); + } + if (codeActionFeature != null) { + codeActionFeature.dispose(); + } + if (declarationFeature != null) { + declarationFeature.dispose(); + } + if (definitionFeature != null) { + definitionFeature.dispose(); + } + if (documentLinkFeature != null) { + documentLinkFeature.dispose(); + } + if (documentHighlightFeature != null) { + documentHighlightFeature.dispose(); + } + if (documentSymbolFeature != null) { + documentSymbolFeature.dispose(); + } + if (diagnosticFeature != null) { + diagnosticFeature.dispose(); + } + if (foldingRangeFeature != null) { + foldingRangeFeature.dispose(); + } + if (formattingFeature != null) { + formattingFeature.dispose(); + } + if (implementationFeature != null) { + implementationFeature.dispose(); + } + if (inlayHintFeature != null) { + inlayHintFeature.dispose(); + } + if (hoverFeature != null) { + hoverFeature.dispose(); + } + if (referencesFeature != null) { + referencesFeature.dispose(); + } + if (renameFeature != null) { + renameFeature.dispose(); + } + if (semanticTokensFeature != null) { + semanticTokensFeature.dispose(); + } + if (signatureHelpFeature != null) { + signatureHelpFeature.dispose(); + } + if (typeDefinitionFeature != null) { + typeDefinitionFeature.dispose(); + } + if (usageFeature != null) { + usageFeature.dispose(); + } + if (workspaceSymbolFeature != null) { + workspaceSymbolFeature.dispose(); + } + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeActionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeActionFeature.java new file mode 100644 index 000000000..458827a51 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeActionFeature.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerBundle; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.internal.StringUtils; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Command; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * LSP codeAction feature. + */ +@ApiStatus.Experimental +public class LSPCodeActionFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isCodeActionSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codeAction and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codeAction and false otherwise. + */ + public boolean isCodeActionSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isCodeActionSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + /** + * Returns true if quick fixes are enabled and false otherwise. + * + * @return true if quick fixes are enabled and false otherwise. + */ + public boolean isQuickFixesEnabled(@NotNull PsiFile file) { + return isEnabled(file); + } + + /** + * Returns true if intent action are enabled and false otherwise. + * + * @return true if intent action are enabled and false otherwise. + */ + public boolean isIntentionActionsEnabled(@NotNull PsiFile file) { + return isEnabled(file); + } + + /** + * Returns the IntelliJ intention action text from the given LSP code action and null to ignore the code action. + * + * @param codeAction the LSP code action. + * @return the IntelliJ intention action text from the given LSP code action and null to ignore the code action. + */ + @Nullable + public String getText(@NotNull CodeAction codeAction) { + return codeAction.getTitle(); + } + + /** + * Returns the IntelliJ intention action family name from the given LSP code action. + * + * @param codeAction the LSP code action. + * @return the IntelliJ intention action family name from the given LSP code action. + */ + @NotNull + public String getFamilyName(@NotNull CodeAction codeAction) { + String kind = codeAction.getKind(); + if (StringUtils.isNotBlank(kind)) { + switch (kind) { + case CodeActionKind.QuickFix: + return LanguageServerBundle.message("lsp.intention.code.action.kind.quickfix"); + case CodeActionKind.Refactor: + return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor"); + case CodeActionKind.RefactorExtract: + return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.extract"); + case CodeActionKind.RefactorInline: + return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.inline"); + case CodeActionKind.RefactorRewrite: + return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.rewrite"); + case CodeActionKind.Source: + return LanguageServerBundle.message("lsp.intention.code.action.kind.source"); + case CodeActionKind.SourceFixAll: + return LanguageServerBundle.message("lsp.intention.code.action.kind.source.fixAll"); + case CodeActionKind.SourceOrganizeImports: + return LanguageServerBundle.message("lsp.intention.code.action.kind.source.organizeImports"); + } + } + return LanguageServerBundle.message("lsp.intention.code.action.kind.empty"); + } + + /** + * Returns the IntelliJ intention action text from the given LSP command and null to ignore the command. + * + * @param command the LSP command. + * @return the IntelliJ intention action text from the given LSP command and null to ignore the command. + */ + @Nullable + public String getText(@NotNull Command command) { + return command.getTitle(); + } + + /** + * Returns the IntelliJ intention action family name from the given LSP command. + * + * @param command the LSP command. + * @return the IntelliJ intention action family name from the given LSP command. + */ + @NotNull + public String getFamilyName(@NotNull Command command) { + return "LSP Command"; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeLensFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeLensFeature.java new file mode 100644 index 000000000..c86e50e18 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCodeLensFeature.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.codeVision.CodeVisionEntry; +import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry; +import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry; +import com.intellij.psi.PsiFile; +import com.intellij.util.ui.UIUtil; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.commands.CommandExecutor; +import com.redhat.devtools.lsp4ij.commands.LSPCommandContext; +import com.redhat.devtools.lsp4ij.internal.StringUtils; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.Command; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; + +/** + * LSP codeLens feature. + */ +@ApiStatus.Experimental +public class LSPCodeLensFeature extends AbstractLSPDocumentFeature { + + public static class LSPCodeLensContext { + private final @NotNull PsiFile psiFile; + private final @NotNull LanguageServerItem languageServer; + private Boolean resolveCodeLensSupported; + + public LSPCodeLensContext(@NotNull PsiFile psiFile, @NotNull LanguageServerItem languageServer) { + this.psiFile = psiFile; + this.languageServer = languageServer; + } + + public @NotNull PsiFile getPsiFile() { + return psiFile; + } + + @NotNull + public LanguageServerItem getLanguageServer() { + return languageServer; + } + + public boolean isResolveCodeLensSupported() { + if (resolveCodeLensSupported == null) { + resolveCodeLensSupported = languageServer.getClientFeatures().getCodeLensFeature().isResolveCodeLensSupported(getPsiFile()); + } + return resolveCodeLensSupported; + } + + } + /** + * Create an IntelliJ {@link CodeVisionEntry} from the given LSP CodeLens and null otherwise (to ignore the LSP CodeLens). + * + * @param codeLens the LSP codeLens. + * @param providerId the code vision provider Id. + * @param codeLensContext the LSP CodeLens context. + * @return an IntelliJ {@link CodeVisionEntry} from the given LSP CodeLens and null otherwise (to ignore the LSP CodeLens). + */ + @Nullable + public CodeVisionEntry createCodeVisionEntry(@NotNull CodeLens codeLens, + @NotNull String providerId, + @NotNull LSPCodeLensContext codeLensContext) { + String text = getText(codeLens); + if (text == null) { + // Ignore the code vision entry + return null; + } + Command command = codeLens.getCommand(); + String commandId = command.getCommand(); + if (StringUtils.isEmpty(commandId)) { + // Create a simple text code vision. + return new TextCodeVisionEntry(text, providerId, null, text, text, Collections.emptyList()); + } + // Code lens defines a command, create a clickable code vsion to execute the command. + return new ClickableTextCodeVisionEntry(text, providerId, (e, editor) -> { + LSPCommandContext context = new LSPCommandContext(command, codeLensContext.getPsiFile(), LSPCommandContext.ExecutedBy.CODE_LENS, editor, codeLensContext.getLanguageServer()) + .setInputEvent(e); + if (codeLensContext.isResolveCodeLensSupported()) { + codeLensContext.getLanguageServer() + .getTextDocumentService() + .resolveCodeLens(codeLens) + .thenAcceptAsync(resolvedCodeLens -> { + if (resolvedCodeLens != null) { + UIUtil.invokeLaterIfNeeded(() -> + CommandExecutor.executeCommand(context)); + } + } + ); + } else { + CommandExecutor.executeCommand(context); + } + return null; + }, null, text, text, Collections.emptyList()); + } + + /** + * Returns the code vision entry text from the LSP CodeLens and null otherwise (to ignore the LSP CodeLens). + * + * @param codeLens the LSP CodeLens + * @return the code vision entry text from the LSP CodeLens and null otherwise (to ignore the LSP CodeLens). + */ + @Nullable + public String getText(@NotNull CodeLens codeLens) { + Command command = codeLens.getCommand(); + if (command == null || command.getTitle().isEmpty()) { + return null; + } + return command.getTitle(); + } + + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isCodeLensSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isCodeLensSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isCodeLensSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + /** + * Returns true if the file associated with a language server can support resolve codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support resolve codelens and false otherwise. + */ + public boolean isResolveCodeLensSupported(@NotNull PsiFile file) { + return LanguageServerItem.isResolveCodeLensSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPColorFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPColorFeature.java new file mode 100644 index 000000000..a25671127 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPColorFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP color feature. + */ +@ApiStatus.Experimental +public class LSPColorFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isColorSupported(file); + } + + /** + * Returns true if the file associated with a language server can support color and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support color and false otherwise. + */ + public boolean isColorSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isColorSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} 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..697a630a4 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * 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.completion.CompletionParameters; +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.codeInsight.completion.PrioritizedLookupElement; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.features.completion.CompletionPrefix; +import com.redhat.devtools.lsp4ij.internal.StringUtils; +import com.redhat.devtools.lsp4ij.ui.IconMapper; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionItemTag; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * LSP completion feature. + */ +@ApiStatus.Experimental +public class LSPCompletionFeature extends AbstractLSPDocumentFeature { + + public static class LSPCompletionContext { + + private final @NotNull CompletionParameters parameters; + private final @NotNull LanguageServerItem languageServer; + private Boolean signatureHelpSupported; + private Boolean resolveCompletionSupported; + + public LSPCompletionContext(@NotNull CompletionParameters parameters, @NotNull LanguageServerItem languageServer) { + this.parameters = parameters; + this.languageServer = languageServer; + } + + public @NotNull CompletionParameters getParameters() { + return parameters; + } + + public boolean isSignatureHelpSupported() { + if (signatureHelpSupported == null) { + signatureHelpSupported = languageServer.getClientFeatures().getSignatureHelpFeature().isSupported(parameters.getOriginalFile()); + } + return signatureHelpSupported; + } + + public boolean isResolveCompletionSupported() { + if (resolveCompletionSupported == null) { + resolveCompletionSupported = languageServer.getClientFeatures().getCompletionFeature().isResolveCompletionSupported(parameters.getOriginalFile()); + } + return resolveCompletionSupported; + } + + @NotNull + @ApiStatus.Internal + LanguageServerItem getLanguageServer() { + return languageServer; + } + } + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isCompletionSupported(file); + } + + /** + * Returns true if the file associated with a language server can support completion and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support completion and false otherwise. + */ + public boolean isCompletionSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isCompletionSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + /** + * Returns true if the file associated with a language server can support resolve completion and false otherwise. + * + * @param file the file. + * @return true the file associated with a language server can support resolve completion and false otherwise. + */ + public boolean isResolveCompletionSupported(@Nullable PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isResolveCompletionSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + /** + * Create a completion lookup element from the given LSP completion item and context and null otherwise. + * + * @param item the LSP completion item. + * @param context the LSP completion context. + * @return a completion lookup element from the given LSP completion item and context and null otherwise. + */ + @Nullable + public LookupElement createLookupElement(@NotNull CompletionItem item, + @NotNull LSPCompletionContext context) { + if (StringUtils.isBlank(item.getLabel())) { + // Invalid completion Item, ignore it + return null; + } + // Update text edit range, commitCharacters, ... with item defaults if needed + return new LSPCompletionProposal(item, context, this); + } + + /** + * Update the given IntelliJ lookup element presentation with the given LSP completion item. + * + * @param presentation the lookup element presentation to update. + * @param item the LSP completion . + */ + public void renderLookupElement(@NotNull LookupElementPresentation presentation, + @NotNull CompletionItem item) { + presentation.setItemText(this.getItemText(item)); + presentation.setTypeText(this.getTypeText(item)); + presentation.setIcon(this.getIcon(item)); + presentation.setStrikeout(this.isStrikeout(item)); + presentation.setTailText(this.getTailText(item)); + presentation.setItemTextBold(this.isItemTextBold(item)); + } + + /** + * Returns the IntelliJ lookup item text from the given LSP completion item and null otherwise. + * + * @param item the LSP completion item. + * @return the IntelliJ lookup item text from the given LSP completion item and null otherwise. + */ + @Nullable + public String getItemText(@NotNull CompletionItem item) { + return item.getLabel(); + } + + /** + * Returns the IntelliJ lookup type text from the given LSP completion item and null otherwise. + * + * @param item the LSP completion item. + * @return the IntelliJ lookup type text from the given LSP completion item and null otherwise. + */ + @Nullable + public String getTypeText(CompletionItem item) { + return item.getDetail(); + } + + /** + * Returns the IntelliJ lookup icon from the given LSP completion item and null otherwise. + * + * @param item the LSP completion item. + * @return the IntelliJ lookup icon from the given LSP completion item and null otherwise. + */ + @Nullable + public Icon getIcon(@NotNull CompletionItem item) { + return IconMapper.getIcon(item); + } + + /** + * Returns true if the IntelliJ lookup is strike out and false otherwise. + * + * @param item + * @return true if the IntelliJ lookup is strike out and false otherwise. + */ + public boolean isStrikeout(@NotNull CompletionItem item) { + return (item.getTags() != null && item.getTags().contains(CompletionItemTag.Deprecated)) + || (item.getDeprecated() != null && item.getDeprecated().booleanValue()); + } + + /** + * Returns the IntelliJ lookup tail text from the given LSP completion item and null otherwise. + * + * @param item the LSP completion item. + * @return the IntelliJ lookup tail text from the given LSP completion item and null otherwise. + */ + @Nullable + public String getTailText(@NotNull CompletionItem item) { + var labelDetails = item.getLabelDetails(); + return labelDetails != null ? labelDetails.getDetail() : null; + } + + /** + * Returns the IntelliJ lookup item text bold from the given LSP completion item and null otherwise. + * + * @param item the LSP completion item. + * @return the IntelliJ lookup item text bold from the given LSP completion item and null otherwise. + */ + public boolean isItemTextBold(@NotNull CompletionItem item) { + return item.getKind() != null && item.getKind() == CompletionItemKind.Keyword; + } + + public void addLookupItem(@NotNull CompletionPrefix completionPrefix, + @NotNull CompletionResultSet result, + @NotNull LookupElement lookupItem, + int priority, + @NotNull CompletionItem item) { + var prioritizedLookupItem = PrioritizedLookupElement.withPriority(lookupItem, priority); + // Compute the prefix + var textEditRange = ((LSPCompletionProposal) lookupItem).getTextEditRange(); + String prefix = textEditRange != null ? completionPrefix.getPrefixFor(textEditRange, item) : null; + if (prefix != null) { + // Add the IJ completion item (lookup item) by using the computed prefix + result.withPrefixMatcher(prefix) + .caseInsensitive() + .addElement(prioritizedLookupItem); + } else { + // Should happen rarely, only when text edit is for multi-lines or if completion is triggered outside the text edit range. + // Add the IJ completion item (lookup item) which will use the IJ prefix + result.caseInsensitive() + .addElement(prioritizedLookupItem); + } + } + + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java similarity index 91% rename from src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java rename to src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java index 271f4110d..af003fb5d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java @@ -8,7 +8,7 @@ * Contributors: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ -package com.redhat.devtools.lsp4ij.features.completion; +package com.redhat.devtools.lsp4ij.client.features; import com.intellij.codeInsight.AutoPopupController; import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; @@ -34,10 +34,13 @@ import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.commands.CommandExecutor; import com.redhat.devtools.lsp4ij.commands.LSPCommandContext; +import com.redhat.devtools.lsp4ij.features.completion.CompletionProposalTools; +import com.redhat.devtools.lsp4ij.features.completion.SnippetTemplateFactory; import com.redhat.devtools.lsp4ij.features.completion.snippet.LspSnippetIndentOptions; 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 org.slf4j.Logger; @@ -53,47 +56,42 @@ import static com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationHelper.getValidMarkupContents; import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; -import static com.redhat.devtools.lsp4ij.ui.IconMapper.getIcon; /** * LSP completion lookup element. */ +@ApiStatus.Internal public class LSPCompletionProposal extends LookupElement implements Pointer, Symbol, DocumentationTarget { + private static final Logger LOGGER = LoggerFactory.getLogger(LSPCompletionProposal.class); private final CompletionItem item; private final PsiFile file; - private final Boolean supportResolveCompletion; - private final boolean supportSignatureHelp; - private final LSPCompletionContributor completionContributor; // offset where completion has been triggered // ex : string.charA| private final int completionOffset; + private final LSPCompletionFeature.@NotNull LSPCompletionContext completionContext; + private final @NotNull LSPCompletionFeature completionFeature; // offset where prefix completion starts // ex : string.|charA private int prefixStartOffset; private final Editor editor; - private final LanguageServerItem languageServer; + private CompletableFuture resolvedCompletionItemFuture; - public LSPCompletionProposal(@NotNull PsiFile file, - @NotNull Editor editor, - int completionOffset, - @NotNull CompletionItem item, - @NotNull LanguageServerItem languageServer, - @NotNull LSPCompletionContributor completionContributor) { - this.file = file; + public LSPCompletionProposal(@NotNull CompletionItem item, + @NotNull LSPCompletionFeature.LSPCompletionContext completionContext, + @NotNull LSPCompletionFeature completionFeature) { + this.file = completionContext.getParameters().getOriginalFile(); this.item = item; - this.editor = editor; - this.languageServer = languageServer; - this.completionContributor = completionContributor; - this.completionOffset = completionOffset; + this.editor = completionContext.getParameters().getEditor(); + this.completionContext = completionContext; + this.completionOffset = completionContext.getParameters().getOffset(); this.prefixStartOffset = getPrefixStartOffset(editor.getDocument(), completionOffset); - this.supportResolveCompletion = languageServer.isResolveCompletionSupported(); - this.supportSignatureHelp = languageServer.isSignatureHelpSupported(); + this.completionFeature = completionFeature; putUserData(CodeCompletionHandlerBase.DIRECT_INSERTION, true); } @@ -115,7 +113,7 @@ public void handleInsert(@NotNull InsertionContext context) { } // Apply all text edits - apply(context.getDocument(), context.getCompletionChar(), 0, context.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET)); + apply(context.getDocument(), context.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET)); if (shouldStartTemplate(template)) { // LSP completion with snippet syntax, activate the inline template @@ -127,10 +125,10 @@ public void handleInsert(@NotNull InsertionContext context) { // Execute custom command of the completion item if needed Command command = item.getCommand(); if (command != null) { - executeCustomCommand(command, context.getFile(), context.getEditor(), languageServer); + executeCustomCommand(command, context.getFile(), context.getEditor(), completionContext.getLanguageServer()); } - if (supportSignatureHelp) { + if (completionContext.isSignatureHelpSupported()) { // The language server supports signature help, open the parameter info popup AutoPopupController popupController = AutoPopupController.getInstance(context.getProject()); if (popupController != null) { @@ -213,21 +211,9 @@ public String getLookupString() { return item.getLabel(); } - private boolean isDeprecated() { - return (item.getTags() != null && item.getTags().contains(CompletionItemTag.Deprecated)) - || (item.getDeprecated() != null && item.getDeprecated().booleanValue()); - } - @Override public void renderElement(LookupElementPresentation presentation) { - presentation.setItemText(item.getLabel()); - presentation.setTypeText(item.getDetail()); - presentation.setIcon(getIcon(item)); - if (isDeprecated()) { - presentation.setStrikeout(true); - } - var labelDetails = item.getLabelDetails(); - presentation.setTailText(labelDetails != null ? labelDetails.getDetail() : null); + completionFeature.renderLookupElement(presentation, item); } @Override @@ -265,10 +251,10 @@ public void renderElement(LookupElement element, LookupElementPresentation prese * @return true if the LSP completion item 'detail' must be resolved and false otherwise. */ public boolean needToResolveCompletionDetail() { - return item.getDetail() == null && supportResolveCompletion; + return item.getDetail() == null && completionContext.isResolveCompletionSupported(); } - protected void apply(Document document, char trigger, int stateMask, int offset) { + protected void apply(Document document, int offset) { String insertText = null; Either eitherTextEdit = item.getTextEdit(); TextEdit textEdit = null; @@ -334,7 +320,7 @@ protected void apply(Document document, char trigger, int stateMask, int offset) } List additionalEdits = item.getAdditionalTextEdits(); - if ((additionalEdits == null || additionalEdits.isEmpty()) && supportResolveCompletion) { + if ((additionalEdits == null || additionalEdits.isEmpty()) && completionContext.isResolveCompletionSupported()) { // The LSP completion item 'additionalEdits' is not filled, try to resolve it. CompletionItem resolved = getResolvedCompletionItem(); if (resolved != null) { @@ -450,14 +436,15 @@ public CompletionItem getItem() { */ private CompletionItem getResolvedCompletionItem() { if (resolvedCompletionItemFuture == null) { - resolvedCompletionItemFuture = languageServer.getServer() + resolvedCompletionItemFuture = completionContext.getLanguageServer().getServer() .getTextDocumentService() .resolveCompletionItem(item); } try { // Wait until the future is finished and stop the wait if there are some ProcessCanceledException. waitUntilDone(resolvedCompletionItemFuture, file); - } catch (ProcessCanceledException e) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility + } catch ( + ProcessCanceledException e) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility //TODO delete block when minimum required version is 2024.2 return null; } catch (CancellationException e) { @@ -591,8 +578,8 @@ public DocumentationResult computeDocumentation() { if (contents.isEmpty()) { return null; } - return DocumentationResult.documentation(convertToHtml(contents, file)); - } else if (supportResolveCompletion) { + return DocumentationResult.documentation(convertToHtml(contents, null, file)); + } else if (completionContext.isResolveCompletionSupported()) { if (resolvedCompletionItemFuture != null && resolvedCompletionItemFuture.isDone()) { CompletionItem resolved = getResolvedCompletionItem(); if (resolved != null) { @@ -602,7 +589,7 @@ public DocumentationResult computeDocumentation() { if (contents.isEmpty()) { return null; } - return DocumentationResult.documentation(convertToHtml(contents, file)); + return DocumentationResult.documentation(convertToHtml(contents, null, file)); } else { DocumentationResult.asyncDocumentation(() -> { // The LSP completion item 'documentation' is not filled, try to resolve it @@ -615,7 +602,7 @@ public DocumentationResult computeDocumentation() { if (contents.isEmpty()) { return null; } - return DocumentationResult.documentation(convertToHtml(contents, file)); + return DocumentationResult.documentation(convertToHtml(contents, null, file)); }); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDeclarationFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDeclarationFeature.java new file mode 100644 index 000000000..0a1fd79fe --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDeclarationFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP declaration feature. + */ +@ApiStatus.Experimental +public class LSPDeclarationFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDeclarationSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isDeclarationSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDeclarationSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDefinitionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDefinitionFeature.java new file mode 100644 index 000000000..5232a11b0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDefinitionFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP definition feature. + */ +@ApiStatus.Experimental +public class LSPDefinitionFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDefinitionSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isDefinitionSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDefinitionSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} 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..6a2ffdc8e --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDiagnosticFeature.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * 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.intellij.psi.PsiFile; +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 diagnostic feature. + */ +@ApiStatus.Experimental +public class LSPDiagnosticFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return true; + } + + /** + * 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/LSPDocumentHighlightFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentHighlightFeature.java new file mode 100644 index 000000000..001d742b6 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentHighlightFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP documentHighlight feature. + */ +@ApiStatus.Experimental +public class LSPDocumentHighlightFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDocumentHighlightSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isDocumentHighlightSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDocumentHighlightSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentLinkFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentLinkFeature.java new file mode 100644 index 000000000..cccf22df8 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentLinkFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP documentLink feature. + */ +@ApiStatus.Experimental +public class LSPDocumentLinkFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDocumentLinkSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isDocumentLinkSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDocumentLinkSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentSymbolFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentSymbolFeature.java new file mode 100644 index 000000000..a89ea5254 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPDocumentSymbolFeature.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * 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.ide.structureView.StructureViewTreeElement; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.features.documentSymbol.DocumentSymbolData; +import com.redhat.devtools.lsp4ij.features.documentSymbol.LSPDocumentSymbolStructureViewModel; +import com.redhat.devtools.lsp4ij.ui.IconMapper; +import org.eclipse.lsp4j.DocumentSymbol; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * LSP documentSymbol feature. + */ +@ApiStatus.Experimental +public class LSPDocumentSymbolFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDocumentSymbolSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isDocumentSymbolSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDocumentSymbolSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + @Nullable + public StructureViewTreeElement getStructureViewTreeElement(@NotNull DocumentSymbolData documentSymbol) { + return new LSPDocumentSymbolStructureViewModel.LSPDocumentSymbolViewElement(documentSymbol); + } + + public @Nullable String getPresentableText(@NotNull DocumentSymbol documentSymbol, + @NotNull PsiFile psiFile) { + return documentSymbol.getName(); + } + + public @Nullable Icon getIcon(@NotNull DocumentSymbol documentSymbol, + @NotNull PsiFile psiFile, + boolean unused) { + return IconMapper.getIcon(documentSymbol.getKind()); + } + + public @Nullable String getLocationString(@NotNull DocumentSymbol documentSymbol, + @NotNull PsiFile psiFile) { + return documentSymbol.getDetail(); + } + + public void navigate(@NotNull DocumentSymbol documentSymbol, + @NotNull PsiFile psiFile, + boolean requestFocus) { + var selectionRange = documentSymbol.getSelectionRange(); + LSPIJUtils.openInEditor(psiFile.getVirtualFile(), selectionRange.getStart(), requestFocus, psiFile.getProject()); + } + + public boolean canNavigate(@NotNull DocumentSymbol documentSymbol, + @NotNull PsiFile psiFile) { + var selectionRange = documentSymbol.getSelectionRange(); + return selectionRange != null && selectionRange.getStart() != null; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFoldingRangeFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFoldingRangeFeature.java new file mode 100644 index 000000000..1fa973f19 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFoldingRangeFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP foldingRange feature. + */ +@ApiStatus.Experimental +public class LSPFoldingRangeFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isFoldingRangeSupported(file); + } + + /** + * Returns true if the file associated with a language server can support folding range and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support folding range and false otherwise. + */ + public boolean isFoldingRangeSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isFoldingRangeSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} 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..028ea3a4b --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPFormattingFeature.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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.lang.LanguageFormatting; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP formatting feature. + */ +@ApiStatus.Experimental +public class LSPFormattingFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isEnabled(@NotNull PsiFile file) { + if (!isExistingFormatterOverrideable(file) && LanguageFormatting.INSTANCE.forContext(file) != null) { + return false; + } + return true; + } + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isDocumentFormattingSupported(file); + } + + /** + * Returns true if existing formatter are overrideable and false (default value) otherwise. + * + * @return true if existing formatter are overrideable and false (default value) otherwise. + */ + protected boolean isExistingFormatterOverrideable(@NotNull PsiFile file) { + return false; + } + + /** + * Returns true if the file associated with a language server can support formatting and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support formatting and false otherwise. + */ + public boolean isDocumentFormattingSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDocumentFormattingSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + /** + * Returns true if the file associated with a language server can support range formatting and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support range formatting and false otherwise. + */ + public boolean isDocumentRangeFormattingSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isDocumentRangeFormattingSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPHoverFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPHoverFeature.java new file mode 100644 index 000000000..0beade2c4 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPHoverFeature.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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.util.text.StringUtilRt; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * LSP hover feature. + */ +@ApiStatus.Experimental +public class LSPHoverFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isHoverSupported(file); + } + + /** + * Returns the HTML content from the given LSP Markup content and null otherwise. + * + * @param content the LSP Markup content. + * @param file the file. + * @return the HTML content from the given LSP Markup content and null otherwise. + */ + @Nullable + public String getContent(@NotNull MarkupContent content, + @NotNull PsiFile file) { + if (MarkupKind.MARKDOWN.equals(content.getKind())) { + return convertMarkDownToHtml(content, file); + } + return content.getValue(); + } + + @Nullable + protected String convertMarkDownToHtml(@NotNull MarkupContent content, + @NotNull PsiFile file) { + var project = file.getProject(); + return MarkdownConverter.getInstance(project) + .toHtml(StringUtilRt.convertLineSeparators(content.getValue()), file); + } + + /** + * Returns true if the file associated with a language server can support hover and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support hover and false otherwise. + */ + public boolean isHoverSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isHoverSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPImplementationFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPImplementationFeature.java new file mode 100644 index 000000000..6826312fa --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPImplementationFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP implementation feature. + */ +@ApiStatus.Experimental +public class LSPImplementationFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isImplementationSupported(file); + } + + /** + * Returns true if the file associated with a language server can support implementation and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support implementation and false otherwise. + */ + public boolean isImplementationSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isImplementationSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPInlayHintFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPInlayHintFeature.java new file mode 100644 index 000000000..aba832535 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPInlayHintFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP inlayHint feature. + */ +@ApiStatus.Experimental +public class LSPInlayHintFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isInlayHintSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isInlayHintSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isInlayHintSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPReferencesFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPReferencesFeature.java new file mode 100644 index 000000000..0a3187a3e --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPReferencesFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 references + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.client.features; + +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP references feature. + */ +@ApiStatus.Experimental +public class LSPReferencesFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isReferencesSupported(file); + } + + /** + * Returns true if the file associated with a language server can support references and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support references and false otherwise. + */ + public boolean isReferencesSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isReferencesSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPRenameFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPRenameFeature.java new file mode 100644 index 000000000..de2d6dec8 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPRenameFeature.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP rename feature. + */ +@ApiStatus.Experimental +public class LSPRenameFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isRenameSupported(file); + } + + /** + * Returns true if the file associated with a language server can support codelens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support codelens and false otherwise. + */ + public boolean isRenameSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isRenameSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } + + public boolean isWillRenameFilesSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isWillRenameFilesSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSemanticTokensFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSemanticTokensFeature.java new file mode 100644 index 000000000..e59b3915e --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSemanticTokensFeature.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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.editor.colors.TextAttributesKey; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * LSP semanticTokens feature. + */ +@ApiStatus.Experimental +public class LSPSemanticTokensFeature extends AbstractLSPDocumentFeature implements SemanticTokensColorsProvider { + + /** + * Returns the {@link TextAttributesKey} to use for colorization for the given token type and given token modifiers and null otherwise. + * + * @param tokenType the token type. + * @param tokenModifiers the token modifiers. + * @param file the Psi file. + * @return the {@link TextAttributesKey} to use for colorization for the given token type and given token modifiers and null otherwise. + */ + @Override + public @Nullable TextAttributesKey getTextAttributesKey(@NotNull String tokenType, + @NotNull List tokenModifiers, + @NotNull PsiFile file) { + return getClientFeatures() + .getServerWrapper() + .getServerDefinition() + .getSemanticTokensColorsProvider() + .getTextAttributesKey(tokenType,tokenModifiers, file); + } + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isSemanticTokensSupported(file); + } + + /** + * Returns true if the file associated with a language server can support semanticTokens and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support semanticTokens and false otherwise. + */ + public boolean isSemanticTokensSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isSemanticTokensSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSignatureHelpFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSignatureHelpFeature.java new file mode 100644 index 000000000..c74bfacf9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPSignatureHelpFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP signatureHelp feature. + */ +@ApiStatus.Experimental +public class LSPSignatureHelpFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isSignatureHelpSupported(file); + } + + /** + * Returns true if the file associated with a language server can support signatureHelp and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support signatureHelp and false otherwise. + */ + public boolean isSignatureHelpSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isSignatureHelpSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPTypeDefinitionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPTypeDefinitionFeature.java new file mode 100644 index 000000000..f42c6479d --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPTypeDefinitionFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP typeDefinition feature. + */ +@ApiStatus.Experimental +public class LSPTypeDefinitionFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isTypeDefinitionSupported(file); + } + + /** + * Returns true if the file associated with a language server can support typeDefinition and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support typeDefinition and false otherwise. + */ + public boolean isTypeDefinitionSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isTypeDefinitionSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPUsageFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPUsageFeature.java new file mode 100644 index 000000000..4c4bacdef --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPUsageFeature.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * LSP usage feature. + */ +@ApiStatus.Experimental +public class LSPUsageFeature extends AbstractLSPDocumentFeature { + + @Override + public boolean isSupported(@NotNull PsiFile file) { + return isUsageSupported(file); + } + + /** + * Returns true if the file associated with a language server can support usage and false otherwise. + * + * @param file the file. + * @return true if the file associated with a language server can support usage and false otherwise. + */ + public boolean isUsageSupported(@NotNull PsiFile file) { + // TODO implement documentSelector to use language of the given file + return LanguageServerItem.isUsageSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceSymbolFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceSymbolFeature.java new file mode 100644 index 000000000..cb01cc5ab --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceSymbolFeature.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * 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.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.ServerStatus; +import org.jetbrains.annotations.ApiStatus; + +/** + * LSP workspace symbol feature. + */ +@ApiStatus.Experimental +public class LSPWorkspaceSymbolFeature extends AbstractLSPWorkspaceFeature { + + @Override + public boolean isEnabled() { + var serverStatus = getServerStatus(); + return serverStatus == ServerStatus.starting || serverStatus == ServerStatus.started; + } + + @Override + public boolean isSupported() { + return LanguageServerItem.isWorkspaceSymbolSupported(getClientFeatures().getServerWrapper().getServerCapabilitiesSync()); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java b/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java index 67887a254..807563033 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java @@ -74,7 +74,7 @@ public static boolean executeCommand(LSPCommandContext context) { Command command = context.getCommand(); // 1. try to execute command on server side - if (executeCommandServerSide(command, context.getPreferredLanguageServer())) { + if (executeCommandServerSide(command, context.getPreferredLanguageServer(), context.getPreferredLanguageServerId(), context.getProject())) { return true; } @@ -117,7 +117,7 @@ public static boolean executeCommand(LSPCommandContext context) { String commandId = context.getCommand().getCommand(); var preferredLanguageServer = context.getPreferredLanguageServer(); String content = preferredLanguageServer != null ? - MarkdownConverter.getInstance(project).toHtml(LanguageServerBundle.message("lsp.command.error.with.ls.content", commandId, preferredLanguageServer.getServerWrapper().getServerDefinition().getDisplayName())): + MarkdownConverter.getInstance(project).toHtml(LanguageServerBundle.message("lsp.command.error.with.ls.content", commandId, preferredLanguageServer.getServerWrapper().getServerDefinition().getDisplayName())) : MarkdownConverter.getInstance(project).toHtml(LanguageServerBundle.message("lsp.command.error.without.ls.content", commandId)); Notification notification = new Notification(LSPNotificationConstants.LSP4IJ_GENERAL_NOTIFICATIONS_ID, @@ -134,47 +134,63 @@ public static boolean executeCommand(LSPCommandContext context) { /** * Execute LSP command on server side. * - * @param command the LSP Command to be executed. If {@code null} this method will - * do nothing. - * @param languageServer the language server for which the {@code command} is - * applicable. + * @param command the LSP Command to be executed. If {@code null} this method will + * do nothing. + * @param languageServer the language server for which the {@code command} is + * applicable. + * @param preferredLanguageServerId + * @param project * @return true if the LSP command on server side has been executed successfully and false otherwise. */ private static boolean executeCommandServerSide(@NotNull Command command, - @Nullable LanguageServerItem languageServer) { - CompletableFuture languageServerFuture = getLanguageServerForCommand(command, languageServer); + @Nullable LanguageServerItem languageServer, + @Nullable String preferredLanguageServerId, + @NotNull Project project) { + CompletableFuture languageServerFuture = getLanguageServerForCommand(command, languageServer, preferredLanguageServerId, project); if (languageServerFuture == null) { return false; } // Server can handle command - languageServerFuture.thenAcceptAsync(server -> { - ExecuteCommandParams params = new ExecuteCommandParams(); - params.setCommand(command.getCommand()); - params.setArguments(command.getArguments()); - server.getWorkspaceService() - .executeCommand(params) - .exceptionally(error -> { - // Language server throws an error when executing a command - // Display it with an IntelliJ notification. - var languageServerWrapper = languageServer.getServerWrapper(); - MessageParams messageParams = new MessageParams(MessageType.Error, error.getMessage()); - var languageServerDefinition = languageServerWrapper.getServerDefinition(); - ServerMessageHandler.showMessage(languageServerDefinition.getDisplayName(), messageParams, languageServerWrapper.getProject()); - return error; - }); - }); + languageServerFuture + .thenAcceptAsync(server -> { + if (server == null) { + MessageParams messageParams = new MessageParams(MessageType.Error, "TODO"); + ServerMessageHandler.showMessage("Unkwonw server id" + preferredLanguageServerId, messageParams, project); + return; + } + ExecuteCommandParams params = new ExecuteCommandParams(); + params.setCommand(command.getCommand()); + params.setArguments(command.getArguments()); + server.getWorkspaceService() + .executeCommand(params) + .exceptionally(error -> { + // Language server throws an error when executing a command + // Display it with an IntelliJ notification. + var languageServerWrapper = languageServer.getServerWrapper(); + MessageParams messageParams = new MessageParams(MessageType.Error, error.getMessage()); + var languageServerDefinition = languageServerWrapper.getServerDefinition(); + ServerMessageHandler.showMessage(languageServerDefinition.getDisplayName(), messageParams, project); + return error; + }); + }); return true; } @Nullable private static CompletableFuture getLanguageServerForCommand(@NotNull Command command, - @Nullable LanguageServerItem languageServer) { + @Nullable LanguageServerItem languageServer, + @Nullable String preferredLanguageServerId, @NotNull Project project) { if (languageServer != null && languageServer.supportsCommand(command)) { return languageServer .getServerWrapper() .getInitializedServer(); } + if (preferredLanguageServerId != null) { + return LanguageServerManager.getInstance(project) + .getLanguageServer(preferredLanguageServerId) + .thenApply(ls -> ls != null ? ls.getServer() : null); + } return null; } @@ -336,7 +352,7 @@ private static WorkspaceEdit createWorkspaceEdit(List commandArguments, } boolean hasTextEdits = changes.values() .stream() - .anyMatch(edits -> edits != null && !edits.isEmpty()); + .anyMatch(edits -> edits != null && !edits.isEmpty()); if (!hasTextEdits) { return null; } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/commands/LSPCommandContext.java b/src/main/java/com/redhat/devtools/lsp4ij/commands/LSPCommandContext.java index 3712b0037..eb7f6e67c 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/commands/LSPCommandContext.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/commands/LSPCommandContext.java @@ -28,7 +28,7 @@ */ public class LSPCommandContext { - public static enum ExecutedBy { + public enum ExecutedBy { CODE_LENS, INLAY_HINT, CODE_ACTION, @@ -53,6 +53,8 @@ public static enum ExecutedBy { private InputEvent inputEvent; @Nullable private LanguageServerItem preferredLanguageServer; + @Nullable + private String preferredLanguageServerId; public LSPCommandContext(@NotNull Command command, @NotNull PsiFile psiFile, @@ -98,6 +100,15 @@ public LSPCommandContext setPreferredLanguageServer(LanguageServerItem preferred return this; } + public @Nullable String getPreferredLanguageServerId() { + return preferredLanguageServerId; + } + + public LSPCommandContext setPreferredLanguageServerId(@Nullable String preferredLanguageServerId) { + this.preferredLanguageServerId = preferredLanguageServerId; + return this; + } + public boolean isShowNotificationError() { return showNotificationError; } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPDocumentFeatureSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPDocumentFeatureSupport.java index 1357582ac..fdc8e798d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPDocumentFeatureSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPDocumentFeatureSupport.java @@ -11,9 +11,15 @@ package com.redhat.devtools.lsp4ij.features; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; /** * Base class to consume LSP requests (ex : textDocument/codeLens) from all language servers applying to a given Psi file. @@ -67,4 +73,13 @@ protected synchronized CompletableFuture load(Params params) { this.modificationStamp = this.file.getModificationStamp(); return future; } + + protected static CompletableFuture> getLanguageServers(@NotNull PsiFile file, + @Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter) { + return LanguageServiceAccessor.getInstance(file.getProject()) + .getLanguageServers(file.getVirtualFile(), + beforeStartingServerFilter, + afterStartingServerFilter); + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPWorkspaceFeatureSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPWorkspaceFeatureSupport.java index f200d41ef..8f9a1d4cd 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPWorkspaceFeatureSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPWorkspaceFeatureSupport.java @@ -11,7 +11,15 @@ package com.redhat.devtools.lsp4ij.features; import com.intellij.openapi.project.Project; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; /** * Base class to consume LSP requests (ex : workspace/symbol) from all language servers applying to a given project. @@ -41,4 +49,11 @@ public AbstractLSPWorkspaceFeatureSupport(@NotNull Project project) { protected boolean checkValid() { return !project.isDisposed(); } + + protected static CompletableFuture> getLanguageServers(@NotNull Project project, + @Nullable Predicate beforeStartingServerFilter, + @Nullable Predicate afterStartingServerFilter) { + return LanguageServiceAccessor.getInstance(project) + .getLanguageServers(beforeStartingServerFilter, afterStartingServerFilter); + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/LSPLazyCodeActionIntentionAction.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/LSPLazyCodeActionIntentionAction.java index 67ab5eeff..175c59d4b 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/LSPLazyCodeActionIntentionAction.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/LSPLazyCodeActionIntentionAction.java @@ -27,9 +27,7 @@ import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.commands.CommandExecutor; import com.redhat.devtools.lsp4ij.commands.LSPCommandContext; -import com.redhat.devtools.lsp4ij.internal.StringUtils; import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.CodeActionKind; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.jetbrains.annotations.NotNull; @@ -42,6 +40,7 @@ */ public class LSPLazyCodeActionIntentionAction implements IntentionAction { + private LSPLazyCodeActionProvider lazyCodeActions; private final int index; @@ -148,46 +147,29 @@ private void loadCodeActionIfNeeded() { // The LSP code action has been already loaded. return; } - // Try to get the LSP code action from the given indes + // Try to get the LSP code action from the given index this.action = lazyCodeActions.getCodeActionAt(index); if (isValidCodeAction()) { + var codeActionFeature = getLanguageServer().getClientFeatures().getCodeActionFeature(); var action = this.action.getLeft().codeAction(); if (action.isRight()) { codeAction = action.getRight(); - title = action.getRight().getTitle(); - familyName = getFamilyName(codeAction); + title = codeActionFeature.getText(codeAction); + if (title != null) { + familyName = codeActionFeature.getFamilyName(codeAction); + } } else if (action.isLeft()) { command = action.getLeft(); - title = command.getTitle(); - familyName = "LSP Command"; + title = codeActionFeature.getText(command); + if (title != null) { + familyName = codeActionFeature.getFamilyName(command); + } } - } - } - - @NotNull - private static String getFamilyName(@NotNull CodeAction codeAction) { - String kind = codeAction.getKind(); - if (StringUtils.isNotBlank(kind)) { - switch (kind) { - case CodeActionKind.QuickFix: - return LanguageServerBundle.message("lsp.intention.code.action.kind.quickfix"); - case CodeActionKind.Refactor: - return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor"); - case CodeActionKind.RefactorExtract: - return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.extract"); - case CodeActionKind.RefactorInline: - return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.inline"); - case CodeActionKind.RefactorRewrite: - return LanguageServerBundle.message("lsp.intention.code.action.kind.refactor.rewrite"); - case CodeActionKind.Source: - return LanguageServerBundle.message("lsp.intention.code.action.kind.source"); - case CodeActionKind.SourceFixAll: - return LanguageServerBundle.message("lsp.intention.code.action.kind.source.fixAll"); - case CodeActionKind.SourceOrganizeImports: - return LanguageServerBundle.message("lsp.intention.code.action.kind.source.organizeImports"); + if (title == null) { + // The LSP code action feature returns null, ignore the code action + this.action = Either.forRight(Boolean.FALSE); } } - return LanguageServerBundle.message("lsp.intention.code.action.kind.empty"); } private boolean isValidCodeAction() { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionAction.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionAction.java index f8db12865..641424e33 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionAction.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionAction.java @@ -50,21 +50,17 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file /** * Create the LSP code action parameters for the given diagnostic and file. * + * @param editor the editor. * @param file the file. * @return the LSP code action parameters for the given diagnostic and file. */ - private static CodeActionParams createCodeActionParams(Editor editor, PsiFile file) { - CodeActionParams params = new CodeActionParams(); - params.setTextDocument(LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile())); - + private static CodeActionParams createCodeActionParams(@NotNull Editor editor, + @NotNull PsiFile file) { Document document = LSPIJUtils.getDocument(file.getVirtualFile()); Caret caret = editor.getCaretModel().getPrimaryCaret(); Range range = LSPIJUtils.toRange(caret.getSelectionRange(), document); - params.setRange(range); - CodeActionContext context = new CodeActionContext(Collections.emptyList()); context.setTriggerKind(CodeActionTriggerKind.Automatic); - params.setContext(context); - return params; + return new CodeActionParams(LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile()), range, context); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionCodeActionSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionCodeActionSupport.java index 3220509bb..bfe381e41 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionCodeActionSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeAction/intention/LSPIntentionCodeActionSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.codeAction.intention; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.features.codeAction.CodeActionData; import com.redhat.devtools.lsp4ij.features.codeAction.LSPLazyCodeActionProvider; @@ -61,16 +58,16 @@ public CompletableFuture> getCodeActions(CodeActionParams p protected CompletableFuture> doLoad(@NotNull CodeActionParams params, @NotNull CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getCodeActions(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getCodeActions(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getCodeActions(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getCodeActions(@NotNull PsiFile file, @NotNull CodeActionParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isCodeActionSupported) + return getLanguageServers(file, + f -> f.getCodeActionFeature().isIntentionActionsEnabled(file), + f -> f.getCodeActionFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have code action capability 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/codeLens/LSPCodeLensProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensProvider.java index 655b6cd90..744683cd3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensProvider.java @@ -11,8 +11,6 @@ package com.redhat.devtools.lsp4ij.features.codeLens; import com.intellij.codeInsight.codeVision.*; -import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry; -import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Editor; @@ -24,18 +22,15 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.util.ui.EDT; -import com.intellij.util.ui.UIUtil; import com.redhat.devtools.lsp4ij.LSPFileSupport; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.LanguageServersRegistry; -import com.redhat.devtools.lsp4ij.commands.CommandExecutor; -import com.redhat.devtools.lsp4ij.commands.LSPCommandContext; +import com.redhat.devtools.lsp4ij.client.features.LSPCodeLensFeature; import com.redhat.devtools.lsp4ij.internal.StringUtils; import kotlin.Pair; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CodeLensParams; -import org.eclipse.lsp4j.Command; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -43,8 +38,9 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -121,6 +117,7 @@ public CodeVisionState computeCodeVision(@NotNull Editor editor, Void uiData) { return CodeVisionState.Companion.getREADY_EMPTY(); } if (!data.isEmpty()) { + Map codeLensContexts = new HashMap<>(); // At this step codelens are sorted by line number // Create IJ CodeVision from LSP CodeLens // As CodeVision cannot support showing several CodeVision entries for the same line, we create @@ -160,12 +157,26 @@ public CodeVisionState computeCodeVision(@NotNull Editor editor, Void uiData) { } } if (codeLens.getCommand() != null) { + var codeLensFeature = codeLensData.languageServer().getClientFeatures().getCodeLensFeature(); // Code lens is valid, create the proper code vision entry and text range. String text = codeLens.getCommand().getTitle(); if (!StringUtils.isEmpty(text)) { - TextRange textRange = LSPIJUtils.toTextRange(codeLens.getRange(), editor.getDocument(), null, true); - CodeVisionEntry entry = createCodeVisionEntry(codeLens, nbCodeLensForCurrentLine, psiFile, codeLensData.languageServer()); - result.add(new Pair<>(textRange, entry)); + // If LSP CodeLens is the first lens for the current line, we use the same providerId as this LSPCodeLensProvider ('LSPCodeLensProvider) + // other we generate a providerId like 'LSPCodeLensProvider0' and as DummyCodeVisionProvider are registered with 'LSPCodeLensProvider0', etc + // the code vision entry will be updated correctly + // See https://github.com/JetBrains/intellij-community/blob/f18aa7b9d65ab4b03d75a26aaec1e726821dc4d7/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionHost.kt#L348 + var ls = codeLensData.languageServer(); + var context = codeLensContexts.get(ls); + if (context == null) { + context = new LSPCodeLensFeature.LSPCodeLensContext(psiFile, ls); + codeLensContexts.put(ls, context); + } + String providerId = nbCodeLensForCurrentLine == -1 ? getId() : getId() + nbCodeLensForCurrentLine; + CodeVisionEntry entry = codeLensFeature.createCodeVisionEntry(codeLens, providerId, context); + if (entry != null) { + TextRange textRange = LSPIJUtils.toTextRange(codeLens.getRange(), editor.getDocument(), null, true); + result.add(new Pair<>(textRange, entry)); + } } } previous = codeLensData; @@ -218,44 +229,6 @@ private static int getCodeLensLine(CodeLensData codeLensData) { return codeLensData.codeLens().getRange().getStart().getLine(); } - @NotNull - private TextCodeVisionEntry createCodeVisionEntry(@NotNull CodeLens codeLens, - int nbCodeLensForCurrentLine, - @NotNull PsiFile psiFile, - @NotNull LanguageServerItem languageServer) { - Command command = codeLens.getCommand(); - String text = getCodeLensContent(codeLens); - String commandId = command.getCommand(); - // If LSP CodeLens is the first lens for the current line, we use the same providerId as this LSPCodeLensProvider ('LSPCodeLensProvider) - // other we generate a providerId like 'LSPCodeLensProvider0' and as DummyCodeVisionProvider are registered with 'LSPCodeLensProvider0', etc - // the code vision entry will be updated correctly - // See https://github.com/JetBrains/intellij-community/blob/f18aa7b9d65ab4b03d75a26aaec1e726821dc4d7/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionHost.kt#L348 - String providerId = nbCodeLensForCurrentLine == -1 ? getId() : getId() + nbCodeLensForCurrentLine; - if (StringUtils.isEmpty(commandId)) { - // Create a simple text code vision. - return new TextCodeVisionEntry(text, providerId, null, text, text, Collections.emptyList()); - } - // Code lens defines a command, create a clickable code vsion to execute the command. - return new ClickableTextCodeVisionEntry(text, providerId, (e, editor) -> { - LSPCommandContext context = new LSPCommandContext(command, psiFile, LSPCommandContext.ExecutedBy.CODE_LENS, editor, languageServer) - .setInputEvent(e); - if (languageServer.isResolveCodeLensSupported()) { - languageServer.getTextDocumentService() - .resolveCodeLens(codeLens) - .thenAcceptAsync(resolvedCodeLens -> { - if (resolvedCodeLens != null) { - UIUtil.invokeLaterIfNeeded(() -> - CommandExecutor.executeCommand(context)); - } - } - ); - } else { - CommandExecutor.executeCommand(context); - } - return null; - }, null, text, text, Collections.emptyList()); - } - private static CompletableFuture> getCodeLenses(@NotNull PsiFile psiFile) { LSPCodeLensSupport codeLensSupport = LSPFileSupport.getSupport(psiFile).getCodeLensSupport(); var params = new CodeLensParams(LSPIJUtils.toTextDocumentIdentifier(psiFile.getVirtualFile())); @@ -281,11 +254,4 @@ public List getRelativeOrderings() { return List.of(new CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("lsp")); } - static String getCodeLensContent(CodeLens codeLens) { - Command command = codeLens.getCommand(); - if (command == null || command.getTitle().isEmpty()) { - return null; - } - return command.getTitle(); - } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensSupport.java index 1c5dabbd6..1e9a9a592 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeLens/LSPCodeLensSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.codeLens; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -29,8 +26,6 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; -import static com.redhat.devtools.lsp4ij.features.codeLens.LSPCodeLensProvider.getCodeLensContent; - /** * LSP codeLens support which loads and caches code lenses by consuming: * @@ -52,16 +47,16 @@ public CompletableFuture> getCodeLenses(CodeLensParams params @Override protected CompletableFuture> doLoad(CodeLensParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getCodeLenses(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getCodeLenses(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getCodeLenses(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getCodeLenses(@NotNull PsiFile file, @NotNull CodeLensParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isCodeLensSupported) + return getLanguageServers(file, + f -> f.getCodeLensFeature().isEnabled(file), + f -> f.getCodeLensFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have code lens capability @@ -107,7 +102,8 @@ private static CompletableFuture> getCodeLensesFor(CodeLensPa .getTextDocumentService() .resolveCodeLens(codeLens), languageServer, LSPRequestConstants.TEXT_DOCUMENT_RESOLVE_CODE_LENS); } - if (getCodeLensContent(codeLens) != null || resolvedCodeLensFuture != null) { + var codeLensFeature = languageServer.getClientFeatures().getCodeLensFeature(); + if (codeLensFeature.getText(codeLens) != null || resolvedCodeLensFuture != null) { // The codelens content is filled or the codelens must be resolved data.add(new CodeLensData(codeLens, languageServer, resolvedCodeLensFuture)); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorSupport.java index 963d09863..276291843 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.color; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -48,16 +45,16 @@ public CompletableFuture> getColors(DocumentColorParams params) protected CompletableFuture> doLoad(@NotNull DocumentColorParams params, @NotNull CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getColors(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getColors(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getColors(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getColors(@NotNull PsiFile file, @NotNull DocumentColorParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isColorSupported) + return getLanguageServers(file, + f -> f.getColorFeature().isEnabled(file), + f -> f.getColorFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have color capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java index a4c8fa0f7..24d4f3733 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java @@ -32,23 +32,17 @@ */ public class CompletionPrefix { - private final int completionOffset; private final Position completionPos; private final Document document; private final Map prefixCache; public CompletionPrefix(int completionOffset, Document document) { - this.completionOffset = completionOffset; this.document = document; this.completionPos = LSPIJUtils.toPosition(completionOffset, document); this.prefixCache = new HashMap<>(); } - public int getCompletionOffset() { - return completionOffset; - } - public Document getDocument() { return document; } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java index e64381a0b..5edea0dcb 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java @@ -13,11 +13,7 @@ import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionResultSet; -import com.intellij.codeInsight.completion.PrioritizedLookupElement; -import com.intellij.codeInsight.lookup.Lookup; -import com.intellij.codeInsight.lookup.LookupEvent; -import com.intellij.codeInsight.lookup.LookupListener; -import com.intellij.codeInsight.lookup.LookupManagerListener; +import com.intellij.codeInsight.lookup.*; import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; @@ -29,7 +25,8 @@ import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.LanguageServersRegistry; -import com.redhat.devtools.lsp4ij.internal.StringUtils; +import com.redhat.devtools.lsp4ij.client.features.LSPCompletionFeature; +import com.redhat.devtools.lsp4ij.client.features.LSPCompletionProposal; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.jetbrains.annotations.NotNull; @@ -100,7 +97,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No CompletionPrefix completionPrefix = new CompletionPrefix(offset, document); for (var item : data) { ProgressManager.checkCanceled(); - addCompletionItems(psiFile, editor, completionPrefix, item.completion(), item.languageServer(), result); + addCompletionItems(parameters, completionPrefix, item.completion(), item.languageServer(), result); } } } @@ -108,8 +105,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No private static final CompletionItemComparator completionProposalComparator = new CompletionItemComparator(); - private void addCompletionItems(@NotNull PsiFile file, - @NotNull Editor editor, + private void addCompletionItems(@NotNull CompletionParameters parameters, @NotNull CompletionPrefix completionPrefix, @NotNull Either, CompletionList> completion, @NotNull LanguageServerItem languageServer, @@ -128,48 +124,24 @@ private void addCompletionItems(@NotNull PsiFile file, items.sort(completionProposalComparator); int size = items.size(); - //Items now sorted by priority, low index == high priority + var completionFeature = languageServer.getClientFeatures().getCompletionFeature(); + LSPCompletionFeature.LSPCompletionContext context = new LSPCompletionFeature.LSPCompletionContext(parameters, languageServer); + // Items now sorted by priority, low index == high priority for (int i = 0; i < size; i++) { var item = items.get(i); - if (StringUtils.isBlank(item.getLabel())) { - // Invalid completion Item, ignore it - continue; - } ProgressManager.checkCanceled(); + // Update text edit range, commitCharacters, ... with item defaults if needed + updateWithItemDefaults(item, itemDefaults); // Create lookup item - var lookupItem = createLookupItem(file, editor, completionPrefix.getCompletionOffset(), item, itemDefaults, languageServer); - - var prioritizedLookupItem = PrioritizedLookupElement.withPriority(lookupItem, size - i); - // Compute the prefix - var textEditRange = lookupItem.getTextEditRange(); - String prefix = textEditRange != null ? completionPrefix.getPrefixFor(textEditRange, item) : null; - if (prefix != null) { - // Add the IJ completion item (lookup item) by using the computed prefix - result.withPrefixMatcher(prefix) - .caseInsensitive() - .addElement(prioritizedLookupItem); - } else { - // Should happen rarely, only when text edit is for multi-lines or if completion is triggered outside the text edit range. - // Add the IJ completion item (lookup item) which will use the IJ prefix - result.caseInsensitive() - .addElement(prioritizedLookupItem); + LookupElement lookupItem = completionFeature.createLookupElement(item, context); + if (lookupItem != null) { + completionFeature.addLookupItem(completionPrefix, result, lookupItem, size- i, item); } } } - private LSPCompletionProposal createLookupItem(@NotNull PsiFile file, - @NotNull Editor editor, - int completionOffset, - @NotNull CompletionItem item, - @Nullable CompletionItemDefaults itemDefaults, - @NotNull LanguageServerItem languageServer) { - // Update text edit range, commitCharacters, ... with item defaults if needed - updateWithItemDefaults(item, itemDefaults); - return new LSPCompletionProposal(file, editor, completionOffset, item, languageServer, this); - } - - private static void updateWithItemDefaults(@NotNull CompletionItem item, - @Nullable CompletionItemDefaults itemDefaults) { + protected void updateWithItemDefaults(@NotNull CompletionItem item, + @Nullable CompletionItemDefaults itemDefaults) { if (itemDefaults == null) { return; } @@ -206,7 +178,6 @@ private static void updateWithItemDefaults(@NotNull CompletionItem item, } } - /** * LSP lookup listener to track the selected completion item * and resolve if needed the LSP completionItem to get the detail diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java index e3f5105da..308af16ea 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.completion; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -54,16 +51,16 @@ public CompletableFuture> getCompletions(LSPCompletionParam protected CompletableFuture> doLoad(@NotNull LSPCompletionParams params, @NotNull CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getCompletions(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getCompletions(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getCompletions(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getCompletions(@NotNull PsiFile file, @NotNull LSPCompletionParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isCompletionSupported) + return getLanguageServers(file, + f -> f.getCompletionFeature().isEnabled(file), + f -> f.getCompletionFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have completion capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/declaration/LSPDeclarationSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/declaration/LSPDeclarationSupport.java index 671c8bead..f5f40f161 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/declaration/LSPDeclarationSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/declaration/LSPDeclarationSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.declaration; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -53,15 +50,15 @@ public CompletableFuture> getDeclarations(LSPDeclarationParams pa @Override protected CompletableFuture> doLoad(LSPDeclarationParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectDeclarations(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectDeclarations(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectDeclarations(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectDeclarations(@NotNull PsiFile file, @NotNull LSPDeclarationParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isDeclarationSupported) + return getLanguageServers(file, + f -> f.getDeclarationFeature().isEnabled(file), + f -> f.getDeclarationFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have declaration capability 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..9fe6adbbb 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,29 +14,24 @@ 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 org.eclipse.lsp4j.Diagnostic; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.net.URI; +import java.util.Collections; import java.util.List; /** @@ -95,148 +90,17 @@ 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. + var clientFeatures = diagnosticsForServer.getClientFeatures(); + var diagnosticSupport = clientFeatures.getDiagnosticFeature(); + if (!diagnosticSupport.isEnabled(file)) { 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(""); + List fixes = Collections.emptyList(); + var codeActionFeature = clientFeatures.getCodeActionFeature(); + if (codeActionFeature.isQuickFixesEnabled(file)) { + 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..6cecb2c8a 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,10 +13,11 @@ *******************************************************************************/ 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.client.features.LSPClientFeatures; import com.redhat.devtools.lsp4ij.features.codeAction.quickfix.LSPLazyCodeActions; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Position; @@ -53,6 +54,9 @@ public LSPDiagnosticsForServer(LanguageServerItem languageServer, VirtualFile fi this.diagnostics = Collections.emptyMap(); } + public LSPClientFeatures getClientFeatures() { + return languageServer.getClientFeatures(); + } /** * Update the new LSP published diagnosics. * @@ -63,7 +67,9 @@ public void update(List diagnostics) { this.diagnostics = toMap(diagnostics, this.diagnostics); } - private Map toMap(List diagnostics, Map existingDiagnostics) { + private Map toMap(List diagnostics, + Map existingDiagnostics) { + // Collect quick fixes from LSP code action Map map = new HashMap<>(diagnostics.size()); // Sort diagnostics by range List sortedDiagnostics = diagnostics @@ -134,7 +140,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/documentLink/LSPDocumentLinkSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkSupport.java index 9a4ec589f..c8fd67e7d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentLink/LSPDocumentLinkSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.documentLink; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -48,16 +45,15 @@ public CompletableFuture> getDocumentLinks(DocumentLinkPa protected CompletableFuture> doLoad(@NotNull DocumentLinkParams params, @NotNull CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getDocumentLinks(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getDocumentLinks(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getDocumentLinks(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getDocumentLinks(@NotNull PsiFile file, @NotNull DocumentLinkParams params, @NotNull CancellationSupport cancellationSupport) { - - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isDocumentLinkSupported) + return getLanguageServers(file, + f -> f.getDocumentLinkFeature().isEnabled(file), + f -> f.getDocumentLinkFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have document link capability @@ -77,8 +73,8 @@ protected CompletableFuture> doLoad(@NotNull DocumentLink } private static CompletableFuture> getDocumentLinksFor(@NotNull DocumentLinkParams params, - @NotNull LanguageServerItem languageServer, - @NotNull CancellationSupport cancellationSupport) { + @NotNull LanguageServerItem languageServer, + @NotNull CancellationSupport cancellationSupport) { return cancellationSupport.execute(languageServer .getTextDocumentService() .documentLink(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_DOCUMENT_LINK) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/DocumentSymbolData.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/DocumentSymbolData.java index 07a2340d4..713070a1e 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/DocumentSymbolData.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/DocumentSymbolData.java @@ -14,8 +14,8 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.FakePsiElement; -import com.redhat.devtools.lsp4ij.LSPIJUtils; -import com.redhat.devtools.lsp4ij.ui.IconMapper; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import org.eclipse.lsp4j.DocumentSymbol; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,57 +26,59 @@ /** * LSP document symbol data. */ -class DocumentSymbolData extends FakePsiElement { +public class DocumentSymbolData extends FakePsiElement { private static final DocumentSymbolData[] EMPTY_ARRAY = new DocumentSymbolData[0]; private final @NotNull DocumentSymbol documentSymbol; private final @NotNull PsiFile psiFile; private final DocumentSymbolData parent; + private final @NotNull LanguageServerItem languageServer; private DocumentSymbolData[] cachedChildren; public DocumentSymbolData(@NotNull DocumentSymbol documentSymbol, - @NotNull PsiFile psiFile) { - this(documentSymbol, psiFile,null); + @NotNull PsiFile psiFile, + @NotNull LanguageServerItem languageServer) { + this(documentSymbol, psiFile, languageServer, null); } public DocumentSymbolData(@NotNull DocumentSymbol documentSymbol, @NotNull PsiFile psiFile, + @NotNull LanguageServerItem languageServer, @Nullable DocumentSymbolData parent) { this.documentSymbol = documentSymbol; this.psiFile = psiFile; + this.languageServer = languageServer; this.parent = parent; } - private @NotNull DocumentSymbol getDocumentSymbol() { + public @NotNull DocumentSymbol getDocumentSymbol() { return documentSymbol; } @Override public @Nullable String getPresentableText() { - return documentSymbol.getName(); + return getClientFeatures().getDocumentSymbolFeature().getPresentableText(documentSymbol, psiFile); } @Override public @Nullable Icon getIcon(boolean unused) { - return IconMapper.getIcon(documentSymbol.getKind()); + return getClientFeatures().getDocumentSymbolFeature().getIcon(documentSymbol, psiFile, unused); } @Override public @Nullable String getLocationString() { - return documentSymbol.getDetail(); + return getClientFeatures().getDocumentSymbolFeature().getLocationString(documentSymbol, psiFile); } @Override public void navigate(boolean requestFocus) { - var selectionRange = getDocumentSymbol().getSelectionRange(); - LSPIJUtils.openInEditor(psiFile.getVirtualFile(), selectionRange.getStart(), psiFile.getProject()); + getClientFeatures().getDocumentSymbolFeature().navigate(documentSymbol, psiFile, requestFocus); } @Override public boolean canNavigate() { - var selectionRange = getDocumentSymbol().getSelectionRange(); - return selectionRange != null && selectionRange.getStart() != null; + return getClientFeatures().getDocumentSymbolFeature().canNavigate(documentSymbol, psiFile); } @Override @@ -92,12 +94,16 @@ public boolean canNavigate() { } if (cachedChildren == null) { cachedChildren = children.stream() - .map(child -> new DocumentSymbolData(child, psiFile, this)) + .map(child -> new DocumentSymbolData(child, psiFile, languageServer,this)) .toArray(DocumentSymbolData[]::new); } return cachedChildren; } + public @NotNull LSPClientFeatures getClientFeatures() { + return languageServer.getClientFeatures(); + } + @Override public @NotNull Project getProject() { return psiFile.getProject(); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolStructureViewModel.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolStructureViewModel.java index fad36d83e..9e94fe83a 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolStructureViewModel.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolStructureViewModel.java @@ -26,6 +26,7 @@ import javax.swing.*; import java.util.Collection; import java.util.Collections; +import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; @@ -96,7 +97,8 @@ public LSPFileStructureViewElement(@NotNull PsiFile psiFile) { return Collections.emptyList(); } return documentSymbols.stream() - .map(documentSymbol -> (StructureViewTreeElement) new LSPDocumentSymbolViewElement(documentSymbol)) + .map(documentSymbol -> getStructureViewTreeElement(documentSymbol)) + .filter(Objects::nonNull) .toList(); } return Collections.emptyList(); @@ -118,7 +120,7 @@ public LSPFileStructureViewElement(@NotNull PsiFile psiFile) { } } - static class LSPDocumentSymbolViewElement extends PsiTreeElementBase { + public static class LSPDocumentSymbolViewElement extends PsiTreeElementBase { public LSPDocumentSymbolViewElement(DocumentSymbolData documentSymbolData) { super(documentSymbolData); @@ -135,7 +137,8 @@ public LSPDocumentSymbolViewElement(DocumentSymbolData documentSymbolData) { return Collections.emptyList(); } return Stream.of(children) - .map(child -> (StructureViewTreeElement) new LSPDocumentSymbolViewElement(child)) + .map(child -> getStructureViewTreeElement(child)) + .filter(Objects::nonNull) .toList(); } @@ -145,4 +148,8 @@ public LSPDocumentSymbolViewElement(DocumentSymbolData documentSymbolData) { } } + private static @Nullable StructureViewTreeElement getStructureViewTreeElement(DocumentSymbolData documentSymbol) { + return documentSymbol.getClientFeatures().getDocumentSymbolFeature().getStructureViewTreeElement(documentSymbol); + } + } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolSupport.java index cde680260..49563769e 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentSymbol/LSPDocumentSymbolSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.documentSymbol; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -51,14 +48,12 @@ protected CompletableFuture> doLoad(DocumentSymbolParam return getDocumentSymbols(file, documentSymbolParams, cancellationSupport); } - private static @NotNull CompletableFuture> getDocumentSymbols(@NotNull PsiFile psiFile, + private static @NotNull CompletableFuture> getDocumentSymbols(@NotNull PsiFile file, @NotNull DocumentSymbolParams params, @NotNull CancellationSupport cancellationSupport) { - - Project project = psiFile.getProject(); - VirtualFile file = psiFile.getVirtualFile(); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isDocumentSymbolSupported) + return getLanguageServers(file, + f -> f.getDocumentSymbolFeature().isEnabled(file), + f -> f.getDocumentSymbolFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have document link capability @@ -69,7 +64,7 @@ protected CompletableFuture> doLoad(DocumentSymbolParam // Collect list of textDocument/documentSymbol future for each language servers List>> documentSymbolInformationPerServerFutures = languageServers .stream() - .map(languageServer -> getDocumentSymbolsFor(params, languageServer, psiFile, cancellationSupport)) + .map(languageServer -> getDocumentSymbolsFor(params, languageServer, file, cancellationSupport)) .toList(); // Merge list of textDocument/documentSymbol future in one future which return the list of document link @@ -92,12 +87,11 @@ private static CompletableFuture> getDocumentSymbolsFor return documentSymbol.stream() .filter(Objects::nonNull) .map(symbol -> { - if (symbol.isLeft()){ + if (symbol.isLeft()) { var si = symbol.getLeft(); - return new DocumentSymbolData(convertToDocumentSymbol(si), psiFile); - } - else { - return new DocumentSymbolData(symbol.getRight(), psiFile); + return new DocumentSymbolData(convertToDocumentSymbol(si), psiFile, languageServer); + } else { + return new DocumentSymbolData(symbol.getRight(), psiFile, languageServer); } }) .toList(); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/HoverData.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/HoverData.java new file mode 100644 index 000000000..9cbdec1d1 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/HoverData.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 http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.documentation; + +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.eclipse.lsp4j.Hover; +import org.jetbrains.annotations.NotNull; + +/** + * Hover Data + * + * @param hover the LSP Hover + * @param languageServer the language server which has created the codeLens. + */ +record HoverData(@NotNull Hover hover, + @NotNull LanguageServerItem languageServer) { + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationHelper.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationHelper.java index 79513fcbe..c6b0682d3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationHelper.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationHelper.java @@ -11,8 +11,9 @@ package com.redhat.devtools.lsp4ij.features.documentation; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtilRt; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.client.features.LSPHoverFeature; import com.redhat.devtools.lsp4ij.internal.StringUtils; import org.eclipse.lsp4j.*; import org.jetbrains.annotations.NotNull; @@ -126,6 +127,7 @@ private static boolean isValidContent(@Nullable String value) { * @return the converted HTML of the LSP markup content and an empty string otherwise. */ public static String convertToHtml(@NotNull List contents, + @Nullable LanguageServerItem languageServer, @NotNull PsiFile file) { Project project = file.getProject(); StringBuilder htmlBody = new StringBuilder(); @@ -133,10 +135,13 @@ public static String convertToHtml(@NotNull List contents, if (i > 0) { htmlBody.append("
"); } - MarkupContent content = contents.get(i); - htmlBody.append(MarkupKind.MARKDOWN.equals(content.getKind()) ? - MarkdownConverter.getInstance(project).toHtml(StringUtilRt.convertLineSeparators(content.getValue()), file) : - content.getValue()); + MarkupContent markupContent = contents.get(i); + var hoverFeatures = languageServer != null ? languageServer.getClientFeatures().getHoverFeature() + : new LSPHoverFeature(); + String content = hoverFeatures.getContent(markupContent, file); + if(content != null) { + htmlBody.append(content); + } } return htmlBody.toString(); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java index 4d2692086..781024887 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTarget.java @@ -15,6 +15,7 @@ import com.intellij.platform.backend.documentation.DocumentationTarget; import com.intellij.platform.backend.presentation.TargetPresentation; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LanguageServerItem; import org.eclipse.lsp4j.MarkupContent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,15 +30,18 @@ public class LSPDocumentationTarget implements DocumentationTarget { private final List contents; + private final LanguageServerItem languageServer; private final String presentationText; private final PsiFile file; - public LSPDocumentationTarget(List contents, + public LSPDocumentationTarget(@NotNull List contents, String presentationText, - @NotNull PsiFile file) { + @NotNull PsiFile file, + @NotNull LanguageServerItem languageServer) { this.contents = contents; this.presentationText = presentationText; this.file = file; + this.languageServer = languageServer; } @NotNull @@ -51,7 +55,7 @@ public TargetPresentation computePresentation() { @Nullable @Override public DocumentationResult computeDocumentation() { - return DocumentationResult.documentation(convertToHtml(contents, file)); + return DocumentationResult.documentation(convertToHtml(contents, languageServer, file)); } @NotNull diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java index 8908b3f3e..4882dfdf6 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java @@ -65,7 +65,7 @@ public class LSPDocumentationTargetProvider implements DocumentationTargetProvid } var params = new LSPHoverParams(LSPIJUtils.toTextDocumentIdentifier(file), LSPIJUtils.toPosition(offset, document), offset); LSPHoverSupport hoverSupport = LSPFileSupport.getSupport(psiFile).getHoverSupport(); - CompletableFuture> hoverFuture = hoverSupport.getHover(params); + CompletableFuture> hoverFuture = hoverSupport.getHover(params); try { waitUntilDone(hoverFuture, psiFile); @@ -81,7 +81,7 @@ public class LSPDocumentationTargetProvider implements DocumentationTargetProvid if (isDoneNormally(hoverFuture)) { // textDocument/hover has been collected correctly - List hovers = hoverFuture.getNow(null); + List hovers = hoverFuture.getNow(null); if (hovers != null) { return hovers .stream() @@ -95,9 +95,10 @@ public class LSPDocumentationTargetProvider implements DocumentationTargetProvid } @Nullable - private static DocumentationTarget toDocumentTarget(@Nullable Hover hover, + private static DocumentationTarget toDocumentTarget(@Nullable HoverData hoverData, @NotNull Document document, @NotNull PsiFile file) { + Hover hover = hoverData != null ? hoverData.hover() : null; if (hover == null) { return null; } @@ -106,7 +107,7 @@ private static DocumentationTarget toDocumentTarget(@Nullable Hover hover, return null; } String presentationText = getPresentationText(hover, document); - return new LSPDocumentationTarget(contents, presentationText, file); + return new LSPDocumentationTarget(contents, presentationText, file, hoverData.languageServer()); } private static String getPresentationText(@NotNull Hover hover, @NotNull Document document) { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPHoverSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPHoverSupport.java index 0bea503aa..063a982f9 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPHoverSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPHoverSupport.java @@ -10,17 +10,14 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.documentation; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; -import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -34,7 +31,7 @@ *
  • LSP 'textDocument/hover' requests
  • * */ -public class LSPHoverSupport extends AbstractLSPDocumentFeatureSupport> { +public class LSPHoverSupport extends AbstractLSPDocumentFeatureSupport> { private Integer previousOffset; @@ -42,7 +39,7 @@ public LSPHoverSupport(@NotNull PsiFile file) { super(file); } - public CompletableFuture> getHover(LSPHoverParams params) { + public CompletableFuture> getHover(LSPHoverParams params) { int offset = params.getOffset(); if (previousOffset != null && !previousOffset.equals(offset)) { super.cancel(); @@ -52,18 +49,17 @@ public CompletableFuture> getHover(LSPHoverParams params) { } @Override - protected CompletableFuture> doLoad(HoverParams params, CancellationSupport cancellationSupport) { + protected CompletableFuture> doLoad(HoverParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getHover(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getHover(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getHover(@NotNull VirtualFile file, - @NotNull Project project, - @NotNull HoverParams params, - @NotNull CancellationSupport cancellationSupport) { - - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isHoverSupported) + private static @NotNull CompletableFuture> getHover(@NotNull PsiFile file, + @NotNull HoverParams params, + @NotNull CancellationSupport cancellationSupport) { + return getLanguageServers(file, + f -> f.getHoverFeature().isEnabled(file), + f -> f.getHoverFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have hover capability @@ -72,7 +68,7 @@ protected CompletableFuture> doLoad(HoverParams params, Cancellation } // Collect list of textDocument/hover future for each language servers - List> hoverPerServerFutures = languageServers + List> hoverPerServerFutures = languageServers .stream() .map(languageServer -> getHoverFor(params, languageServer, cancellationSupport)) .toList(); @@ -82,13 +78,13 @@ protected CompletableFuture> doLoad(HoverParams params, Cancellation }); } - public static @NotNull CompletableFuture> mergeInOneFuture(@NotNull List> futures, - @NotNull CancellationSupport cancellationSupport) { + public static @NotNull CompletableFuture> mergeInOneFuture(@NotNull List> futures, + @NotNull CancellationSupport cancellationSupport) { CompletableFuture allFutures = cancellationSupport .execute(CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))); return allFutures.thenApply(Void -> { - List mergedDataList = new ArrayList<>(futures.size()); - for (CompletableFuture dataListFuture : futures) { + List mergedDataList = new ArrayList<>(futures.size()); + for (CompletableFuture dataListFuture : futures) { var data = dataListFuture.join(); if (data != null) { mergedDataList.add(data); @@ -98,12 +94,13 @@ protected CompletableFuture> doLoad(HoverParams params, Cancellation }); } - private static CompletableFuture getHoverFor(@NotNull HoverParams params, - @NotNull LanguageServerItem languageServer, - @NotNull CancellationSupport cancellationSupport) { + private static CompletableFuture<@Nullable HoverData> getHoverFor(@NotNull HoverParams params, + @NotNull LanguageServerItem languageServer, + @NotNull CancellationSupport cancellationSupport) { return cancellationSupport.execute(languageServer - .getTextDocumentService() - .hover(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_HOVER); + .getTextDocumentService() + .hover(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_HOVER) + .thenApply(hover -> hover != null ? new HoverData(hover, languageServer) : null); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkupContentData.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkupContentData.java new file mode 100644 index 000000000..185221404 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/MarkupContentData.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 http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.documentation; + +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.eclipse.lsp4j.MarkupContent; +import org.jetbrains.annotations.NotNull; + +/** + * MarkupContent Data + * + * @param markupContent the LSP MarkupContent + * @param languageServer the language server which has created the codeLens. + */ +record MarkupContentData(@NotNull MarkupContent markupContent, + @NotNull LanguageServerItem languageServer) { + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeSupport.java index 1ce251366..0372afc02 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.foldingRange; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -48,16 +45,16 @@ public CompletableFuture> getFoldingRanges(FoldingRangeReques @Override protected CompletableFuture> doLoad(FoldingRangeRequestParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getFoldingRanges(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getFoldingRanges(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getFoldingRanges(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getFoldingRanges(@NotNull PsiFile file, @NotNull FoldingRangeRequestParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isFoldingSupported) + return getLanguageServers(file, + f -> f.getFoldingRangeFeature().isEnabled(file), + f -> f.getFoldingRangeFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have folding range capability 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..6e7927764 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 @@ -12,18 +12,14 @@ import com.intellij.formatting.service.AsyncDocumentFormattingService; import com.intellij.formatting.service.AsyncFormattingRequest; -import com.intellij.lang.LanguageFormatting; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; 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 org.eclipse.lsp4j.ServerCapabilities; +import com.redhat.devtools.lsp4ij.*; +import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -108,21 +104,25 @@ public boolean isRunUnderProgress() { @Override public final boolean canFormat(@NotNull PsiFile file) { - if (!LanguageFormatting.INSTANCE.allForLanguage(file.getLanguage()).isEmpty()) { - // When IJ provides formatting for the language (ex : JAVA, HTML) - // the LSP formatting support is ignored to avoid having some conflicts when formatting is done - return false; - } if (!LanguageServersRegistry.getInstance().isFileSupported(file)) { // The file is not associated to a language server return false; } - // Check if the file can support formatting / range formatting Project project = file.getProject(); return LanguageServiceAccessor.getInstance(project) - .hasAny(file.getVirtualFile(), ls -> canSupportFormatting(ls.getServerCapabilitiesSync())); + .hasAny(file.getVirtualFile(), ls -> canSupportFormatting(ls, file)); } + private boolean canSupportFormatting(LanguageServerWrapper ls, PsiFile file) { + var formattingFeature= ls.getClientFeatures().getFormattingFeature(); + if (!formattingFeature.isEnabled(file)) { + return false; + } + return canSupportFormatting(formattingFeature, file); + } + + protected abstract boolean canSupportFormatting(LSPFormattingFeature formattingFeature, PsiFile file); + private static TextRange getFormattingRange(AsyncFormattingRequest formattingRequest) { List ranges = formattingRequest.getFormattingRanges(); if (ranges.isEmpty()) { @@ -136,5 +136,4 @@ private static TextRange getFormattingRange(AsyncFormattingRequest formattingReq return textRange; } - protected abstract boolean canSupportFormatting(@Nullable ServerCapabilities serverCapabilities); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingAndRangeBothService.java b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingAndRangeBothService.java index 8edd4b344..978a5e037 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingAndRangeBothService.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingAndRangeBothService.java @@ -10,10 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.formatting; -import com.redhat.devtools.lsp4ij.LanguageServerItem; -import org.eclipse.lsp4j.ServerCapabilities; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Set; @@ -30,7 +29,8 @@ public class LSPFormattingAndRangeBothService extends AbstractLSPFormattingServi } @Override - protected boolean canSupportFormatting(@Nullable ServerCapabilities serverCapabilities) { - return LanguageServerItem.isDocumentRangeFormattingSupported(serverCapabilities); + protected boolean canSupportFormatting(LSPFormattingFeature formattingFeature, PsiFile file) { + return formattingFeature.isDocumentRangeFormattingSupported(file); } + } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingOnlyService.java b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingOnlyService.java index 2bb435f7f..4f3aacb41 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingOnlyService.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingOnlyService.java @@ -10,10 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.formatting; -import com.redhat.devtools.lsp4ij.LanguageServerItem; -import org.eclipse.lsp4j.ServerCapabilities; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.Set; @@ -31,8 +30,9 @@ public class LSPFormattingOnlyService extends AbstractLSPFormattingService { } @Override - protected boolean canSupportFormatting(@Nullable ServerCapabilities serverCapabilities) { - return LanguageServerItem.isDocumentFormattingSupported(serverCapabilities) && - !LanguageServerItem.isDocumentRangeFormattingSupported(serverCapabilities); + protected boolean canSupportFormatting(LSPFormattingFeature formattingFeature, PsiFile file) { + return formattingFeature.isDocumentFormattingSupported(file) && + !formattingFeature.isDocumentRangeFormattingSupported(file); } + } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingSupport.java index f505944cc..665b224f2 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/formatting/LSPFormattingSupport.java @@ -14,14 +14,12 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProcessCanceledException; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.*; @@ -84,20 +82,20 @@ private static void handleError(@NotNull AsyncFormattingRequest formattingReques @Override protected CompletableFuture> doLoad(LSPFormattingParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getFormatting(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getFormatting(file, params, cancellationSupport); } - protected @NotNull CompletableFuture> getFormatting(@NotNull VirtualFile file, - @NotNull Project project, + protected @NotNull CompletableFuture> getFormatting(@NotNull PsiFile file, @NotNull LSPFormattingParams params, @NotNull CancellationSupport cancellationSupport) { boolean isRangeFormatting = params.textRange() != null; - Predicate filter = !isRangeFormatting ? - LanguageServerItem::isDocumentFormattingSupported : - capability -> (LanguageServerItem.isDocumentRangeFormattingSupported(capability) || - LanguageServerItem.isDocumentFormattingSupported(capability)); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, filter) + Predicate filter = !isRangeFormatting ? + f -> f.getFormattingFeature().isDocumentFormattingSupported(file) : + f -> f.getFormattingFeature().isDocumentFormattingSupported(file) || + f.getFormattingFeature().isDocumentRangeFormattingSupported(file); + return getLanguageServers(file, + f -> f.getFormattingFeature().isEnabled(file), + filter) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have formatting capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/highlight/LSPHighlightSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/highlight/LSPHighlightSupport.java index b6f745770..bd0c988b9 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/highlight/LSPHighlightSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/highlight/LSPHighlightSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.highlight; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.DocumentHighlight; @@ -55,16 +52,15 @@ public CompletableFuture> getHighlights(LSPDocumentHighl @Override protected CompletableFuture> doLoad(DocumentHighlightParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getHighlights(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getHighlights(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getHighlights(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getHighlights(@NotNull PsiFile file, @NotNull DocumentHighlightParams params, @NotNull CancellationSupport cancellationSupport) { - - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isDocumentHighlightSupported) + return getLanguageServers(file, + f -> f.getDocumentHighlightFeature().isEnabled(file), + f -> f.getDocumentHighlightFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have documentHighlights capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/implementation/LSPImplementationSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/implementation/LSPImplementationSupport.java index 7650fe786..220fdba4f 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/implementation/LSPImplementationSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/implementation/LSPImplementationSupport.java @@ -10,13 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.implementation; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; -import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -29,7 +25,7 @@ /** * LSP implementation support which collect: - * + * *
      *
    • textDocument/implementation
    • *
    @@ -54,16 +50,15 @@ public CompletableFuture> getImplementations(LSPImplementationPar @Override protected CompletableFuture> doLoad(LSPImplementationParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectImplementations(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectImplementations(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectImplementations(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectImplementations(@NotNull PsiFile file, @NotNull LSPImplementationParams params, @NotNull CancellationSupport cancellationSupport) { - var textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isImplementationSupported) + return getLanguageServers(file, + f -> f.getImplementationFeature().isEnabled(file), + f -> f.getImplementationFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have implementation capability @@ -83,8 +78,8 @@ protected CompletableFuture> doLoad(LSPImplementationParams param } private static CompletableFuture> getImplementationFor(LSPImplementationParams params, - LanguageServerItem languageServer, - CancellationSupport cancellationSupport) { + LanguageServerItem languageServer, + CancellationSupport cancellationSupport) { return cancellationSupport.execute(languageServer .getTextDocumentService() .implementation(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_IMPLEMENTATION) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsSupport.java index 608fa49c9..3fdbf6e25 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsSupport.java @@ -10,15 +10,12 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.inlayhint; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; -import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; -import com.redhat.devtools.lsp4ij.LSPRequestConstants; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintParams; import org.jetbrains.annotations.NotNull; @@ -50,16 +47,16 @@ public CompletableFuture> getInlayHints(InlayHintParams para @Override protected CompletableFuture> doLoad(InlayHintParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getInlayHints(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getInlayHints(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getInlayHints(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getInlayHints(@NotNull PsiFile file, @NotNull InlayHintParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isInlayHintSupported) + return getLanguageServers(file, + f -> f.getInlayHintFeature().isEnabled(file), + f -> f.getInlayHintFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have inlay hint capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPDefinitionSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPDefinitionSupport.java index 8ee39ca02..b4a8cb61b 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPDefinitionSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPDefinitionSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.navigation; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -53,15 +50,15 @@ public CompletableFuture> getDefinitions(LSPDefinitionParams para @Override protected CompletableFuture> doLoad(LSPDefinitionParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectDefinitions(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectDefinitions(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectDefinitions(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectDefinitions(@NotNull PsiFile psiFile, @NotNull LSPDefinitionParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isDefinitionSupported) + return getLanguageServers(psiFile, + f -> f.getDefinitionFeature().isEnabled(psiFile), + f -> f.getDefinitionFeature().isSupported(psiFile)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have definition capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPGotoDeclarationHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPGotoDeclarationHandler.java index 7274e5641..23ecd523d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPGotoDeclarationHandler.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/navigation/LSPGotoDeclarationHandler.java @@ -59,10 +59,11 @@ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement return PsiElement.EMPTY_ARRAY; } Document document = editor.getDocument(); + + // Consume LSP 'textDocument/definition' request LSPDefinitionSupport definitionSupport = LSPFileSupport.getSupport(psiFile).getDefinitionSupport(); var params = new LSPDefinitionParams(LSPIJUtils.toTextDocumentIdentifier(psiFile.getVirtualFile()), LSPIJUtils.toPosition(offset, document), offset); CompletableFuture> definitionsFuture = definitionSupport.getDefinitions(params); - try { waitUntilDone(definitionsFuture, psiFile); } catch (ProcessCanceledException ex) { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/refactoring/LSPRenamePsiElementProcessor.java b/src/main/java/com/redhat/devtools/lsp4ij/features/refactoring/LSPRenamePsiElementProcessor.java index 3adb42252..bc7c681d4 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/refactoring/LSPRenamePsiElementProcessor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/refactoring/LSPRenamePsiElementProcessor.java @@ -72,7 +72,9 @@ public void prepareRenaming(@NotNull PsiElement element, CancellationSupport cancellationSupport = new CancellationSupport(); var willRenameFilesFuture = LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file.getVirtualFile(), LanguageServerItem::isWillRenameFilesSupported) + .getLanguageServers(file.getVirtualFile(), + null, + f -> f.getRenameFeature().isWillRenameFilesSupported(file)) .thenComposeAsync(languageServerItems -> { if (languageServerItems.isEmpty()) { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/references/LSPReferenceSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/references/LSPReferenceSupport.java index ec03d8282..67a35240c 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/references/LSPReferenceSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/references/LSPReferenceSupport.java @@ -10,13 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.references; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; -import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -54,16 +50,15 @@ public CompletableFuture> getReferences(LSPReferenceParams params @Override protected CompletableFuture> doLoad(LSPReferenceParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectReferences(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectReferences(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectReferences(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectReferences(@NotNull PsiFile file, @NotNull LSPReferenceParams params, @NotNull CancellationSupport cancellationSupport) { - var textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isReferencesSupported) + return getLanguageServers(file, + f -> f.getReferencesFeature().isEnabled(file), + f -> f.getReferencesFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have reference capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPPrepareRenameSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPPrepareRenameSupport.java index 3c5da81ae..b7309c1da 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPPrepareRenameSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPPrepareRenameSupport.java @@ -10,13 +10,10 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.rename; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -60,16 +57,15 @@ public CompletableFuture> getPrepareRenameResult(L protected CompletableFuture> doLoad(@NotNull LSPPrepareRenameParams params, @NotNull CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getPrepareRenameResult(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getPrepareRenameResult(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> getPrepareRenameResult(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> getPrepareRenameResult(@NotNull PsiFile file, @NotNull LSPPrepareRenameParams params, @NotNull CancellationSupport cancellationSupport) { - - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isRenameSupported) + return getLanguageServers(file, + f -> f.getRenameFeature().isEnabled(file), + f -> f.getRenameFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have 'rename' support diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java index 908bd9ba5..8da403a07 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java @@ -10,12 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.semanticTokens; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.SemanticTokensLegend; @@ -47,16 +44,16 @@ public CompletableFuture getSemanticTokens(SemanticTokensPar @Override protected CompletableFuture doLoad(SemanticTokensParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getSemanticTokens(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getSemanticTokens(file, params, cancellationSupport); } - private static @NotNull CompletableFuture getSemanticTokens(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture getSemanticTokens(@NotNull PsiFile file, @NotNull SemanticTokensParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isSemanticTokensSupported) + return getLanguageServers(file, + f -> f.getSemanticTokensFeature().isEnabled(file), + f -> f.getSemanticTokensFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have folding range capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/signatureHelp/LSPSignatureHelpSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/signatureHelp/LSPSignatureHelpSupport.java index 6d8e3f99d..0f7f0ffe4 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/signatureHelp/LSPSignatureHelpSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/signatureHelp/LSPSignatureHelpSupport.java @@ -10,14 +10,11 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.signatureHelp; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; -import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; -import com.redhat.devtools.lsp4ij.LSPRequestConstants; +import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelpParams; import org.jetbrains.annotations.NotNull; @@ -43,16 +40,16 @@ public CompletableFuture getSignatureHelp(SignatureHelpParams par @Override protected CompletableFuture doLoad(SignatureHelpParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return getSignatureHelp(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return getSignatureHelp(file, params, cancellationSupport); } - private static @NotNull CompletableFuture getSignatureHelp(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture getSignatureHelp(@NotNull PsiFile file, @NotNull SignatureHelpParams params, @NotNull CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isSignatureHelpSupported) + return getLanguageServers(file, + f -> f.getSignatureHelpFeature().isEnabled(file), + f -> f.getSignatureHelpFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have signature help capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/typeDefinition/LSPTypeDefinitionSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/typeDefinition/LSPTypeDefinitionSupport.java index c6fa4e19b..0b05fd0ea 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/typeDefinition/LSPTypeDefinitionSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/typeDefinition/LSPTypeDefinitionSupport.java @@ -10,13 +10,9 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.typeDefinition; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; -import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -29,7 +25,7 @@ /** * LSP typeDefinition support which collect: - * + * *
      *
    • textDocument/typeDefinition
    • *
    @@ -54,16 +50,15 @@ public CompletableFuture> getTypeDefinitions(LSPTypeDefinitionPar @Override protected CompletableFuture> doLoad(LSPTypeDefinitionParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectTypeDefinitions(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectTypeDefinitions(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectTypeDefinitions(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectTypeDefinitions(@NotNull PsiFile file, @NotNull LSPTypeDefinitionParams params, @NotNull CancellationSupport cancellationSupport) { - var textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LanguageServerItem::isTypeDefinitionSupported) + return getLanguageServers(file, + f -> f.getTypeDefinitionFeature().isEnabled(file), + f -> f.getTypeDefinitionFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have typeDefinition capability diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceSymbol/LSPWorkspaceSymbolSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceSymbol/LSPWorkspaceSymbolSupport.java index c773452bd..9d2df7bf0 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceSymbol/LSPWorkspaceSymbolSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceSymbol/LSPWorkspaceSymbolSupport.java @@ -13,7 +13,6 @@ import com.intellij.openapi.project.Project; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.features.AbstractLSPWorkspaceFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; @@ -26,9 +25,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; /** * LSP workspace symbol support which loads and caches symbol result by consuming: @@ -54,32 +51,27 @@ protected CompletableFuture> doLoad(WorkspaceSymbolPar } private static @NotNull CompletableFuture> getWorkspaceSymbol(@NotNull Project project, - @NotNull WorkspaceSymbolParams params, - @NotNull CancellationSupport cancellationSupport) { - - - // TODO : support workspace symbol when language server is not started - Set languageServers = - LanguageServiceAccessor.getInstance(project).getStartedServers() - .stream() - .map(ls -> new LanguageServerItem(ls.getServer(), ls)) - .filter(LanguageServerItem::isWorkspaceSymbolSupported) - .collect(Collectors.toSet()); - - // Here languageServers is the list of language servers which have workspace symbol capability - if (languageServers.isEmpty()) { - return CompletableFuture.completedFuture(null); - } + @NotNull WorkspaceSymbolParams params, + @NotNull CancellationSupport cancellationSupport) { + return getLanguageServers(project, + f -> f.getWorkspaceSymbolFeature().isEnabled(), + f -> f.getWorkspaceSymbolFeature().isSupported()) + .thenComposeAsync(languageServers -> { + // Here languageServers is the list of language servers which have workspaceSymbol capability + if (languageServers.isEmpty()) { + return CompletableFuture.completedFuture(null); + } - // Collect list of workspace/symbol future for each language servers - List>> workspaceSymbolPerServerFutures = languageServers - .stream() - .map(languageServer -> getWorkspaceSymbolFor(params, languageServer, cancellationSupport, project)) - .filter(Objects::nonNull) - .toList(); + // Collect list of workspace/symbol future for each language servers + List>> workspaceSymbolPerServerFutures = languageServers + .stream() + .map(languageServer -> getWorkspaceSymbolFor(params, languageServer, cancellationSupport, project)) + .filter(Objects::nonNull) + .toList(); - // Merge list of workspace/symbol future in one future which return the list of workspace symbol data - return CompletableFutures.mergeInOneFuture(workspaceSymbolPerServerFutures, cancellationSupport); + // Merge list of workspace/symbol future in one future which return the list of workspace symbol data + return CompletableFutures.mergeInOneFuture(workspaceSymbolPerServerFutures, cancellationSupport); + }); } private static CompletableFuture> getWorkspaceSymbolFor(@NotNull WorkspaceSymbolParams params, 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..d1d556e70 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.LSPClientFeatures; 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 LSPClientFeatures createClientFeatures() { + LSPClientFeatures clientFeatures = null; + try { + clientFeatures = getFactory().createClientFeatures(); + } catch (Exception e) { + LOGGER.warn("Exception occurred while creating an instance of the LSP client features", e); //$NON-NLS-1$ + } + if (clientFeatures == null) { + clientFeatures = super.createClientFeatures(); + } + return clientFeatures; + } + @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(); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageClient.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageClient.java index e184552a5..031925160 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageClient.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageClient.java @@ -58,6 +58,7 @@ public void triggerChangeConfiguration() { super.triggerChangeConfiguration(); } + @Override public UserDefinedLanguageServerDefinition getServerDefinition() { return serverDefinition; } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSupport.java index 829e8a8c4..c833f7d29 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSupport.java @@ -11,15 +11,12 @@ package com.redhat.devtools.lsp4ij.usages; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPIJUtils; -import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.LSPRequestConstants; +import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; -import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; -import com.redhat.devtools.lsp4ij.LSPRequestConstants; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.jetbrains.annotations.NotNull; @@ -44,8 +41,7 @@ */ public class LSPUsageSupport extends AbstractLSPDocumentFeatureSupport> { - public static record LSPUsageSupportParams(@NotNull Position position) { - } + public record LSPUsageSupportParams(@NotNull Position position) {} public LSPUsageSupport(@NotNull PsiFile file) { super(file, false); @@ -54,16 +50,17 @@ public LSPUsageSupport(@NotNull PsiFile file) { @Override protected CompletableFuture> doLoad(LSPUsageSupportParams params, CancellationSupport cancellationSupport) { PsiFile file = super.getFile(); - return collectUsages(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + return collectUsages(file, params, cancellationSupport); } - private static @NotNull CompletableFuture> collectUsages(@NotNull VirtualFile file, - @NotNull Project project, + private static @NotNull CompletableFuture> collectUsages(@NotNull PsiFile file, @NotNull LSPUsageSupportParams params, @NotNull CancellationSupport cancellationSupport) { - var textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file); - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, LSPUsageSupport::isUsageSupported) + var textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile()); + Project project = file.getProject(); + return getLanguageServers(file, + f -> f.getUsageFeature().isEnabled(file), + f -> f.getUsageFeature().isSupported(file)) .thenComposeAsync(languageServers -> { // Here languageServers is the list of language servers which matches the given file // and which have usage (references, implementation, etc) capability @@ -208,12 +205,6 @@ private static ReferenceParams createReferenceParams(@NotNull TextDocumentIdenti return new ReferenceParams(textDocument, position, context); } - private static boolean isUsageSupported(ServerCapabilities serverCapabilities) { - return LanguageServerItem.isDeclarationSupported(serverCapabilities) || - LanguageServerItem.isTypeDefinitionSupported(serverCapabilities) || - LanguageServerItem.isDefinitionSupported(serverCapabilities) || - LanguageServerItem.isReferencesSupported(serverCapabilities) || - LanguageServerItem.isImplementationSupported(serverCapabilities); - } + } diff --git a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPFormattingFixtureTestCase.java b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPFormattingFixtureTestCase.java index 12ec670e0..d3b6e2a62 100644 --- a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPFormattingFixtureTestCase.java +++ b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPFormattingFixtureTestCase.java @@ -62,7 +62,7 @@ protected void assertFormatting(@NotNull String fileName, // to avoid having some block when ReadAction#compute is required (ex: call of LSP4IJUtils#getDocument). try { LanguageServiceAccessor.getInstance(file.getProject()) - .getLanguageServers(file.getVirtualFile(), null) + .getLanguageServers(file.getVirtualFile(), null, null) .get(5000, TimeUnit.MILLISECONDS); } catch (Exception e) { e.printStackTrace(); diff --git a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPRenameFixtureTestCase.java b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPRenameFixtureTestCase.java index 693050eb5..efd6bcc89 100644 --- a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPRenameFixtureTestCase.java +++ b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPRenameFixtureTestCase.java @@ -192,7 +192,7 @@ private void assertRename(@NotNull String fileName, // we wait for some ms. try { LanguageServiceAccessor.getInstance(file.getProject()) - .getLanguageServers(file.getVirtualFile(), null) + .getLanguageServers(file.getVirtualFile(), null, null) .get(5000, TimeUnit.MILLISECONDS); } catch (Exception e) { e.printStackTrace();