diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index 6e09a7ffb..d268ec78b 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -43,6 +43,7 @@ import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor; import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet; import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext; import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetManager; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SpringIndexer; import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor; @@ -77,6 +78,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { private final SpringLiveHoverWatchdog liveHoverWatchdog; private final ProjectObserver projectObserver; private final BootJavaConfig config; + private final CompilationUnitCache cuCache; private final WordHighlighter testHightlighter = null; //new WordHighlighter("foo"); @@ -88,6 +90,10 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { this.config = new BootJavaConfig(); + projectFinder = serverParams.projectFinder; + projectObserver = serverParams.projectObserver; + cuCache = new CompilationUnitCache(projectFinder, getWorkspaceService().getFileObserver(), projectObserver); + propertyIndexProvider = serverParams.indexProvider; JavaProjectFinder javaProjectFinder = serverParams.projectFinder; @@ -126,9 +132,6 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { documents.onCodeLens(codeLensHandler::createCodeLenses); documents.onCodeLensResolve(codeLensHandler::resolveCodeLens); - projectFinder = serverParams.projectFinder; - projectObserver = serverParams.projectObserver; - liveHoverWatchdog = new SpringLiveHoverWatchdog(this, hoverInfoProvider, serverParams.runningAppProvider, projectFinder, projectObserver, serverParams.watchDogInterval); documents.onDidChangeContent(params -> { TextDocument doc = params.getDocument(); @@ -140,6 +143,14 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { } }); + documents.onDidClose(doc -> { + if (testHightlighter != null) { + getClient().highlight(new HighlightParams(doc.getId(), testHightlighter.apply(doc))); + } else { + liveHoverWatchdog.unwatchDocument(doc.getUri()); + } + }); + workspaceService.onDidChangeConfiguraton(settings -> { config.handleConfigurationChange(settings); if (config.isBootHintsEnabled()) { @@ -177,6 +188,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { public CompletableFuture shutdown() { this.liveHoverWatchdog.shutdown(); this.indexer.shutdown(); + this.cuCache.dispose(); return super.shutdown(); } @@ -332,4 +344,8 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { public BootJavaConfig getConfig() { return config; } + + public CompilationUnitCache getCompilationUnitCache() { + return cuCache; + } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index 014b16966..df8421d98 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -12,7 +12,6 @@ package org.springframework.ide.vscode.boot.java.autowired; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -24,14 +23,13 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; -import org.springframework.ide.vscode.boot.java.livehover.ASTUtils; import org.springframework.ide.vscode.boot.java.livehover.LiveHoverUtils; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; -import org.springframework.ide.vscode.commons.util.Optionals; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index 3e6b8c10b..c01dccb6d 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -10,19 +10,14 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.handlers; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.CompilationUnit; @@ -37,7 +32,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentPositionParams; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; -import org.springframework.ide.vscode.boot.java.livehover.ASTUtils; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -45,7 +40,6 @@ import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFin import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; -import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -91,17 +85,15 @@ public class BootJavaHoverProvider implements HoverHandler { public Range[] getLiveHoverHints(final TextDocument document, final SpringBootApp[] runningBootApps) { List result = new ArrayList<>(); - getProject(document).ifPresent(project -> { - try { - CompilationUnit cu = parse(document, project); - + try { + CompilationUnit cu = server.getCompilationUnitCache().getCompilationUnit(document); + if (cu != null) { cu.accept(new ASTVisitor() { @Override public boolean visit(SingleMemberAnnotation node) { try { extractLiveHints(node, document, runningBootApps, result); - } - catch (Exception e) { + } catch (Exception e) { Log.log(e); } @@ -112,8 +104,7 @@ public class BootJavaHoverProvider implements HoverHandler { public boolean visit(NormalAnnotation node) { try { extractLiveHints(node, document, runningBootApps, result); - } - catch (Exception e) { + } catch (Exception e) { Log.log(e); } @@ -124,18 +115,17 @@ public class BootJavaHoverProvider implements HoverHandler { public boolean visit(MarkerAnnotation node) { try { extractLiveHints(node, document, runningBootApps, result); - } - catch (Exception e) { + } catch (Exception e) { Log.log(e); } return super.visit(node); } }); - } catch (Exception e) { - Log.log(e); } - }); + } catch (Exception e) { + Log.log(e); + } return result.toArray(new Range[result.size()]); } @@ -167,7 +157,7 @@ public class BootJavaHoverProvider implements HoverHandler { private CompletableFuture provideHover(TextDocument document, int offset) throws Exception { IJavaProject project = getProject(document).orElse(null); if (project!=null) { - CompilationUnit cu = parse(document, project); + CompilationUnit cu = server.getCompilationUnitCache().getCompilationUnit(document); ASTNode node = NodeFinder.perform(cu, offset, 0); if (node != null) { return provideHoverForAnnotation(node, offset, document, project); @@ -176,30 +166,6 @@ public class BootJavaHoverProvider implements HoverHandler { return null; } - private CompilationUnit parse(TextDocument document, IJavaProject project) - throws Exception, BadLocationException { - ASTParser parser = ASTParser.newParser(AST.JLS8); - Map options = JavaCore.getOptions(); - JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options); - parser.setCompilerOptions(options); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setStatementsRecovery(true); - parser.setBindingsRecovery(true); - parser.setResolveBindings(true); - - String[] classpathEntries = getClasspathEntries(project); - String[] sourceEntries = new String[] {}; - parser.setEnvironment(classpathEntries, sourceEntries, null, true); - - String docURI = document.getUri(); - String unitName = docURI.substring(docURI.lastIndexOf("/")); - parser.setUnitName(unitName); - parser.setSource(document.get(0, document.getLength()).toCharArray()); - - CompilationUnit cu = (CompilationUnit) parser.createAST(null); - return cu; - } - private CompletableFuture provideHoverForAnnotation(ASTNode node, int offset, TextDocument doc, IJavaProject project) { Annotation annotation = null; @@ -261,14 +227,6 @@ public class BootJavaHoverProvider implements HoverHandler { return this.projectFinder.find(new TextDocumentIdentifier(doc.getUri())); } - private String[] getClasspathEntries(IJavaProject project) throws Exception { - IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); - return classpathEntries - .filter(path -> path.toFile().exists()) - .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); - } - private SpringBootApp[] getRunningSpringApps(IJavaProject project) { try { return runningAppProvider.getAllRunningSpringApps().toArray(new SpringBootApp[0]); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index a62a73431..f1f8eddd8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -24,6 +24,7 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java index 989813023..281e3522a 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover; +import static org.springframework.ide.vscode.boot.java.utils.ASTUtils.*; + import java.util.Collection; import java.util.List; import java.util.Optional; @@ -25,6 +27,7 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -33,8 +36,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableSet; -import static org.springframework.ide.vscode.boot.java.livehover.ASTUtils.*; - /** * @author Kris De Volder */ @@ -88,7 +89,7 @@ public class ActiveProfilesProvider implements HoverProvider { annotation.accept(new ASTVisitor() { @Override public boolean visit(StringLiteral node) { - String value = node.getLiteralValue(); + String value = ASTUtils.getLiteralValue(node); if (value!=null && allActiveProfiles.contains(value)) { rangeOf(doc, node).ifPresent(ranges::add); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java index 62545208b..de845a638 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java @@ -14,6 +14,7 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.util.Optionals; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 3335796e9..39c053d20 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.core.dom.MarkerAnnotation; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.springframework.ide.vscode.boot.java.autowired.Constants; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.StringUtil; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java index 62118146e..620449c76 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java @@ -20,21 +20,18 @@ import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; -import org.eclipse.jdt.core.dom.ArrayInitializer; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NormalAnnotation; -import org.eclipse.jdt.core.dom.QualifiedName; -import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; -import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.util.text.TextDocument; /** @@ -81,7 +78,7 @@ public class RequestMappingSymbolProvider implements SymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && valueName.equals("method")) { Expression expression = pair.getValue(); - methods = getExpressionValueAsArray(expression); + methods = ASTUtils.getExpressionValueAsArray(expression); break; } } @@ -111,14 +108,14 @@ public class RequestMappingSymbolProvider implements SymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && (valueName.equals("value") || valueName.equals("path"))) { Expression expression = pair.getValue(); - return getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression); } } } } else if (node.isSingleMemberAnnotation()) { SingleMemberAnnotation singleNode = (SingleMemberAnnotation) node; Expression expression = singleNode.getValue(); - return getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression); } return null; @@ -173,31 +170,4 @@ public class RequestMappingSymbolProvider implements SymbolProvider { return null; } - private static String getExpressionValueAsString(Expression exp) { - if (exp instanceof StringLiteral) { - return ((StringLiteral) exp).getLiteralValue(); - } else if (exp instanceof QualifiedName) { - return getExpressionValueAsString(((QualifiedName) exp).getName()); - } else if (exp instanceof SimpleName) { - return ((SimpleName) exp).getIdentifier(); - } else { - return null; - } - } - - @SuppressWarnings("unchecked") - private static String[] getExpressionValueAsArray(Expression exp) { - if (exp instanceof ArrayInitializer) { - ArrayInitializer array = (ArrayInitializer) exp; - return ((List) array.expressions()).stream().map(e -> getExpressionValueAsString(e)) - .filter(Objects::nonNull).toArray(String[]::new); - } else { - String rm = getExpressionValueAsString(exp); - if (rm != null) { - return new String[] { rm }; - } - } - return null; - } - } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java similarity index 79% rename from headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java rename to headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 6487529b3..9cf569fab 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -8,29 +8,28 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.livehover; +package org.springframework.ide.vscode.boot.java.utils; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ArrayInitializer; import org.eclipse.jdt.core.dom.Expression; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Range; -import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; -import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; -import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; public class ASTUtils { @@ -87,7 +86,7 @@ public class ASTUtils { */ public static Optional getFirstString(Expression exp) { if (exp instanceof StringLiteral) { - return Optional.ofNullable(((StringLiteral) exp).getLiteralValue()); + return Optional.ofNullable(getLiteralValue((StringLiteral) exp)); } else if (exp instanceof ArrayInitializer) { ArrayInitializer array = (ArrayInitializer) exp; Object objs = array.getStructuralProperty(ArrayInitializer.EXPRESSIONS_PROPERTY); @@ -143,4 +142,38 @@ public class ASTUtils { return null; } + public static String getLiteralValue(StringLiteral node) { + synchronized (node.getAST()) { + return node.getLiteralValue(); + } + } + + public static String getExpressionValueAsString(Expression exp) { + if (exp instanceof StringLiteral) { + return getLiteralValue((StringLiteral) exp); + } else if (exp instanceof QualifiedName) { + return getExpressionValueAsString(((QualifiedName) exp).getName()); + } else if (exp instanceof SimpleName) { + return ((SimpleName) exp).getIdentifier(); + } else { + return null; + } + } + + @SuppressWarnings("unchecked") + public + static String[] getExpressionValueAsArray(Expression exp) { + if (exp instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) exp; + return ((List) array.expressions()).stream().map(e -> getExpressionValueAsString(e)) + .filter(Objects::nonNull).toArray(String[]::new); + } else { + String rm = getExpressionValueAsString(exp); + if (rm != null) { + return new String[] { rm }; + } + } + return null; + } + } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java new file mode 100644 index 000000000..b42c7362d --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -0,0 +1,174 @@ +package org.springframework.ide.vscode.boot.java.utils; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; +import java.util.stream.Stream; + +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.FileObserver; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +public final class CompilationUnitCache { + + private static final String GLOB_ALL_JAVA_FILES = "**/*.java"; + + private JavaProjectFinder projectFinder; + private FileObserver fileObserver; + private ProjectObserver projectObserver; + private Cache uriToCu; + private Cache> projectToDocs; + private String fileChangeSubscription; + private String fileDeletedSubscription; + private ProjectObserver.Listener projectListener; + + private ReadLock readLock; + private WriteLock writeLock; + + public CompilationUnitCache(JavaProjectFinder projectFinder, FileObserver fileObserver, ProjectObserver projectObserver) { + this.projectFinder = projectFinder; + this.fileObserver = fileObserver; + this.projectObserver = projectObserver; + projectListener = new CUProjectListener(); + + uriToCu = CacheBuilder.newBuilder().build(); + projectToDocs = CacheBuilder.newBuilder().build(); + + if (this.fileObserver != null) { + fileChangeSubscription = this.fileObserver.onFileChanged(Collections.singletonList(GLOB_ALL_JAVA_FILES), (uri) -> invalidateCuForJavaFile(uri)); + fileDeletedSubscription = this.fileObserver.onFileDeleted(Collections.singletonList(GLOB_ALL_JAVA_FILES), (uri) -> invalidateCuForJavaFile(uri)); + } + + if (this.projectObserver != null) { + this.projectObserver.addListener(projectListener); + } + + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + } + + public void dispose() { + if (fileObserver != null) { + fileObserver.unsubscribe(fileChangeSubscription); + fileObserver.unsubscribe(fileDeletedSubscription); + } + if (projectObserver != null) { + projectObserver.removeListener(projectListener); + } + } + + public CompilationUnit getCompilationUnit(TextDocument document) throws Exception { + URI uri = URI.create(document.getUri()); + readLock.lock(); + try { + return uriToCu.get(uri, () -> parse(document)); + } finally { + readLock.unlock(); + } + } + + private void invalidateCuForJavaFile(String uriStr) { + URI uri = URI.create(uriStr); + writeLock.lock(); + try { + uriToCu.invalidate(uri); + } finally { + writeLock.unlock(); + } + } + + public static CompilationUnit parse(TextDocument document, IJavaProject project) throws Exception { + ASTParser parser = ASTParser.newParser(AST.JLS8); + Map options = JavaCore.getOptions(); + JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options); + parser.setCompilerOptions(options); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + parser.setStatementsRecovery(true); + parser.setBindingsRecovery(true); + parser.setResolveBindings(true); + + String[] classpathEntries = getClasspathEntries(document, project); + String[] sourceEntries = new String[] {}; + parser.setEnvironment(classpathEntries, sourceEntries, null, true); + + String docURI = document.getUri(); + String unitName = docURI.substring(docURI.lastIndexOf("/")); + parser.setUnitName(unitName); + parser.setSource(document.get(0, document.getLength()).toCharArray()); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + + return cu; + } + + private CompilationUnit parse(TextDocument document) + throws Exception, BadLocationException { + IJavaProject project = projectFinder.find(document.getId()).orElse(null); + CompilationUnit cu = parse(document, project); + if (project != null) { + projectToDocs.get(project, () -> new HashSet<>()).add(URI.create(document.getUri())); + } + return cu; + } + + private static String[] getClasspathEntries(TextDocument document, IJavaProject project) throws Exception { + if (project == null) { + return new String[0]; + } else { + IClasspath classpath = project.getClasspath(); + Stream classpathEntries = classpath.getClasspathEntries(); + return classpathEntries + .filter(path -> path.toFile().exists()) + .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); + } + } + + private void invalidateProject(IJavaProject project) { + Set docUris = projectToDocs.getIfPresent(project); + if (docUris != null) { + writeLock.lock(); + try { + uriToCu.invalidateAll(docUris); + projectToDocs.invalidate(project); + } finally { + writeLock.unlock(); + } + } + } + + private class CUProjectListener implements ProjectObserver.Listener { + + @Override + public void created(IJavaProject project) { + } + + @Override + public void changed(IJavaProject project) { + invalidateProject(project); + } + + @Override + public void deleted(IJavaProject project) { + invalidateProject(project); + } + + } +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java new file mode 100644 index 000000000..e5977bb92 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +import org.springframework.ide.vscode.commons.maven.MavenCore; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +/** + * CU Cache tests + * + * @author Alex Boyko + * + */ +public class CompilationUnitCacheTest { + + private LanguageServerHarness harness; + + @Before + public void setup() throws Exception { + harness = BootLanguageServerHarness.builder().build(); + } + + @Test + public void cu_cached() throws Exception { + harness.intialize(null); + + TextDocument doc = new TextDocument(harness.createTempUri(), LanguageId.JAVA, 0, "package my.package\n" + + "\n" + + "public class SomeClass {\n" + + "\n" + + "}\n"); + CompilationUnit cu = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertNotNull(cu); + + CompilationUnit cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertTrue(cu == cuAnother); + } + + @Test + public void cu_cache_invalidated_by_file_change() throws Exception { + harness.intialize(null); + + TextDocument doc = new TextDocument(harness.createTempUri(), LanguageId.JAVA, 0, "package my.package\n" + + "\n" + + "public class SomeClass {\n" + + "\n" + + "}\n"); + CompilationUnit cu = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertNotNull(cu); + + harness.changeFile(doc.getUri()); + CompilationUnit cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertNotNull(cuAnother); + assertFalse(cu == cuAnother); + + CompilationUnit cuYetAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertTrue(cuAnother == cuYetAnother); + } + + @Test + public void cu_cache_invalidated_by_file_removal() throws Exception { + harness.intialize(null); + + TextDocument doc = new TextDocument(harness.createTempUri(), LanguageId.JAVA, 0, "package my.package\n" + + "\n" + + "public class SomeClass {\n" + + "\n" + + "}\n"); + CompilationUnit cu = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertNotNull(cu); + + harness.deleteFile(doc.getUri()); + CompilationUnit cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertNotNull(cuAnother); + assertFalse(cu == cuAnother); + + CompilationUnit cuYetAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(doc); + assertTrue(cuAnother == cuYetAnother); + } + + @Test + public void cu_cache_invalidated_by_project_change() throws Exception { + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/HelloWorldController.java"; + + harness.intialize(directory); + + URI fileUri = new URI(docUri); + Path path = Paths.get(fileUri); + String content = new String(Files.readAllBytes(path)); + + TextDocument document = new TextDocument(docUri, LanguageId.JAVA, 0, content); + + CompilationUnit cu = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertNotNull(cu); + CompilationUnit cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertTrue(cu == cuAnother); + + harness.changeFile(directory.toPath().resolve(MavenCore.POM_XML).toUri().toString()); + cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertNotNull(cuAnother); + assertFalse(cu == cuAnother); + } + + @Test + public void cu_cache_invalidated_by_project_deletion() throws Exception { + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/HelloWorldController.java"; + + harness.intialize(directory); + + URI fileUri = new URI(docUri); + Path path = Paths.get(fileUri); + String content = new String(Files.readAllBytes(path)); + + TextDocument document = new TextDocument(docUri, LanguageId.JAVA, 0, content); + + CompilationUnit cu = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertNotNull(cu); + CompilationUnit cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertTrue(cu == cuAnother); + + harness.deleteFile(directory.toPath().resolve(MavenCore.POM_XML).toUri().toString()); + cuAnother = harness.getServer().getCompilationUnitCache().getCompilationUnit(document); + assertNotNull(cuAnother); + assertFalse(cu == cuAnother); + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java index 0a8d64eb5..b687114e0 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java @@ -70,6 +70,7 @@ public class SimpleTextDocumentService implements TextDocumentService { final private SimpleLanguageServer server; private Map documents = new HashMap<>(); private ListenerList documentChangeListeners = new ListenerList<>(); + private ListenerList documentCloseListeners = new ListenerList<>(); private CompletionHandler completionHandler = null; private CompletionResolveHandler completionResolveHandler = null; @@ -201,6 +202,7 @@ public class SimpleTextDocumentService implements TextDocumentService { // a document (this resulst in a dicClose even as being sent to the language server if that changes make the // document go 'out of scope'. publishDiagnostics(params.getTextDocument(), ImmutableList.of()); + documentCloseListeners.fire(doc.getDocument()); documents.remove(url); } else { Log.warn("Close event ignored! Assuming document still open because openCount = "+doc.getOpenCount()); @@ -219,6 +221,10 @@ public class SimpleTextDocumentService implements TextDocumentService { documentChangeListeners.add(l); } + public void onDidClose(Consumer l) { + documentCloseListeners.add(l); + } + public void onDidSave(Consumer l) { documentSaveListener=l; } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 8cfe0a45d..4bffd7afc 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -133,7 +133,7 @@ public class LanguageServerHarness { this.defaultLanguageId = defaultLanguageId; } - public static final Duration HIGHLIGHTS_TIMEOUT = Duration.ofMillis(6_000); //TODO: why does it need to be this long, that's fishy! + public static final Duration HIGHLIGHTS_TIMEOUT = Duration.ofMillis(1_000); //TODO: why does it need to be this long, that's fishy! public LanguageServerHarness(Callable factory) throws Exception { this(factory, LanguageId.PLAINTEXT); @@ -520,7 +520,7 @@ public class LanguageServerHarness { return 1; } - protected String createTempUri() throws Exception { + public String createTempUri() throws Exception { return File.createTempFile("workingcopy", getFileExtension()).toURI().toString(); }