Skip to content

Commit

Permalink
feat: Support for textDocument/documentLink (#832)
Browse files Browse the repository at this point in the history
Fixes #832

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr authored and fbricon committed Jun 19, 2023
1 parent ce55178 commit 31aec3e
Show file tree
Hide file tree
Showing 19 changed files with 558 additions and 162 deletions.
59 changes: 54 additions & 5 deletions src/main/java/com/redhat/devtools/intellij/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public static void openInEditor(Location location, Project project) {

public static void openInEditor(String fileUri, Position position, Project project) {
VirtualFile file = findResourceFor(fileUri);
openInEditor(file, position, project);
}

public static void openInEditor(VirtualFile file, Position position, Project project) {
if (file != null) {
if (position == null) {
FileEditorManager.getInstance(project).openFile(file, true);
Expand Down Expand Up @@ -138,7 +142,7 @@ public static Document getDocument(VirtualFile docFile) {
return FileDocumentManager.getInstance().getDocument(docFile);
}

public static Module getProject(VirtualFile file) {
public static @Nullable Module getProject(VirtualFile file) {
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
Module module = ReadAction.compute(() -> ProjectFileIndex.getInstance(project).getModuleForFile(file));
if (module != null) {
Expand Down Expand Up @@ -178,6 +182,24 @@ public static Range toRange(TextRange range, Document document) {
return new Range(LSPIJUtils.toPosition(range.getStartOffset(), document), LSPIJUtils.toPosition(range.getEndOffset(), document));
}

/**
* Returns the IJ {@link TextRange} from the given LSP range and null otherwise.
*
* @param range the LSP range to conert.
* @param document the document.
*
* @return the IJ {@link TextRange} from the given LSP range and null otherwise.
*/
public static @Nullable TextRange toTextRange(Range range, Document document) {
final int start = LSPIJUtils.toOffset(range.getStart(), document);
final int end = LSPIJUtils.toOffset(range.getEnd(), document);
if (start >= end) {
// Language server reports invalid diagnostic, ignore it.
return null;
}
return new TextRange(start, end);
}

public static Location toLocation(PsiElement psiMember) {
PsiElement sourceElement = getNavigationElement(psiMember);
if (sourceElement != null) {
Expand Down Expand Up @@ -238,10 +260,8 @@ public static void applyWorkspaceEdit(WorkspaceEdit edit, String label) {
}
} else {
try {
URI targetURI = URI.create(createOperation.getUri());
File f = new File(targetURI);
f.createNewFile();
VfsUtil.findFileByIoFile(f, true);
String fileUri = createOperation.getUri();
createFile(fileUri);
} catch (IOException e) {
LOGGER.warn(e.getLocalizedMessage(), e);
}
Expand Down Expand Up @@ -273,6 +293,35 @@ public static void applyWorkspaceEdit(WorkspaceEdit edit, String label) {
}
}

/**
* Create the file with the given file Uri.
*
* @param fileUri the file Uri.
*
* @throws IOException
*
* @return the created virtual file and null otherwise.
*/
public static @Nullable VirtualFile createFile(String fileUri) throws IOException {
URI targetURI = URI.create(fileUri);
return createFile(targetURI);
}

/**
* Create the file with the given file Uri.
*
* @param fileUri the file Uri.
*
* @throws IOException
*
* @return the created virtual file and null otherwise.
*/
public static @Nullable VirtualFile createFile(URI fileUri) throws IOException {
File newFile = new File(fileUri);
newFile.createNewFile();
return VfsUtil.findFileByIoFile(newFile, true);
}

private static void applyWorkspaceEdit(Document document, List<TextEdit> edits) {
for (TextEdit edit : edits) {
if (edit.getRange() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.devtools.intellij.lsp4ij;

import com.intellij.openapi.vfs.VirtualFile;
import com.redhat.devtools.intellij.lsp4ij.operations.diagnostics.LSPDiagnosticsForServer;
import com.redhat.devtools.intellij.lsp4ij.operations.documentLink.LSPDocumentLinkForServer;

/**
* LSP data stored in {@link VirtualFile} which are used by some LSP operations.
*
* @author Angelo ZERR
*/
public class LSPVirtualFileData {

private final LSPDiagnosticsForServer diagnosticsForServer;

private final LSPDocumentLinkForServer documentLinkForServer;

public LSPVirtualFileData(LanguageServerWrapper languageServerWrapper, VirtualFile file) {
this.diagnosticsForServer = new LSPDiagnosticsForServer(languageServerWrapper,file);
this.documentLinkForServer = new LSPDocumentLinkForServer(languageServerWrapper, file);
}

public LSPDiagnosticsForServer getLSPDiagnosticsForServer() {
return diagnosticsForServer;
}

public LSPDocumentLinkForServer getDocumentLinkForServer() {
return documentLinkForServer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.redhat.devtools.intellij.lsp4ij.operations.diagnostics.LSPDiagnosticsForServer;
import com.redhat.devtools.intellij.lsp4ij.operations.documentLink.LSPDocumentLinkForServer;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DocumentLink;

import java.util.List;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* LSP wrapper for VirtualFile which maintains the current diagnostics
Expand All @@ -37,11 +41,11 @@ public class LSPVirtualFileWrapper {

private final VirtualFile file;

private final Map<LanguageServerWrapper, LSPDiagnosticsForServer /* current diagnostics */> diagnosticsPerServer;
private final Map<LanguageServerWrapper, LSPVirtualFileData /* current LSP data (diagnostics, documentLink, etc) */> dataPerServer;

LSPVirtualFileWrapper(VirtualFile file) {
this.file = file;
this.diagnosticsPerServer = new HashMap<>();
this.dataPerServer = new HashMap<>();
}

public VirtualFile getFile() {
Expand All @@ -57,11 +61,7 @@ public VirtualFile getFile() {
* @param languageServerWrapper the language server id which has published those diagnostics.
*/
public void updateDiagnostics(List<Diagnostic> diagnostics, LanguageServerWrapper languageServerWrapper) {
LSPDiagnosticsForServer diagnosticsForServer = diagnosticsPerServer.get(languageServerWrapper);
if (diagnosticsForServer == null) {
diagnosticsForServer = new LSPDiagnosticsForServer(languageServerWrapper, getFile());
diagnosticsPerServer.put(languageServerWrapper, diagnosticsForServer);
}
LSPDiagnosticsForServer diagnosticsForServer = getLSPVirtualFileData(languageServerWrapper).getLSPDiagnosticsForServer();
diagnosticsForServer.update(diagnostics);
}

Expand All @@ -71,18 +71,68 @@ public void updateDiagnostics(List<Diagnostic> diagnostics, LanguageServerWrappe
* @return all current diagnostics reported by all language servers mapped with the file.
*/
public Collection<LSPDiagnosticsForServer> getAllDiagnostics() {
if (diagnosticsPerServer.isEmpty()) {
return Collections.emptyList();
return getData(LSPVirtualFileData::getLSPDiagnosticsForServer);
}

// ------------------------ LSP textDocument/documentLink

/**
* Update the new collected documentLinks for the given language server id.
*
* @param documentLinks the new documentLink list
* @param languageServerWrapper the language server id which has collected those documentLinks.
*/
public void updateDocumentLink(List<DocumentLink> documentLinks, LanguageServerWrapper languageServerWrapper) {
LSPDocumentLinkForServer documentLinkForServer = getLSPVirtualFileData(languageServerWrapper).getDocumentLinkForServer();
documentLinkForServer.update(documentLinks);
}

/**
* Returns all current document link reported by all language servers mapped with the file.
*
* @return all current document link reported by all language servers mapped with the file.
*/
public Collection<LSPDocumentLinkForServer> getAllDocumentLink() {
return getData(LSPVirtualFileData::getDocumentLinkForServer);
}

// ------------------------ Other methods

private LSPVirtualFileData getLSPVirtualFileData(LanguageServerWrapper languageServerWrapper) {
LSPVirtualFileData dataForServer = dataPerServer.get(languageServerWrapper);
if (dataForServer != null) {
return dataForServer;
}
return diagnosticsPerServer.values();
return getOrCreateLSPVirtualFileData(languageServerWrapper);
}

public void dispose() {
this.diagnosticsPerServer.clear();
private synchronized LSPVirtualFileData getOrCreateLSPVirtualFileData(LanguageServerWrapper languageServerWrapper) {
LSPVirtualFileData dataForServer = dataPerServer.get(languageServerWrapper);
if (dataForServer != null) {
return dataForServer;
}
dataForServer = new LSPVirtualFileData(languageServerWrapper, getFile());
dataPerServer.put(languageServerWrapper, dataForServer);
return dataForServer;
}

private <T> Collection<T> getData(Function<LSPVirtualFileData, ? extends T> mapper) {
if (dataPerServer.isEmpty()) {
return Collections.emptyList();
}
return dataPerServer
.values()
.stream()
.map(mapper)
.collect(Collectors.toList());
}

// ------------------------ Static accessor

public void dispose() {
this.dataPerServer.clear();
}

/**
* Returns the LSPVirtualFileWrapper for the given file.
*
Expand All @@ -107,6 +157,10 @@ private static synchronized LSPVirtualFileWrapper getOrCreateLSPVirtualFileWrapp
return wrapper;
}

public static boolean hasWrapper(VirtualFile file) {
return file != null && file.getUserData(KEY) != null;
}

public static void dispose(VirtualFile file) {
LSPVirtualFileWrapper wrapper = file.getUserData(KEY);
if (wrapper != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.redhat.devtools.intellij.lsp4ij.server.StreamConnectionProvider;
import org.eclipse.lsp4j.ServerCapabilities;
Expand Down Expand Up @@ -561,20 +562,14 @@ public List<LSPDocumentInfo> getLSPDocumentInfosFor(@Nonnull Document document,
return res;
}

/**
* @param document
* @param filter
* @return
* @since 0.9
*/
@Nonnull
public CompletableFuture<List<LanguageServer>> getLanguageServers(@Nonnull Document document,
Predicate<ServerCapabilities> filter) {
public CompletableFuture<List<Pair<LanguageServerWrapper, LanguageServer>>> getLanguageServers(@Nonnull Document document,
Predicate<ServerCapabilities> filter) {
URI uri = LSPIJUtils.toUri(document);
if (uri == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
final List<LanguageServer> res = Collections.synchronizedList(new ArrayList<>());
final List<Pair<LanguageServerWrapper, LanguageServer>> res = Collections.synchronizedList(new ArrayList<>());
try {
return CompletableFuture.allOf(getLSWrappers(document).stream().map(wrapper ->
wrapper.getInitializedServer().thenComposeAsync(server -> {
Expand All @@ -588,15 +583,13 @@ public CompletableFuture<List<LanguageServer>> getLanguageServers(@Nonnull Docum
return CompletableFuture.completedFuture(null);
}).thenAccept(server -> {
if (server != null) {
res.add(server);
res.add(new Pair(wrapper, server));
}
})).toArray(CompletableFuture[]::new)).thenApply(theVoid -> res);
} catch (final Exception e) {
LOGGER.warn(e.getLocalizedMessage(), e);
}
return CompletableFuture.completedFuture(Collections.emptyList());


}

public boolean checkCapability(LanguageServer languageServer, Predicate<ServerCapabilities> condition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @
CompletableFuture<Void> future = LanguageServiceAccessor.getInstance(project)
.getLanguageServers(editor.getDocument(), capabilities -> capabilities.getCodeLensProvider() != null)
.thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream()
.map(languageServer -> languageServer.getTextDocumentService().codeLens(param)
.map(languageServer -> languageServer.getSecond().getTextDocumentService().codeLens(param)
.thenAcceptAsync(codeLenses -> {
// textDocument/codeLens may return null
if (codeLenses != null) {
codeLenses.stream().filter(Objects::nonNull)
.forEach(codeLens -> pairs.add(new Pair(codeLens, languageServer)));
.forEach(codeLens -> pairs.add(new Pair(codeLens, languageServer.getSecond())));
}
}))
.toArray(CompletableFuture[]::new)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper;
import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
Expand Down Expand Up @@ -51,7 +52,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No
Editor editor = parameters.getEditor();
Project project = parameters.getOriginalFile().getProject();
int offset = parameters.getOffset();
CompletableFuture<List<LanguageServer>> completionLanguageServersFuture = initiateLanguageServers(project, document);
CompletableFuture<List<Pair<LanguageServerWrapper, LanguageServer>>> completionLanguageServersFuture = initiateLanguageServers(project, document);
CompletionParams param;
try {
/*
Expand All @@ -63,8 +64,8 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No
BlockingDeque<Pair<Either<List<CompletionItem>, CompletionList>, LanguageServer>> proposals = new LinkedBlockingDeque<>();
CompletableFuture<Void> future = completionLanguageServersFuture
.thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream()
.map(languageServer -> languageServer.getTextDocumentService().completion(param)
.thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer))))
.map(languageServer -> languageServer.getSecond().getTextDocumentService().completion(param)
.thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer.getSecond()))))
.toArray(CompletableFuture[]::new)));
while (!future.isDone() || !proposals.isEmpty()) {
ProgressManager.checkCanceled();
Expand Down Expand Up @@ -108,7 +109,7 @@ private LookupElement createErrorProposal(int offset, Exception ex) {
return LookupElementBuilder.create("Error while computing completion", "");
}

private CompletableFuture<List<LanguageServer>> initiateLanguageServers(Project project, Document document) {
private CompletableFuture<List<Pair<LanguageServerWrapper, LanguageServer>>> initiateLanguageServers(Project project, Document document) {
return LanguageServiceAccessor.getInstance(project).getLanguageServers(document,
capabilities -> {
CompletionOptions provider = capabilities.getCompletionProvider();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,9 @@ public LSPVirtualFileWrapper collectInformation(@NotNull PsiFile file, @NotNull
}

@Override
public void apply(@NotNull PsiFile file, LSPVirtualFileWrapper editorWrapper, @NotNull AnnotationHolder holder) {
public void apply(@NotNull PsiFile file, LSPVirtualFileWrapper wrapper, @NotNull AnnotationHolder holder) {
// Get current LSP diagnostics of the current file
LSPVirtualFileWrapper fileWrapper = LSPVirtualFileWrapper.getLSPVirtualFileWrapper(file.getVirtualFile());
final Collection<LSPDiagnosticsForServer> diagnosticsPerServer = fileWrapper.getAllDiagnostics();
final Collection<LSPDiagnosticsForServer> diagnosticsPerServer = wrapper.getAllDiagnostics();
Document document = LSPIJUtils.getDocument(file.getVirtualFile());

// Loop for language server which report diagnostics for the given file
Expand All @@ -75,15 +74,13 @@ public void apply(@NotNull PsiFile file, LSPVirtualFileWrapper editorWrapper, @N
}

private static boolean createAnnotation(Diagnostic diagnostic, Document document, LSPDiagnosticsForServer diagnosticsForServer, AnnotationHolder holder) {
final int start = LSPIJUtils.toOffset(diagnostic.getRange().getStart(), document);
final int end = LSPIJUtils.toOffset(diagnostic.getRange().getEnd(), document);
if (start >= end) {
TextRange range = LSPIJUtils.toTextRange(diagnostic.getRange(), document);
if (range == null) {
// Language server reports invalid diagnostic, ignore it.
return false;
}
// Collect information required to create Intellij Annotations
HighlightSeverity severity = toHighlightSeverity(diagnostic.getSeverity());
TextRange range = new TextRange(start, end);
String message = diagnostic.getMessage();
List<IntentionAction> fixes = diagnosticsForServer.getQuickFixesFor(diagnostic);

Expand Down
Loading

0 comments on commit 31aec3e

Please sign in to comment.