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 945f8cd04..de2a883e7 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 @@ -17,6 +17,7 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; @@ -109,7 +110,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { liveHoverWatchdog = new SpringLiveHoverWatchdog(this, hoverInfoProvider); documents.onDidChangeContent(params -> { TextDocument doc = params.getDocument(); - if (testHightlighter!=null) { + if (testHightlighter != null) { getClient().highlight(new HighlightParams(params.getDocument().getId(), testHightlighter.apply(doc))); } else { liveHoverWatchdog.watchDocument(doc.getUri()); @@ -121,12 +122,19 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { documents.onReferences(referencesHandler); indexer = createAnnotationIndexer(this, javaProjectFinder); + documents.onDidSave(params -> { + String docURI = params.getDocument().getId().getUri(); + String content = params.getDocument().get(); + indexer.updateDocument(docURI, content); + }); + documents.onDocumentSymbol(new BootJavaDocumentSymbolHandler(indexer)); workspaceService.onWorkspaceSymbol(new BootJavaWorkspaceSymbolHandler(indexer)); BootJavaCodeLensEngine codeLensHandler = createCodeLensEngine(this, javaProjectFinder); documents.onCodeLens(codeLensHandler::createCodeLenses); documents.onCodeLensResolve(codeLensHandler::resolveCodeLens); + } public void setMaxCompletionsNumber(int number) { diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java index 70808a8c3..4427537c2 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java @@ -49,8 +49,6 @@ import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguage import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; -import com.google.common.collect.ImmutableList; - /** * @author Martin Lippert */ @@ -89,8 +87,19 @@ public class SpringIndexer { } } - public void updateDocument(String docURI) { - // TODO: update information because of doc change + public void updateDocument(String docURI, String content) { + if (docURI.endsWith(".java")) { + try { + IJavaProject project = projectFinder.find(new File(new URI(docURI))); + if (project != null) { + String[] classpathEntries = getClasspathEntries(project); + scanFile(docURI, content, classpathEntries); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } } public List getAllSymbols(String query) { @@ -184,6 +193,34 @@ public class SpringIndexer { } } + private void scanFile(String docURI, String content, String[] classpathEntries) 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[] sourceEntries = new String[] {}; + parser.setEnvironment(classpathEntries, sourceEntries, null, true); + + String unitName = docURI.substring(docURI.lastIndexOf("/")); + parser.setUnitName(unitName); + parser.setSource(content.toCharArray()); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + + if (cu != null) { + List oldSymbols = symbolsByDoc.remove(docURI); + symbols.removeAll(oldSymbols); + + AtomicReference docRef = new AtomicReference<>(); + scanAST(cu, docURI, docRef, content); + } + } + private void scanFiles(ASTParser parser, String[] javaFiles, String[] classpathEntries) throws Exception { Map options = JavaCore.getOptions(); @@ -203,20 +240,20 @@ public class SpringIndexer { public void acceptAST(String sourceFilePath, CompilationUnit cu) { String docURI = "file://" + sourceFilePath; AtomicReference docRef = new AtomicReference<>(); - scanAST(cu, docURI, docRef); + scanAST(cu, docURI, docRef, null); } }; parser.createASTs(javaFiles, null, new String[0], requestor, null); } - private void scanAST(final CompilationUnit cu, final String docURI, AtomicReference docRef) { + private void scanAST(final CompilationUnit cu, final String docURI, AtomicReference docRef, final String content) { cu.accept(new ASTVisitor() { @Override public boolean visit(SingleMemberAnnotation node) { try { - extractSymbolInformation(node, docURI, docRef); + extractSymbolInformation(node, docURI, docRef, content); } catch (Exception e) { e.printStackTrace(); @@ -228,7 +265,7 @@ public class SpringIndexer { @Override public boolean visit(NormalAnnotation node) { try { - extractSymbolInformation(node, docURI, docRef); + extractSymbolInformation(node, docURI, docRef, content); } catch (Exception e) { e.printStackTrace(); @@ -240,7 +277,7 @@ public class SpringIndexer { @Override public boolean visit(MarkerAnnotation node) { try { - extractSymbolInformation(node, docURI, docRef); + extractSymbolInformation(node, docURI, docRef, content); } catch (Exception e) { e.printStackTrace(); @@ -251,7 +288,7 @@ public class SpringIndexer { }); } - private void extractSymbolInformation(Annotation node, String docURI, AtomicReference docRef) throws Exception { + private void extractSymbolInformation(Annotation node, String docURI, AtomicReference docRef, String content) throws Exception { ITypeBinding typeBinding = node.resolveTypeBinding(); if (typeBinding != null) { @@ -259,7 +296,7 @@ public class SpringIndexer { SymbolProvider provider = symbolProviders.get(qualifiedTypeName); if (provider != null) { - TextDocument doc = getTempTextDocument(docURI, docRef); + TextDocument doc = getTempTextDocument(docURI, docRef, content); SymbolInformation symbol = provider.getSymbol(node, doc); if (symbol != null) { symbols.add(symbol); @@ -267,7 +304,7 @@ public class SpringIndexer { } } else { - SymbolInformation symbol = provideDefaultSymbol(node, docURI, docRef); + SymbolInformation symbol = provideDefaultSymbol(node, docURI, docRef, content); if (symbol != null) { symbols.add(symbol); symbolsByDoc.computeIfAbsent(docURI, s -> new ArrayList()).add(symbol); @@ -276,30 +313,32 @@ public class SpringIndexer { } } - private TextDocument getTempTextDocument(String docURI, AtomicReference docRef) throws Exception { + private TextDocument getTempTextDocument(String docURI, AtomicReference docRef, String content) throws Exception { TextDocument doc = docRef.get(); if (doc == null) { - doc = createTempTextDocument(docURI); + doc = createTempTextDocument(docURI, content); docRef.set(doc); } return doc; } - private TextDocument createTempTextDocument(String docURI) throws Exception { - Path path = Paths.get(new URI(docURI)); - String content = new String(Files.readAllBytes(path)); + private TextDocument createTempTextDocument(String docURI, String content) throws Exception { + if (content == null) { + Path path = Paths.get(new URI(docURI)); + content = new String(Files.readAllBytes(path)); + } TextDocument doc = new TextDocument(docURI, LanguageId.PLAINTEXT, 0, content); return doc; } - private SymbolInformation provideDefaultSymbol(Annotation node, String docURI, AtomicReference docRef) { + private SymbolInformation provideDefaultSymbol(Annotation node, String docURI, AtomicReference docRef, String content) { try { ITypeBinding type = node.resolveTypeBinding(); if (type != null) { String qualifiedName = type.getQualifiedName(); if (qualifiedName != null && qualifiedName.startsWith("org.springframework")) { - TextDocument doc = getTempTextDocument(docURI, docRef); + TextDocument doc = getTempTextDocument(docURI, docRef, content); SymbolInformation symbol = new SymbolInformation(node.toString(), SymbolKind.Interface, new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))); return symbol; diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java index b6fbbebba..5a09ad17a 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java @@ -14,12 +14,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.File; +import java.net.URI; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import org.apache.commons.io.FileUtils; import org.eclipse.lsp4j.SymbolInformation; import org.junit.Before; import org.junit.Test; @@ -144,6 +146,42 @@ public class SpringIndexerTest { assertTrue(containsSymbol(allSymbols, "@/classlevel/mapping-subpackage -- (no method defined)", uriPrefix + "/src/main/java/org/test/sub/MappingClassSubpackage.java", 7, 1, 7, 38)); } + @Test + public void testUpdateChangedDocument() throws Exception { + harness.intialize(new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-parent/test-annotation-indexing/").toURI())); + + // create initial index content + SpringIndexer indexer = new SpringIndexer(harness.getServer(), projectFinder, symbolProviders); + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-parent/test-annotation-indexing/").toURI()); + indexer.initialize(directory.toPath()); + + // update document and update index + String changedDocURI = "file://" + directory.getAbsolutePath() + "/src/main/java/org/test/SimpleMappingClass.java"; + String newContent = FileUtils.readFileToString(new File(new URI(changedDocURI))).replace("mapping1", "mapping1-CHANGED"); + indexer.updateDocument(changedDocURI, newContent); + + // check for updated index per document + List symbols = indexer.getSymbols(changedDocURI); + assertEquals(2, symbols.size()); + assertTrue(containsSymbol(symbols, "@/mapping1-CHANGED -- (no method defined)", changedDocURI, 6, 1, 6, 36)); + assertTrue(containsSymbol(symbols, "@/mapping2 -- (no method defined)", changedDocURI, 11, 1, 11, 28)); + + // check for updated index in all symbols + List allSymbols = indexer.getAllSymbols(""); + assertEquals(8, allSymbols.size()); + + String uriPrefix = "file://" + directory.getAbsolutePath(); + + assertTrue(containsSymbol(allSymbols, "@SpringBootApplication", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@/embedded-foo-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 17, 1, 17, 41)); + assertTrue(containsSymbol(allSymbols, "@/foo-root-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 24, 0, 24, 36)); + assertTrue(containsSymbol(allSymbols, "@/foo-root-mapping/embedded-foo-mapping-with-root -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 27, 1, 27, 51)); + assertTrue(containsSymbol(allSymbols, "@/mapping1-CHANGED -- (no method defined)", uriPrefix + "/src/main/java/org/test/SimpleMappingClass.java", 6, 1, 6, 36)); + assertTrue(containsSymbol(allSymbols, "@/mapping2 -- (no method defined)", uriPrefix + "/src/main/java/org/test/SimpleMappingClass.java", 11, 1, 11, 28)); + assertTrue(containsSymbol(allSymbols, "@/classlevel -- (no method defined)", uriPrefix + "/src/main/java/org/test/sub/MappingClassSubpackage.java", 4, 0, 4, 30)); + assertTrue(containsSymbol(allSymbols, "@/classlevel/mapping-subpackage -- (no method defined)", uriPrefix + "/src/main/java/org/test/sub/MappingClassSubpackage.java", 7, 1, 7, 38)); + } + @Test public void testFilterSymbolsUsingQueryString() throws Exception { harness.intialize(new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-parent/test-annotation-indexing/").toURI()));