Skip to content

Commit

Permalink
feat: provide LSP API support
Browse files Browse the repository at this point in the history
Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Sep 28, 2024
1 parent 658901b commit 0d570cf
Show file tree
Hide file tree
Showing 35 changed files with 1,294 additions and 295 deletions.
92 changes: 92 additions & 0 deletions docs/LSPApi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 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 customizing the behavior of LSP features to customize:

* [LSP completion feature](#customize-lsp-completion-feature)
* [LSP diagnostic feature](#customize-lsp-diagnostic-feature)
* [LSP formatting feature](#customize-lsp-formatting-feature)

These custom supports are done:

* by extending the default classes `LSP*Feature` (e.g. creating a new class `MyLSPFormattingFeature` that extends
[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 some methods to customize the behavior.
* registering your custom classes with `LanguageServerFactory#createClientFeatures(@NotNull Project)`:

```java
package my.language.server;

import com.intellij.openapi.project.Project;
import com.redhat.devtools.lsp4ij.LanguageServerFactory;
import com.redhat.devtools.lsp4ij.client.features.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
}
}
```

## Customize LSP completion feature

TODO

## Customize LSP diagnostic feature

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);
}

}
```

## Customize LSP formatting feature

Here is an example of code that allows to execute the LSP formatter even if there is a specific formatter registered by an IntelliJ plugin

TODO: revisit this API to manage range formatting too.

```java
package my.language.server;

import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature;
import org.jetbrains.annotations.NotNull;

public class MyLSPFormattingFeature extends LSPFormattingFeature {

@Override
protected boolean isExistingFormatterOverrideable(@NotNull PsiFile file) {
// By default, isExistingFormatterOverrideable return false if it has custom formatter with psi
// returns true even if there is custom formatter
return true;
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -50,4 +51,7 @@ public interface LanguageServerFactory {
return LanguageServer.class;
}

@NotNull default LSPClientFeatures createClientFeatures() {
return new LSPClientFeatures();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1174,4 +1177,12 @@ public boolean isSignatureTriggerCharactersSupported(String charTyped) {
}
return triggerCharacters.contains(charTyped);
}

public LSPClientFeatures getClientFeatures() {
if (clientFeatures == null) {
clientFeatures = getServerDefinition().createClientFeatures();
clientFeatures.setServerWrapper(this);
}
return clientFeatures;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand All @@ -35,7 +36,6 @@
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
* Language server accessor.
Expand Down Expand Up @@ -63,11 +63,11 @@ public void handleRemoved(@NotNull LanguageServerDefinitionListener.LanguageServ
List<LanguageServerWrapper> 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);
}
}

Expand All @@ -81,7 +81,7 @@ public void handleChanged(@NotNull LanguageServerChangedEvent event) {
List<LanguageServerWrapper> serversToRestart = startedServers
.stream()
.filter(server -> event.serverDefinition.equals(server.getServerDefinition()))
.collect(Collectors.toList());
.toList();
serversToRestart.forEach(LanguageServerWrapper::restart);
}
}
Expand Down Expand Up @@ -123,7 +123,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<LanguageServerWrapper> matchedServers = new LinkedHashSet<>();
collectLanguageServersFromDefinition(null, project, Set.of(definition), matchedServers);
collectLanguageServersFromDefinition(null, project, Set.of(definition), matchedServers, null);
for (var ls : matchedServers) {
ls.restart();
}
Expand All @@ -144,7 +144,7 @@ public void findAndStartLanguageServerIfNeeded(LanguageServerDefinition definiti
*/
private void findAndStartLsForFile(@NotNull VirtualFile file,
@NotNull LanguageServerDefinition definition) {
getLanguageServers(file, null, definition);
getLanguageServers(file, null, null, definition);
}

/**
Expand All @@ -154,7 +154,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<LanguageServerWrapper> filter) {
var startedServers = getStartedServers();
Expand All @@ -177,21 +176,23 @@ public boolean hasAny(@NotNull VirtualFile file,

@NotNull
public CompletableFuture<List<LanguageServerItem>> getLanguageServers(@NotNull VirtualFile file,
@Nullable Predicate<ServerCapabilities> filter) {
return getLanguageServers(file, filter, null);
@Nullable Predicate<LSPClientFeatures> beforeStartingServerFilter,
@Nullable Predicate<LSPClientFeatures> afterStartingServerFilter) {
return getLanguageServers(file, beforeStartingServerFilter, afterStartingServerFilter, null);
}

@NotNull
public CompletableFuture<List<LanguageServerItem>> getLanguageServers(@NotNull VirtualFile file,
@Nullable Predicate<ServerCapabilities> filter,
@Nullable LanguageServerDefinition matchServerDefinition) {
private CompletableFuture<List<LanguageServerItem>> getLanguageServers(@NotNull VirtualFile file,
@Nullable Predicate<LSPClientFeatures> beforeStartingServerFilter,
@Nullable Predicate<LSPClientFeatures> afterStartingServerFilter,
@Nullable LanguageServerDefinition matchServerDefinition) {
URI uri = LSPIJUtils.toUri(file);
if (uri == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}

// Collect started (or not) language servers which matches the given file.
CompletableFuture<Collection<LanguageServerWrapper>> matchedServers = getMatchedLanguageServersWrappers(file, matchServerDefinition);
CompletableFuture<Collection<LanguageServerWrapper>> 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());
Expand All @@ -209,12 +210,12 @@ public CompletableFuture<List<LanguageServerItem>> getLanguageServers(@NotNull V
return matchedServers
.thenComposeAsync(result -> CompletableFuture.allOf(result
.stream()
.filter(wrapper -> !wrapper.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);
Expand Down Expand Up @@ -249,7 +250,8 @@ public void projectClosing(Project project) {
@NotNull
private CompletableFuture<Collection<LanguageServerWrapper>> getMatchedLanguageServersWrappers(
@NotNull VirtualFile file,
@Nullable LanguageServerDefinition matchServerDefinition) {
@Nullable LanguageServerDefinition matchServerDefinition,
@Nullable Predicate<LSPClientFeatures> beforeStartingServerFilter) {
MatchedLanguageServerDefinitions mappings = getMatchedLanguageServerDefinitions(file, project, false);
if (mappings == MatchedLanguageServerDefinitions.NO_MATCH) {
// There are no mapping for the given file
Expand All @@ -264,17 +266,17 @@ private CompletableFuture<Collection<LanguageServerWrapper>> getMatchedLanguageS
if (!serverDefinitions.contains(matchServerDefinition)) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
collectLanguageServersFromDefinition(file, project, Set.of(matchServerDefinition), matchedServers);
collectLanguageServersFromDefinition(file, project, Set.of(matchServerDefinition), matchedServers, beforeStartingServerFilter);
} else {
collectLanguageServersFromDefinition(file, project, serverDefinitions, matchedServers);
collectLanguageServersFromDefinition(file, project, serverDefinitions, matchedServers, beforeStartingServerFilter);
}

CompletableFuture<Set<LanguageServerDefinition>> async = mappings.getAsyncMatched();
if (async != null) {
// Collect async server definitions
return async
.thenApply(asyncServerDefinitions -> {
collectLanguageServersFromDefinition(file, project, asyncServerDefinitions, matchedServers);
collectLanguageServersFromDefinition(file, project, asyncServerDefinitions, matchedServers, beforeStartingServerFilter);
return matchedServers;
});
}
Expand All @@ -288,15 +290,21 @@ private CompletableFuture<Collection<LanguageServerWrapper>> getMatchedLanguageS
* @param fileProject the file project.
* @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<LanguageServerDefinition> serverDefinitions, @NotNull Set<LanguageServerWrapper> matchedServers) {
private void collectLanguageServersFromDefinition(@Nullable VirtualFile file,
@NotNull Project fileProject,
@NotNull Set<LanguageServerDefinition> serverDefinitions,
@NotNull Set<LanguageServerWrapper> matchedServers,
@Nullable Predicate<LSPClientFeatures> 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;
Expand All @@ -306,8 +314,10 @@ 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);
if (beforeStartingServerFilter == null || beforeStartingServerFilter.test(wrapper.getClientFeatures())) {
startedServers.add(wrapper);
matchedServers.add(wrapper);
}
}
}
}
Expand Down Expand Up @@ -371,9 +381,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();
Expand Down Expand Up @@ -479,7 +487,8 @@ public List<LanguageServer> getActiveLanguageServers(Predicate<ServerCapabilitie
*/
@NotNull
public List<LanguageServer> getLanguageServers(@Nullable Project project,
Predicate<ServerCapabilities> request, boolean onlyActiveLS) {
@Nullable Predicate<ServerCapabilities> request,
boolean onlyActiveLS) {
List<LanguageServer> serverInfos = new ArrayList<>();
for (LanguageServerWrapper wrapper : startedServers) {
if ((!onlyActiveLS || wrapper.isActive()) && (project == null || wrapper.canOperate(project))) {
Expand Down
Loading

0 comments on commit 0d570cf

Please sign in to comment.