diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks2.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks2.java new file mode 100644 index 000000000..875b50b14 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks2.java @@ -0,0 +1,189 @@ +package org.springframework.ide.vscode.boot.java.links; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Stack; + +import org.eclipse.jdt.internal.compiler.ASTVisitor; +import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; +import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.internal.compiler.lookup.BlockScope; +import org.eclipse.jdt.internal.compiler.lookup.ClassScope; +import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaModuleData; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.Region; + +public abstract class AbstractSourceLinks2 implements SourceLinks { + + private static final Logger log = LoggerFactory.getLogger(AbstractSourceLinks2.class); + + private CompilationUnitCache cuCache; + + private JavaProjectFinder projectFinder; + + protected AbstractSourceLinks2(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + this.cuCache = cuCache; + this.projectFinder = projectFinder; + } + + @Override + public Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { + Optional url = project == null ? Optional.empty() : getSourceLinkUrlForFQName(project, fqName); + if (!url.isPresent()) { + for (IJavaProject jp : projectFinder.all()) { + if (jp != project) { + url = getSourceLinkUrlForFQName(jp, fqName); + if (url.isPresent()) { + break; + } + } + } + } + return url; + } + + private Optional getSourceLinkUrlForFQName(IJavaProject project, String fqName) { + IJavaModuleData classpathResource = project.getIndex().findClasspathResourceContainer(fqName); + if (classpathResource != null) { + File file = classpathResource.getContainer(); + if (file.isDirectory()) { + return javaSourceLinkUrl(project, fqName, classpathResource); + } else { + return jarSourceLinkUrl(project, fqName, classpathResource); + } + } + return Optional.empty(); + } + + @Override + public Optional sourceLinkUrlForClasspathResource(String path) { + return SourceLinks.sourceLinkUrlForClasspathResource(this, projectFinder, path); + } + + private Optional javaSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData folderModuleData) { + IClasspath classpath = project.getClasspath(); + return SourceLinks.sourceFromSourceFolder(fqName, classpath) + .map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName)); + } + + private String javaSourceLinkUrl(IJavaProject project, Path sourcePath, String fqName) { + Optional linkOptional = sourceLinkForResourcePath(sourcePath); + if (linkOptional.isPresent()) { + Optional positionLink = findCU(project, sourcePath.toUri()).map(cu -> positionLink(cu, fqName)); + return positionLink.isPresent() ? linkOptional.get() + positionLink.get() : linkOptional.get(); + } + return null; + } + + abstract protected String positionLink(CompilationUnitDeclaration cu, String fqName); + + private Optional findCU(IJavaProject project, URI uri) { + return cuCache == null ? Optional.empty() : cuCache.withCompilationUnitDeclaration(project, uri, compilationUnit -> Optional.ofNullable(compilationUnit)); + } + + abstract protected Optional jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData); + + private Optional jarSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) { + return jarLinkUrl(project, fqName, jarModuleData).map(sourceUrl -> { + Optional positionLink = findCUForFQNameFromJar(project, jarModuleData, fqName).map(cu -> positionLink(cu, fqName)); + return positionLink.isPresent() ? sourceUrl + positionLink.get() : sourceUrl; + }); + } + + private Optional findCUForFQNameFromJar(IJavaProject project, IJavaModuleData jarModuleData, String fqName) { + return IClasspathUtil.sourceContainer(project.getClasspath(), jarModuleData.getContainer()).map(url -> { + try { + return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName, jarModuleData.getModule()); + } catch (Exception e) { + log.warn("Failed to determine source URL from url={} fqName={}", url, fqName, e); + return null; + } + }).map(sourceUrl -> { + try { + return sourceUrl.toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + }).map(sourcePath -> findCU(project, sourcePath).orElse(null)); + } + + protected Region findTypeRegion(CompilationUnitDeclaration cu, String fqName) { + if (cu == null) { + return null; + } + int[] values = new int[] {0, -1}; + int lastDotIndex = fqName.lastIndexOf('.'); + String packageName = fqName.substring(0, lastDotIndex); + String typeName = fqName.substring(lastDotIndex + 1); + String[] tokens = new String[cu.currentPackage.tokens.length]; + for (int i = 0; i < cu.currentPackage.tokens.length; i++) { + tokens[i] = new String(cu.currentPackage.tokens[i]); + } + String cuPackageName = String.join(".", tokens); + if (packageName.equals(cuPackageName)) { + Stack visitedType = new Stack<>(); + + cu.traverse(new ASTVisitor() { + + private boolean visitDeclaration(TypeDeclaration node) { + visitedType.push(new String(node.name)); + if (values[1] < 0) { + if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { + values[0] = node.sourceStart; + values[1] = node.sourceEnd; + } + } + return values[1] < 0; + } + + @Override + public boolean visit(TypeDeclaration localTypeDeclaration, BlockScope scope) { + return visitDeclaration(localTypeDeclaration); + } + + @Override + public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) { + return visitDeclaration(memberTypeDeclaration); + } + + @Override + public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) { + return visitDeclaration(typeDeclaration); + } + + @Override + public void endVisit(TypeDeclaration localTypeDeclaration, BlockScope scope) { + visitedType.pop(); + super.endVisit(localTypeDeclaration, scope); + } + + @Override + public void endVisit(TypeDeclaration memberTypeDeclaration, ClassScope scope) { + visitedType.pop(); + super.endVisit(memberTypeDeclaration, scope); + } + + @Override + public void endVisit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) { + visitedType.pop(); + super.endVisit(typeDeclaration, scope); + } + + }, new CompilationUnitScope(cu, new CompilerOptions(CompilationUnitCache.createCompilerOptions())), false); + } + return values[1] < 0 ? null : new Region(values[0], values[1]); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java index 25a712f41..001c3798a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java @@ -55,7 +55,7 @@ public final class SourceLinkFactory { switch (LspClient.currentClient()) { case VSCODE: case THEIA: - return /*new VSCodeSourceLinks(cuCache);*/server == null ? new VSCodeSourceLinks(cuCache, projectFinder) :new JavaServerSourceLinks(server, projectFinder); + return /*new VSCodeSourceLinks(cuCache);*/server == null ? new VSCodeSourceLinks2(cuCache, projectFinder) :new JavaServerSourceLinks(server, projectFinder); case ECLIPSE: return /*new EclipseSourceLinks();*/server == null ? new EclipseSourceLinks(projectFinder) : new JavaServerSourceLinks(server, projectFinder); case ATOM: diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks2.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks2.java new file mode 100644 index 000000000..487a920e1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks2.java @@ -0,0 +1,48 @@ +package org.springframework.ide.vscode.boot.java.links; + +import java.nio.file.Path; +import java.util.Optional; + +import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.java.utils.CuDeclarationUtils; +import org.springframework.ide.vscode.commons.java.IJavaModuleData; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.Region; + +public class VSCodeSourceLinks2 extends AbstractSourceLinks2 { + + public VSCodeSourceLinks2(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + super(cuCache, projectFinder); + } + + @Override + public Optional sourceLinkForResourcePath(Path path) { + return Optional.of(path.toUri().toASCIIString()); + } + + @Override + protected String positionLink(CompilationUnitDeclaration cu, String fqName) { + if (cu != null) { + Region region = findTypeRegion(cu, fqName); + if (region != null) { + int line = CuDeclarationUtils.getLineNumber(cu, region.getOffset()); + int column = CuDeclarationUtils.getColumn(cu, region.getOffset()); + StringBuilder sb = new StringBuilder(); + sb.append('#'); + sb.append(line); + sb.append(','); + sb.append(column + 1); // 1-based columns? + return sb.toString(); + } + } + return null; + } + + @Override + protected Optional jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) { + return Optional.ofNullable(JdtJavaDocumentUriProvider.uri(project, fqName)).map(uri -> uri.toASCIIString()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CuDeclarationUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CuDeclarationUtils.java new file mode 100644 index 000000000..8b0c19b8c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CuDeclarationUtils.java @@ -0,0 +1,24 @@ +package org.springframework.ide.vscode.boot.java.utils; + +import java.util.Arrays; + +import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; + +public class CuDeclarationUtils { + + public static int getLineNumber(CompilationUnitDeclaration cu, int offset) { + int insertionIndex = Arrays.binarySearch(cu.compilationResult.lineSeparatorPositions, offset); + if (insertionIndex < 0) { + return -insertionIndex; + } + // start lines from 1. + return insertionIndex + 1; + } + + public static int getColumn(CompilationUnitDeclaration cu, int offset) { + int line = getLineNumber(cu, offset); + // line start from 1 hence -1 and -1 for getting end of previous line + return line == 0 ? offset : offset - cu.compilationResult.lineSeparatorPositions[line - 2] - 1; // -1 at end because offset is for the line separator on the previous line + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/SourceLinksTestConf.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/SourceLinksTestConf.java index 2ce2ec818..a45e6bba6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/SourceLinksTestConf.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/SourceLinksTestConf.java @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Import; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness; import org.springframework.ide.vscode.boot.java.links.SourceLinks; -import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks; +import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks2; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheVoid; @@ -66,7 +66,7 @@ public class SourceLinksTestConf { } @Bean SourceLinks sourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { - return new VSCodeSourceLinks(cuCache, projectFinder); + return new VSCodeSourceLinks2(cuCache, projectFinder); } @Bean MockProjectObserver mockProjectObserver(BootLanguageServerParams params) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java index 0606c1aa3..5a6bf6b6d 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java @@ -24,6 +24,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.gradle.internal.impldep.com.google.common.collect.ImmutableList; import org.junit.jupiter.api.Test; import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks; +import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks2; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.javadoc.JavaDocProviders; @@ -61,7 +62,7 @@ public class VSCodeSourceLinksTest { @Test void testJavaSourceUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); + Optional url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); assertTrue(url.isPresent()); Path projectPath = Paths.get(project.pom().getParent()); URI uri = URI.create(url.get()); @@ -78,7 +79,7 @@ public class VSCodeSourceLinksTest { @Test void testClasspathResourceOnTomcatUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), new JavaProjectFinder() { + Optional url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), new JavaProjectFinder() { @Override public Optional find(TextDocumentIdentifier doc) { @@ -107,7 +108,7 @@ public class VSCodeSourceLinksTest { @Test void testJarUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication"); + Optional url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication"); assertTrue(url.isPresent()); String headerPart = url.get().substring(0, url.get().indexOf('?')); assertEquals("jdt://contents/spring-boot-autoconfigure-1.5.8.RELEASE.jar/org.springframework.boot.autoconfigure/SpringBootApplication.class", headerPart); @@ -118,7 +119,7 @@ public class VSCodeSourceLinksTest { @Test void testJarUrlInnerType() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback"); + Optional url = new VSCodeSourceLinks2(new CompilationUnitCache(null, null, null), null).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback"); assertTrue(url.isPresent()); String headerPart = url.get().substring(0, url.get().indexOf('?')); assertEquals("jdt://contents/spring-web-4.3.12.RELEASE.jar/org.springframework.web.client/RestTemplate$AcceptHeaderRequestCallback.class", headerPart);