diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index 618ead114..3ef1a32a8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -59,6 +59,7 @@ import org.springframework.ide.vscode.commons.util.UriUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; /** * @author Martin Lippert @@ -183,46 +184,55 @@ public class SpringIndexerJava implements SpringIndexer { Set scannedFiles = new HashSet<>(); Set scannedTypes = new HashSet<>(); - Map updatedDocs = new HashMap<>(); + Map updatedDocs = new HashMap<>(); // docURI -> UpdatedDoc String[] javaFiles = new String[docs.length]; + long[] lastModified = new long[docs.length]; for (int i = 0; i < docs.length; i++) { updatedDocs.put(docs[i].getDocURI(), docs[i]); String file = UriUtil.toFileString(docs[i].getDocURI()); + javaFiles[i] = file; + lastModified[i] = docs[i].getLastModified(); + scannedFiles.add(file); } + List generatedSymbols = new ArrayList(); + Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); + FileASTRequestor requestor = new FileASTRequestor() { @Override public void acceptAST(String sourceFilePath, CompilationUnit cu) { File file = new File(sourceFilePath); String docURI = UriUtil.toUri(file).toString(); - long lastModified = file.lastModified(); - AtomicReference docRef = new AtomicReference<>(); + UpdatedDoc updatedDoc = updatedDocs.get(docURI); - - List generatedSymbols = new ArrayList(); + long lastModified = updatedDoc.getLastModified(); + + AtomicReference docRef = new AtomicReference<>(); SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath, lastModified, docRef, updatedDoc.getContent().get(), generatedSymbols, SCAN_PASS.ONE, new ArrayList<>(), scannedTypes); + + dependencies.putAll(sourceFilePath, context.getDependencies()); scanAST(context); - SymbolCacheKey cacheKey = getCacheKey(project); - SpringIndexerJava.this.cache.update(cacheKey, sourceFilePath, lastModified, generatedSymbols, context.getDependencies()); - - for (CachedSymbol symbol : generatedSymbols) { - symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); - } - fileScannedEvent(sourceFilePath); } }; parser.createASTs(javaFiles, null, new String[0], requestor, null); + for (CachedSymbol symbol : generatedSymbols) { + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } + + SymbolCacheKey cacheKey = getCacheKey(project); + SpringIndexerJava.this.cache.update(cacheKey, javaFiles, lastModified, generatedSymbols, dependencies); + scanAffectedFiles(project, scannedTypes, scannedFiles); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java index be93eaf8e..4fc3f5734 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal, Inc. + * Copyright (c) 2019, 2020 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 @@ -26,6 +26,7 @@ public interface SymbolCache { Pair> retrieve(SymbolCacheKey cacheKey, String[] files); void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies); + void update(SymbolCacheKey cacheKey, String[] files, long[] lastModified, List generatedSymbols, Multimap dependencies); void remove(SymbolCacheKey cacheKey); void removeFile(SymbolCacheKey symbolCacheKey, String file); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java index 69a90830c..734c17aaf 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal, Inc. + * Copyright (c) 2019 2020 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 @@ -19,6 +19,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -170,9 +171,10 @@ public class SymbolCacheOnDisc implements SymbolCache { @Override public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies) { - if (dependencies==null) { + if (dependencies == null) { dependencies = ImmutableSet.of(); } + CacheStore cacheStore = this.stores.get(cacheKey); if (cacheStore != null) { @@ -197,6 +199,48 @@ public class SymbolCacheOnDisc implements SymbolCache { } } + @Override + public void update(SymbolCacheKey cacheKey, String[] files, long[] lastModified, List generatedSymbols, Multimap dependencies) { + if (dependencies == null) { + dependencies = ImmutableMultimap.of(); + } + + CacheStore cacheStore = this.stores.get(cacheKey); + + if (cacheStore != null) { + SortedMap timestampedFiles = new TreeMap<>(cacheStore.getTimestampedFiles()); + Set allDocURIs = new HashSet<>(); + + Map> changedDependencies = new HashMap<>(cacheStore.getDependencies()); + + for (int i = 0; i < files.length; i++) { + + // update cache internal map of timestamps per file + String docURI = UriUtil.toUri(new File(files[i])).toString(); + allDocURIs.add(docURI); + + timestampedFiles.put(files[i], lastModified[i]); + + // update cache internal map of dependencies per file + Collection updatedDependencies = dependencies.get(files[i]); + if (updatedDependencies == null || updatedDependencies.isEmpty()) { + changedDependencies.remove(files[i]); + } else { + changedDependencies.put(files[i], ImmutableSet.copyOf(updatedDependencies)); + } + } + + // update cache internal list of cached symbols (by removing old ones and adding all new ones) + List cachedSymbols = cacheStore.getSymbols().stream() + .filter(cachedSymbol -> !allDocURIs.contains(cachedSymbol.getDocURI())) + .collect(Collectors.toList()); + cachedSymbols.addAll(generatedSymbols); + + // store the complete cache content of this project to disc + save(cacheKey, cachedSymbols, timestampedFiles, changedDependencies); + } + } + private void save(SymbolCacheKey cacheKey, List generatedSymbols, SortedMap timestampedFiles, Map> dependencies) { CacheStore store = new CacheStore(timestampedFiles, generatedSymbols, dependencies); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java index 5e568aef2..af3db83cc 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal, Inc. + * Copyright (c) 2019, 2020 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 @@ -35,6 +35,10 @@ public class SymbolCacheVoid implements SymbolCache { public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies) { } + @Override + public void update(SymbolCacheKey cacheKey, String[] file, long[] lastModified, List generatedSymbols, Multimap dependencies) { + } + @Override public void remove(SymbolCacheKey cacheKey) { } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java index 54840cf61..7a7a49823 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal, Inc. + * Copyright (c) 2019, 2020 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 @@ -32,6 +32,7 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; @@ -46,7 +47,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; -import com.google.common.collect.MultimapBuilder; public class SymbolCacheOnDiscTest { @@ -284,6 +284,93 @@ public class SymbolCacheOnDiscTest { assertEquals(2, cachedSymbols.length); } + @Test + public void testSymbolsAddedToMultipleFiles() throws Exception { + + // create 3 files with one symbol each + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + FileTime timeFile2 = Files.getLastModifiedTime(file2); + FileTime timeFile3 = Files.getLastModifiedTime(file3); + + String[] files = {file1.toAbsolutePath().toString(), file2.toAbsolutePath().toString(), file3.toAbsolutePath().toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toString(); + String doc3URI = UriUtil.toUri(file3.toFile()).toString(); + + List generatedSymbols = new ArrayList<>(); + SymbolInformation symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + + SymbolInformation symbol2 = new SymbolInformation("symbol2", SymbolKind.Field, new Location(doc2URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + generatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); + + SymbolInformation symbol3 = new SymbolInformation("symbol3", SymbolKind.Field, new Location(doc3URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol3 = new EnhancedSymbolInformation(symbol3, null); + generatedSymbols.add(new CachedSymbol(doc3URI, timeFile3.toMillis(), enhancedSymbol3)); + + // store original version of the symbols to the cache + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, null); + + + // create updated and new symbols + List updatedSymbols = new ArrayList<>(); + + SymbolInformation updatedSymbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation updatedEnhancedSymbol1 = new EnhancedSymbolInformation(updatedSymbol1, null); + + SymbolInformation newSymbol1 = new SymbolInformation("symbol1-new", SymbolKind.Interface, new Location(doc1URI, new Range(new Position(5, 5), new Position(5, 10)))); + EnhancedSymbolInformation newEnhancedSymbol1 = new EnhancedSymbolInformation(newSymbol1, null); + + updatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, updatedEnhancedSymbol1)); + updatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, newEnhancedSymbol1)); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + SymbolInformation updatedSymbol2 = new SymbolInformation("symbol2-updated", SymbolKind.Field, new Location(doc2URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation updatedEnhancedSymbol2 = new EnhancedSymbolInformation(updatedSymbol2, null); + updatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis() + 3000, updatedEnhancedSymbol2)); + assertTrue(file2.toFile().setLastModified(timeFile2.toMillis() + 3000)); + + String[] updatedFiles = new String[] {file1.toAbsolutePath().toString(), file2.toAbsolutePath().toString()}; + long[] updatedModificationTimestamps = new long[] {timeFile1.toMillis() + 2000, timeFile2.toMillis() + 3000}; + + // update multiple files in the cache + cache.update(new SymbolCacheKey("somekey", "1"), updatedFiles, updatedModificationTimestamps, updatedSymbols, null); + + // double check whether all changes got stored and retrieved correctly + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(cachedSymbols); + assertEquals(4, cachedSymbols.length); + + assertSymbol(updatedEnhancedSymbol1, cachedSymbols); + assertSymbol(newEnhancedSymbol1, cachedSymbols); + assertSymbol(updatedEnhancedSymbol2, cachedSymbols); + assertSymbol(enhancedSymbol3, cachedSymbols); + } + + private void assertSymbol(EnhancedSymbolInformation enhancedSymbol, CachedSymbol[] cachedSymbols) { + for (CachedSymbol cachedSymbol : cachedSymbols) { + SymbolInformation symbol = cachedSymbol.getEnhancedSymbol().getSymbol(); + + if (symbol.toString().equals(enhancedSymbol.getSymbol().toString())) { + return; + } + } + + + Assert.fail("symbol not found: " + enhancedSymbol.getSymbol().toString()); + } + @Test public void testDependencyAddedToExistingFile() throws Exception { Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1");