summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorGeorge Fraser <george@fivetran.com>2019-01-08 19:22:45 -0800
committerGeorge Fraser <george@fivetran.com>2019-01-08 19:22:45 -0800
commit727bd8341afc689c842385a234647b8f7b22de21 (patch)
tree73230e098d2a6e72fc72901f4363b46530433f15 /src/main
parent46e72551e73f1de2417102fd3d7fbc358a42cfa6 (diff)
downloadjava-language-server-727bd8341afc689c842385a234647b8f7b22de21.zip
Eliminate SourcePath, docpath isn't quite working
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/org/javacs/CompileBatch.java2
-rw-r--r--src/main/java/org/javacs/CompileFile.java3
-rw-r--r--src/main/java/org/javacs/CompileFocus.java28
-rw-r--r--src/main/java/org/javacs/Docs.java6
-rw-r--r--src/main/java/org/javacs/FileStore.java227
-rw-r--r--src/main/java/org/javacs/JavaCompilerService.java102
-rw-r--r--src/main/java/org/javacs/JavaLanguageServer.java38
-rw-r--r--src/main/java/org/javacs/Parser.java1
-rw-r--r--src/main/java/org/javacs/SourceFileManager.java69
-rw-r--r--src/main/java/org/javacs/SourceFileObject.java2
-rw-r--r--src/main/java/org/javacs/SourcePath.java168
11 files changed, 249 insertions, 397 deletions
diff --git a/src/main/java/org/javacs/CompileBatch.java b/src/main/java/org/javacs/CompileBatch.java
index 947ba84..de6988c 100644
--- a/src/main/java/org/javacs/CompileBatch.java
+++ b/src/main/java/org/javacs/CompileBatch.java
@@ -79,7 +79,7 @@ public class CompileBatch {
null,
parent.fileManager,
parent.diags::add,
- JavaCompilerService.options(parent.sourcePath, parent.classPath),
+ JavaCompilerService.options(parent.classPath),
Collections.emptyList(),
sources);
}
diff --git a/src/main/java/org/javacs/CompileFile.java b/src/main/java/org/javacs/CompileFile.java
index b0cae2c..3ae5313 100644
--- a/src/main/java/org/javacs/CompileFile.java
+++ b/src/main/java/org/javacs/CompileFile.java
@@ -217,7 +217,8 @@ public class CompileFile {
}
}
// Look at imports in other classes to help us guess how to fix imports
- var sourcePathImports = Parser.existingImports(parent.allJavaFiles.get());
+ // TODO cache parsed imports on a per-file basis
+ var sourcePathImports = Parser.existingImports(FileStore.all());
var classes = new HashSet<String>();
classes.addAll(parent.jdkClasses.classes());
classes.addAll(parent.classPathClasses.classes());
diff --git a/src/main/java/org/javacs/CompileFocus.java b/src/main/java/org/javacs/CompileFocus.java
index 48b5469..882cbf1 100644
--- a/src/main/java/org/javacs/CompileFocus.java
+++ b/src/main/java/org/javacs/CompileFocus.java
@@ -2,7 +2,6 @@ package org.javacs;
import com.sun.source.tree.*;
import com.sun.source.util.*;
-import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
@@ -62,7 +61,7 @@ public class CompileFocus {
null,
parent.fileManager,
parent.diags::add,
- JavaCompilerService.options(parent.sourcePath, parent.classPath),
+ JavaCompilerService.options(parent.classPath),
Collections.emptyList(),
List.of(new SourceFileObject(file, contents)));
}
@@ -135,12 +134,8 @@ public class CompileFocus {
if (!insideClass) {
// If no package declaration is present, suggest package [inferred name];
if (root.getPackage() == null) {
- relativeToSourcePath(file)
- .ifPresent(
- relative -> {
- var name = relative.toString().replace(File.separatorChar, '.');
- result.add(Completion.ofSnippet("package " + name, "package " + name + ";\n\n"));
- });
+ var name = FileStore.suggestedPackageName(Paths.get(file));
+ result.add(Completion.ofSnippet("package " + name, "package " + name + ";\n\n"));
}
// If no class declaration is present, suggest class [file name]
var hasClassDeclaration = false;
@@ -406,17 +401,6 @@ public class CompileFocus {
"double",
};
- private Optional<Path> relativeToSourcePath(URI source) {
- var p = Paths.get(source);
- for (var root : parent.sourcePath) {
- if (p.startsWith(root)) {
- var rel = root.relativize(p.getParent());
- return Optional.of(rel);
- }
- }
- return Optional.empty();
- }
-
private List<ExecutableElement> virtualMethods(DeclaredType type) {
var result = new ArrayList<ExecutableElement>();
for (var member : type.asElement().getEnclosedElements()) {
@@ -701,11 +685,11 @@ public class CompileFocus {
// Check sourcepath
LOG.info("...checking source path");
- for (var file : parent.allJavaFiles.get()) {
+ for (var file : FileStore.all()) {
if (tooManyItems(result.size())) return;
// If file is in the same package, any class defined in the file is accessible
- var pathBasedPackageName = parent.pathBasedPackageName(file);
- var samePackage = pathBasedPackageName.equals(packageName) || pathBasedPackageName.isEmpty();
+ var otherPackageName = FileStore.packageName(file);
+ var samePackage = otherPackageName.equals(packageName) || otherPackageName.isEmpty();
// If file is in a different package, only a public class with the same name as the file is accessible
var maybePublic = matchesPartialName(file.getFileName().toString(), partialName);
if (samePackage || maybePublic) {
diff --git a/src/main/java/org/javacs/Docs.java b/src/main/java/org/javacs/Docs.java
index 10a0c95..b25c0f7 100644
--- a/src/main/java/org/javacs/Docs.java
+++ b/src/main/java/org/javacs/Docs.java
@@ -24,12 +24,14 @@ public class Docs {
}
}
- Docs(Set<Path> sourcePath) {
+ Docs(Set<Path> docPath) {
this.fileManager =
ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null);
// Compute the full source path, including src.zip for platform classes
- var sourcePathFiles = sourcePath.stream().map(Path::toFile).collect(Collectors.toSet());
+ // TODO source path needs to somehow get from FileStore to here; maybe write a FileManager that fuses FileStore
+ // with docPath?
+ var sourcePathFiles = docPath.stream().map(Path::toFile).collect(Collectors.toSet());
try {
fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles);
diff --git a/src/main/java/org/javacs/FileStore.java b/src/main/java/org/javacs/FileStore.java
index 3c10a64..6982caa 100644
--- a/src/main/java/org/javacs/FileStore.java
+++ b/src/main/java/org/javacs/FileStore.java
@@ -13,9 +13,12 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
@@ -25,76 +28,146 @@ import org.javacs.lsp.DidCloseTextDocumentParams;
import org.javacs.lsp.DidOpenTextDocumentParams;
import org.javacs.lsp.TextDocumentContentChangeEvent;
-// TODO get rid of SourcePath and track source path dynamically here
-
class FileStore {
+ private static final Set<Path> workspaceRoots = new HashSet<>();
+
private static final Map<URI, VersionedContent> activeDocuments = new HashMap<>();
- /**
- * modified[file] is the modified time of file. modified contains .java files and directories on the source path. If
- * modified contains a directory, it contains all the .java files in that directory. modified is sorted, so you can
- * list all the .java files in a directory by doing modified.tailSet(directory)
- */
- private static final TreeMap<Path, Instant> modified = new TreeMap<>();
+ /** javaSources[file] is the javaSources time of a .java source file. */
+ private static final TreeMap<Path, Info> javaSources = new TreeMap<>();
- /** Needed in tests */
- static void reset() {
- modified.clear();
- activeDocuments.clear();
+ private static class Info {
+ final Instant modified;
+ final String packageName;
+
+ Info(Instant modified, String packageName) {
+ this.modified = modified;
+ this.packageName = packageName;
+ }
}
- static List<Path> list(Path dir, String packageName) {
- // If we've never indexed dir, index it now
- if (!modified.containsKey(dir)) {
- LOG.info(String.format("Indexing %s for the first time", dir));
- try {
- modified.put(dir, Instant.EPOCH);
- Files.walk(dir).filter(SourcePath::isJavaFile).forEach(FileStore::readModifiedFromDisk);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ static void setWorkspaceRoots(Set<Path> newRoots) {
+ newRoots = normalize(newRoots);
+ for (var root : workspaceRoots) {
+ if (!newRoots.contains(root)) {
+ workspaceRoots.removeIf(f -> f.startsWith(root));
}
}
- // Walk through a section of index, starting at dir and ending when we find the next dir
- var afterDir = modified.tailMap(dir, false).keySet();
- var packageDir = dir.resolve(packageName.replace('.', File.separatorChar));
- var list = new ArrayList<Path>();
- for (var file : afterDir) {
- if (!SourcePath.isJavaFile(file)) continue;
- if (!file.startsWith(dir)) break;
- if (file.getParent().equals(packageDir)) {
- list.add(file);
+ for (var root : newRoots) {
+ if (!workspaceRoots.contains(root)) {
+ addFiles(root);
}
}
+ workspaceRoots.clear();
+ workspaceRoots.addAll(newRoots);
+ }
+
+ private static Set<Path> normalize(Set<Path> newRoots) {
+ var normalize = new HashSet<Path>();
+ for (var root : newRoots) {
+ normalize.add(root.toAbsolutePath().normalize());
+ }
+ return normalize;
+ }
+
+ private static void addFiles(Path root) {
+ try {
+ Files.walk(root).filter(FileStore::isJavaFile).forEach(FileStore::readInfoFromDisk);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static Collection<Path> all() {
+ return javaSources.keySet();
+ }
+
+ static List<Path> list(String packageName) {
+ var list = new ArrayList<Path>();
+ for (var kv : javaSources.entrySet()) {
+ var file = kv.getKey();
+ var info = kv.getValue();
+ if (!FileStore.isJavaFile(file)) continue;
+ if (info.packageName.equals(packageName)) list.add(file);
+ }
return list;
}
+ static boolean contains(Path file) {
+ return isJavaFile(file) && javaSources.containsKey(file);
+ }
+
static Instant modified(Path file) {
// If we've never checked before, look up modified time on disk
- if (!modified.containsKey(file)) {
- readModifiedFromDisk(file);
+ if (!javaSources.containsKey(file)) {
+ readInfoFromDisk(file);
+ }
+
+ // Look up modified time from cache
+ return javaSources.get(file).modified;
+ }
+
+ static String packageName(Path file) {
+ // If we've never checked before, look up modified time on disk
+ if (!javaSources.containsKey(file)) {
+ readInfoFromDisk(file);
}
// Look up modified time from cache
- return modified.get(file);
+ return javaSources.get(file).packageName;
+ }
+
+ static String suggestedPackageName(Path file) {
+ var sourceRoot = sourceRoot(file);
+ var relativePath = sourceRoot.relativize(file).getParent();
+ if (relativePath == null) return "";
+ return relativePath.toString().replace(File.separatorChar, '.');
+ }
+
+ private static Path sourceRoot(Path file) {
+ for (var dir = file.getParent(); dir != null; dir = dir.getParent()) {
+ for (var related : javaSourcesIn(dir)) {
+ if (related.equals(file)) continue;
+ var packageName = packageName(related);
+ var relativePath = Paths.get(packageName.replace('.', File.separatorChar));
+ var sourceRoot = dir;
+ for (var i = 0; i < relativePath.getNameCount(); i++) {
+ sourceRoot = sourceRoot.getParent();
+ }
+ return sourceRoot;
+ }
+ }
+ return file.getParent();
+ }
+
+ private static List<Path> javaSourcesIn(Path dir) {
+ var tail = javaSources.tailMap(dir, false);
+ var list = new ArrayList<Path>();
+ for (var file : tail.keySet()) {
+ if (!file.startsWith(dir)) break;
+ list.add(file);
+ }
+ return list;
}
static void externalCreate(Path file) {
- readModifiedFromDisk(file);
+ readInfoFromDisk(file);
}
static void externalChange(Path file) {
- readModifiedFromDisk(file);
+ readInfoFromDisk(file);
}
static void externalDelete(Path file) {
- modified.remove(file);
+ javaSources.remove(file);
}
- private static void readModifiedFromDisk(Path file) {
+ private static void readInfoFromDisk(Path file) {
try {
var time = Files.getLastModifiedTime(file).toInstant();
- modified.put(file, time);
+ var packageName = Parser.packageName(file);
+ javaSources.put(file, new Info(time, packageName));
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -103,14 +176,14 @@ class FileStore {
static void open(DidOpenTextDocumentParams params) {
var document = params.textDocument;
var uri = document.uri;
- if (!SourcePath.isJavaFile(uri)) return;
+ if (!FileStore.isJavaFile(uri)) return;
activeDocuments.put(uri, new VersionedContent(document.text, document.version));
}
static void change(DidChangeTextDocumentParams params) {
var document = params.textDocument;
var uri = document.uri;
- if (SourcePath.isJavaFile(uri)) {
+ if (FileStore.isJavaFile(uri)) {
var existing = activeDocuments.get(uri);
var newText = existing.content;
@@ -128,7 +201,7 @@ class FileStore {
static void close(DidCloseTextDocumentParams params) {
var document = params.textDocument;
var uri = document.uri;
- if (SourcePath.isJavaFile(uri)) {
+ if (FileStore.isJavaFile(uri)) {
// Remove from source cache
activeDocuments.remove(uri);
}
@@ -144,7 +217,7 @@ class FileStore {
}
static String contents(URI file) {
- if (!SourcePath.isJavaFile(file)) {
+ if (!FileStore.isJavaFile(file)) {
throw new RuntimeException(file + " is not a java file");
}
if (activeDocuments.containsKey(file)) {
@@ -232,5 +305,75 @@ class FileStore {
}
}
+ static boolean isJavaFile(Path file) {
+ var name = file.getFileName().toString();
+ // We hide module-info.java from javac, because when javac sees module-info.java
+ // it goes into "module mode" and starts looking for classes on the module class path.
+ // This becomes evident when javac starts recompiling *way too much* on each task,
+ // because it doesn't realize there are already up-to-date .class files.
+ // The better solution would be for java-language server to detect the presence of module-info.java,
+ // and go into its own "module mode" where it infers a module source path and a module class path.
+ return name.endsWith(".java") && !name.equals("module-info.java");
+ }
+
+ static boolean isJavaFile(URI uri) {
+ return uri.getScheme().equals("file") && isJavaFile(Paths.get(uri));
+ }
+
+ private static Set<Path> allJavaFilesInDirs(Set<Path> dirs) {
+ var all = new HashSet<Path>();
+ for (var dir : dirs) {
+ try {
+ Files.walk(dir).filter(FileStore::isJavaFile).forEach(all::add);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return all;
+ }
+
+ private static Set<Path> sourcePath(Set<Path> workspaceRoots) {
+ LOG.info("Searching for source roots in " + workspaceRoots);
+
+ var certaintyThreshold = 10;
+ var sourceRoots = new HashMap<Path, Integer>();
+ fileLoop:
+ for (var file : allJavaFilesInDirs(workspaceRoots)) {
+ // First, check if we already have a high-confidence source root containing file
+ for (var root : sourceRoots.keySet()) {
+ var confidence = sourceRoots.get(root);
+ if (file.startsWith(root) && confidence > certaintyThreshold) {
+ continue fileLoop;
+ }
+ }
+ // Otherwise, parse the file and look at its package declaration
+ var parse = Parser.parse(file);
+ var packageName = Objects.toString(parse.getPackageName(), "");
+ var packagePath = packageName.replace('.', File.separatorChar);
+ // If file has no package declaration, don't try to guess a source root
+ // This is probably a new file that the user will add a package declaration to later
+ if (packagePath.isEmpty()) {
+ LOG.warning("Ignoring file with missing package declaration " + file);
+ continue fileLoop;
+ }
+ // If path to file contradicts package declaration, give up
+ var dir = file.getParent();
+ if (!dir.endsWith(packagePath)) {
+ LOG.warning("Java source file " + file + " is not in " + packagePath);
+ continue fileLoop;
+ }
+ // Otherwise, use the package declaration to guess the source root
+ var up = Paths.get(packagePath).getNameCount();
+ var sourceRoot = dir;
+ for (int i = 0; i < up; i++) {
+ sourceRoot = sourceRoot.getParent();
+ }
+ // Increment our confidence in sourceRoot as a source root
+ var count = sourceRoots.getOrDefault(sourceRoot, 0);
+ sourceRoots.put(sourceRoot, count + 1);
+ }
+ return sourceRoots.keySet();
+ }
+
private static final Logger LOG = Logger.getLogger("main");
}
diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java
index 23484e9..757722f 100644
--- a/src/main/java/org/javacs/JavaCompilerService.java
+++ b/src/main/java/org/javacs/JavaCompilerService.java
@@ -7,7 +7,6 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
-import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -17,8 +16,7 @@ import javax.tools.*;
// TODO eliminate uses of URI in favor of Path
public class JavaCompilerService {
// Not modifiable! If you want to edit these, you need to create a new instance
- final Set<Path> sourcePath, classPath, docPath;
- final Supplier<Set<Path>> allJavaFiles;
+ final Set<Path> classPath, docPath;
final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next();
final Docs docs;
final ClassSource jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses;
@@ -28,12 +26,7 @@ public class JavaCompilerService {
// TODO intercept files that aren't in the batch and erase method bodies so compilation is faster
final StandardJavaFileManager fileManager;
- public JavaCompilerService(
- Set<Path> sourcePath, Supplier<Set<Path>> allJavaFiles, Set<Path> classPath, Set<Path> docPath) {
- System.err.println("Source path:");
- for (var p : sourcePath) {
- System.err.println(" " + p);
- }
+ public JavaCompilerService(Set<Path> classPath, Set<Path> docPath) {
System.err.println("Class path:");
for (var p : classPath) {
System.err.println(" " + p);
@@ -42,17 +35,12 @@ public class JavaCompilerService {
for (var p : docPath) {
System.err.println(" " + p);
}
- // sourcePath and classPath can't actually be modified, because JavaCompiler remembers them from task to task
- this.sourcePath = Collections.unmodifiableSet(sourcePath);
- this.allJavaFiles = allJavaFiles;
+ // classPath can't actually be modified, because JavaCompiler remembers it from task to task
this.classPath = Collections.unmodifiableSet(classPath);
this.docPath = Collections.unmodifiableSet(docPath);
- var docSourcePath = new HashSet<Path>();
- docSourcePath.addAll(sourcePath);
- docSourcePath.addAll(docPath);
- this.docs = new Docs(docSourcePath);
+ this.docs = new Docs(docPath);
this.classPathClasses = Classes.classPathTopLevelClasses(classPath);
- this.fileManager = new SourceFileManager(sourcePath);
+ this.fileManager = new SourceFileManager();
;
}
@@ -61,11 +49,10 @@ public class JavaCompilerService {
return classOrSourcePath.stream().map(p -> p.toString()).collect(Collectors.joining(File.pathSeparator));
}
- static List<String> options(Set<Path> sourcePath, Set<Path> classPath) {
+ static List<String> options(Set<Path> classPath) {
var list = new ArrayList<String>();
Collections.addAll(list, "-classpath", joinPath(classPath));
- Collections.addAll(list, "-sourcepath", joinPath(sourcePath));
// Collections.addAll(list, "-verbose");
Collections.addAll(list, "-proc:none");
Collections.addAll(list, "-g");
@@ -86,21 +73,6 @@ public class JavaCompilerService {
return list;
}
- String pathBasedPackageName(Path javaFile) {
- if (!javaFile.getFileName().toString().endsWith(".java")) {
- LOG.warning(javaFile + " does not end in .java");
- return "???";
- }
- for (var dir : sourcePath) {
- if (!javaFile.startsWith(dir)) continue;
- var packageDir = javaFile.getParent();
- var relative = dir.relativize(packageDir);
- return relative.toString().replace('/', '.');
- }
- LOG.warning(javaFile + " is not in the source path " + sourcePath);
- return "???";
- }
-
public Docs docs() {
return docs;
}
@@ -141,7 +113,7 @@ public class JavaCompilerService {
// Construct list of sources
var files = new ArrayList<File>();
for (var uri : uris) {
- if (SourcePath.isJavaFile(uri)) {
+ if (FileStore.isJavaFile(uri)) {
files.add(new File(uri));
}
}
@@ -150,7 +122,7 @@ public class JavaCompilerService {
var sources = fileManager.getJavaFileObjectsFromFiles(files);
// Create task
- var options = options(sourcePath, classPath);
+ var options = options(classPath);
var task =
(JavacTask) compiler.getTask(null, fileManager, diags::add, options, Collections.emptyList(), sources);
var trees = Trees.instance(task);
@@ -336,7 +308,7 @@ public class JavaCompilerService {
return files;
}
// If the declaring file isn't a normal file, for example if it's in src.zip
- if (!SourcePath.isJavaFile(toFile.get())) {
+ if (!FileStore.isJavaFile(toFile.get())) {
LOG.info(String.format("...%s is not on the source path", toFile.get()));
return files;
}
@@ -418,36 +390,12 @@ public class JavaCompilerService {
var lastDot = name.lastIndexOf('.');
var packageName = lastDot == -1 ? "" : name.substring(0, lastDot);
var className = name.substring(lastDot + 1);
- // First, look for a file named [ClassName].java
- var packagePath = Paths.get(packageName.replace('.', File.separatorChar));
- var publicClassPath = packagePath.resolve(className + ".java");
- for (var root : sourcePath) {
- var absPath = root.resolve(publicClassPath);
- if (Files.exists(absPath) && containsTopLevelDeclaration(absPath, className)) {
- return Optional.of(absPath.toUri());
- }
- }
- // Then, look for a secondary declaration in all java files in the package
var isPublic = e.getModifiers().contains(Modifier.PUBLIC);
- if (!isPublic) {
- for (var root : sourcePath) {
- // Create directory where this package would live, if this package is in this part of the source path
- var absDir = root.resolve(packagePath);
- // If package isn't in this part of the source path, keep looking
- if (!Files.exists(absDir)) continue;
- // List dirs
- Iterable<Path> list;
- try {
- list = Files.list(absDir)::iterator;
- } catch (IOException err) {
- throw new RuntimeException(err);
- }
- // Check each .java file in the package for package-private class declarations
- for (var f : list) {
- if (SourcePath.isJavaFile(f) && containsTopLevelDeclaration(f, className)) {
- return Optional.of(f.toUri());
- }
- }
+ for (var file : FileStore.list(packageName)) {
+ // Public classes have to be declared in files named ClassName.java
+ if (isPublic && !file.getFileName().toString().equals(className + ".java")) continue;
+ if (containsTopLevelDeclaration(file, className)) {
+ return Optional.of(file.toUri());
}
}
return Optional.empty();
@@ -466,32 +414,20 @@ public class JavaCompilerService {
return false;
}
- private Set<Path> possibleFiles(Element to) {
+ private Collection<Path> possibleFiles(Element to) {
// If `to` is package-private, only look in my own package
if (isPackagePrivate(to)) {
var myPkg = packageName(to);
- var allFiles = sourceFilesInPackages(myPkg);
+ var allFiles = FileStore.list(myPkg);
LOG.info(String.format("...check %d files in my own package %s", allFiles.size(), myPkg));
+ return allFiles;
}
// Otherwise search all files
- var allFiles = allJavaFiles.get();
+ var allFiles = FileStore.all();
LOG.info(String.format("...check %d files", allFiles.size()));
return allFiles;
}
- /** List .java source files in package */
- private Set<Path> sourceFilesInPackages(String inPackage) {
- var packagePath = Paths.get(inPackage.replace('.', File.separatorChar));
- var files = new HashSet<Path>();
- for (var f : allJavaFiles.get()) {
- var dir = f.getParent();
- if (dir.endsWith(packagePath)) {
- files.add(f);
- }
- }
- return files;
- }
-
private static Cache<String, Boolean> cacheContainsWord = new Cache<>();
private List<Path> containsWord(Collection<Path> allFiles, Element to) {
@@ -605,7 +541,7 @@ public class JavaCompilerService {
LOG.info(String.format("Searching for `%s`...", query));
var result = new ArrayList<TreePath>();
- var files = allJavaFiles.get();
+ var files = FileStore.all();
var checked = 0;
var parsed = 0;
for (var file : files) {
diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java
index 8a11e90..175e4ce 100644
--- a/src/main/java/org/javacs/JavaLanguageServer.java
+++ b/src/main/java/org/javacs/JavaLanguageServer.java
@@ -44,7 +44,6 @@ import org.javacs.lsp.*;
class JavaLanguageServer extends LanguageServer {
// TODO allow multiple workspace roots
private Path workspaceRoot;
- private SourcePath sourcePath;
private final LanguageClient client;
private Set<String> externalDependencies = Set.of();
private Set<Path> classPath = Set.of();
@@ -145,8 +144,7 @@ class JavaLanguageServer extends LanguageServer {
// If classpath is specified by the user, don't infer anything
if (!classPath.isEmpty()) {
javaEndProgress();
- return new JavaCompilerService(
- sourcePath.sourceRoots(), sourcePath::allJavaFiles, classPath, Collections.emptySet());
+ return new JavaCompilerService(classPath, Collections.emptySet());
}
// Otherwise, combine inference with user-specified external dependencies
else {
@@ -159,7 +157,7 @@ class JavaLanguageServer extends LanguageServer {
var docPath = infer.buildDocPath();
javaEndProgress();
- return new JavaCompilerService(sourcePath.sourceRoots(), sourcePath::allJavaFiles, classPath, docPath);
+ return new JavaCompilerService(classPath, docPath);
}
}
@@ -180,7 +178,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public InitializeResult initialize(InitializeParams params) {
this.workspaceRoot = Paths.get(params.rootUri);
- this.sourcePath = new SourcePath(Set.of(workspaceRoot));
+ FileStore.setWorkspaceRoots(Set.of(Paths.get(params.rootUri)));
var c = new JsonObject();
c.addProperty("textDocumentSync", 2); // Incremental
@@ -260,30 +258,21 @@ class JavaLanguageServer extends LanguageServer {
@Override
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
// TODO update config when pom.xml changes
- var changed = false;
- var created = new HashSet<Path>();
- var deleted = new HashSet<Path>();
for (var c : params.changes) {
- if (!SourcePath.isJavaFile(c.uri)) continue;
+ if (!FileStore.isJavaFile(c.uri)) continue;
var file = Paths.get(c.uri);
switch (c.type) {
case FileChangeType.Created:
FileStore.externalCreate(file);
- created.add(file);
break;
case FileChangeType.Changed:
FileStore.externalChange(file);
- if (sourcePath.update(file)) changed = true;
break;
case FileChangeType.Deleted:
FileStore.externalDelete(file);
- deleted.add(file);
break;
}
}
- if (sourcePath.create(created)) changed = true;
- if (sourcePath.delete(deleted)) changed = true;
- if (changed) this.compiler = createCompiler();
}
private Integer completionItemKind(Element e) {
@@ -342,7 +331,7 @@ class JavaLanguageServer extends LanguageServer {
// TODO reuse previous compilation when changes are small
var started = Instant.now();
var uri = position.textDocument.uri;
- if (!SourcePath.isJavaFile(uri)) return Optional.empty();
+ if (!FileStore.isJavaFile(uri)) return Optional.empty();
var content = FileStore.contents(uri);
var line = position.position.line + 1;
var column = position.position.character + 1;
@@ -644,7 +633,7 @@ class JavaLanguageServer extends LanguageServer {
public Optional<Hover> hover(TextDocumentPositionParams position) {
// Compile entire file if it's changed since last hover
var uri = position.textDocument.uri;
- if (!SourcePath.isJavaFile(uri)) return Optional.empty();
+ if (!FileStore.isJavaFile(uri)) return Optional.empty();
updateActiveFile(uri);
// Find element undeer cursor
@@ -748,7 +737,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public Optional<SignatureHelp> signatureHelp(TextDocumentPositionParams position) {
var uri = position.textDocument.uri;
- if (!SourcePath.isJavaFile(uri)) return Optional.empty();
+ if (!FileStore.isJavaFile(uri)) return Optional.empty();
var content = FileStore.contents(uri);
var line = position.position.line + 1;
var column = position.position.character + 1;
@@ -762,7 +751,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public Optional<List<Location>> gotoDefinition(TextDocumentPositionParams position) {
var fromUri = position.textDocument.uri;
- if (!SourcePath.isJavaFile(fromUri)) return Optional.empty();
+ if (!FileStore.isJavaFile(fromUri)) return Optional.empty();
var fromLine = position.position.line + 1;
var fromColumn = position.position.character + 1;
@@ -803,7 +792,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public Optional<List<Location>> findReferences(ReferenceParams position) {
var toUri = position.textDocument.uri;
- if (!SourcePath.isJavaFile(toUri)) return Optional.empty();
+ if (!FileStore.isJavaFile(toUri)) return Optional.empty();
var toLine = position.position.line + 1;
var toColumn = position.position.character + 1;
@@ -877,7 +866,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public List<SymbolInformation> documentSymbol(DocumentSymbolParams params) {
var uri = params.textDocument.uri;
- if (!SourcePath.isJavaFile(uri)) return List.of();
+ if (!FileStore.isJavaFile(uri)) return List.of();
updateCachedParse(uri);
var paths = cacheParse.documentSymbols();
var infos = new ArrayList<SymbolInformation>();
@@ -956,7 +945,7 @@ class JavaLanguageServer extends LanguageServer {
public List<CodeLens> codeLens(CodeLensParams params) {
// TODO just create a blank code lens on every method, then resolve it async
var uri = params.textDocument.uri;
- if (!SourcePath.isJavaFile(uri)) return List.of();
+ if (!FileStore.isJavaFile(uri)) return List.of();
updateCachedParse(uri);
var declarations = cacheParse.declarations();
var result = new ArrayList<CodeLens>();
@@ -1190,6 +1179,7 @@ class JavaLanguageServer extends LanguageServer {
edits.addAll(fixImports());
edits.addAll(addOverrides());
// TODO replace var with type name when vars are copy-pasted into fields
+ // TODO replace ThisClass.staticMethod() with staticMethod() when ThisClass is useless
return edits;
}
@@ -1364,7 +1354,7 @@ class JavaLanguageServer extends LanguageServer {
public void didCloseTextDocument(DidCloseTextDocumentParams params) {
FileStore.close(params);
- if (SourcePath.isJavaFile(params.textDocument.uri)) {
+ if (FileStore.isJavaFile(params.textDocument.uri)) {
// Clear diagnostics
publishDiagnostics(List.of(params.textDocument.uri), List.of());
}
@@ -1372,7 +1362,7 @@ class JavaLanguageServer extends LanguageServer {
@Override
public void didSaveTextDocument(DidSaveTextDocumentParams params) {
- if (SourcePath.isJavaFile(params.textDocument.uri)) {
+ if (FileStore.isJavaFile(params.textDocument.uri)) {
// Re-lint all active documents
reportErrors(FileStore.activeDocuments());
}
diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java
index b70b963..b6d07c1 100644
--- a/src/main/java/org/javacs/Parser.java
+++ b/src/main/java/org/javacs/Parser.java
@@ -269,6 +269,7 @@ class Parser {
} catch (IOException e) {
throw new RuntimeException(e);
}
+ // TODO fall back on parsing file
return "";
}
diff --git a/src/main/java/org/javacs/SourceFileManager.java b/src/main/java/org/javacs/SourceFileManager.java
index c174d2b..6af01fa 100644
--- a/src/main/java/org/javacs/SourceFileManager.java
+++ b/src/main/java/org/javacs/SourceFileManager.java
@@ -4,9 +4,7 @@ import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
-import java.net.URLClassLoader;
import java.nio.charset.Charset;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Logger;
@@ -14,11 +12,6 @@ import javax.tools.*;
class SourceFileManager implements StandardJavaFileManager {
private final StandardJavaFileManager delegate = createDelegateFileManager();
- private final Set<Path> sourcePath;
-
- SourceFileManager(Set<Path> sourcePath) {
- this.sourcePath = sourcePath;
- }
private static StandardJavaFileManager createDelegateFileManager() {
var compiler = ServiceLoader.load(JavaCompiler.class).iterator().next();
@@ -31,12 +24,7 @@ class SourceFileManager implements StandardJavaFileManager {
@Override
public ClassLoader getClassLoader(Location location) {
- var thisClassLoader = getClass().getClassLoader();
- if (location == StandardLocation.SOURCE_PATH) {
- return new URLClassLoader(urls(sourcePath), thisClassLoader);
- } else {
- return thisClassLoader;
- }
+ return delegate.getClassLoader(location);
}
private URL[] urls(Set<Path> paths) {
@@ -56,17 +44,15 @@ class SourceFileManager implements StandardJavaFileManager {
public Iterable<JavaFileObject> list(
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
- var dirs = sourcePath.stream();
- var files = dirs.flatMap(dir -> FileStore.list(dir, packageName).stream());
- var filter = files.map(this::asJavaFileObject).filter(this::isJavaSource);
- return filter::iterator;
+ var stream = FileStore.list(packageName).stream().map(this::asJavaFileObject).filter(this::isJavaSource);
+ return stream::iterator;
} else {
return delegate.list(location, packageName, kinds, recurse);
}
}
private boolean isJavaSource(JavaFileObject file) {
- return SourcePath.isJavaFile(file.toUri());
+ return FileStore.isJavaFile(file.toUri());
}
private JavaFileObject asJavaFileObject(Path file) {
@@ -77,27 +63,15 @@ class SourceFileManager implements StandardJavaFileManager {
public String inferBinaryName(Location location, JavaFileObject file) {
if (location == StandardLocation.SOURCE_PATH) {
var source = (SourceFileObject) file;
- return sourceFileBinaryName(sourcePath, source);
+ var packageName = FileStore.packageName(source.path);
+ var className = removeExtension(source.path.getFileName().toString());
+ if (!packageName.isEmpty()) className = packageName + "." + className;
+ return className;
} else {
return delegate.inferBinaryName(location, file);
}
}
- private String sourceFileBinaryName(Set<Path> path, SourceFileObject source) {
- for (var root : path) {
- if (source.path.startsWith(root)) {
- var relativePath = root.relativize(source.path).toString();
- return binaryName(relativePath);
- }
- }
- return null;
- }
-
- private String binaryName(String relativePath) {
- var slash = removeExtension(relativePath);
- return slash.replace(File.separatorChar, '.');
- }
-
private String removeExtension(String fileName) {
var lastDot = fileName.lastIndexOf(".");
return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
@@ -122,8 +96,12 @@ class SourceFileManager implements StandardJavaFileManager {
public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind)
throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
- var relative = className.replace('.', '/') + kind.extension;
- return findFileForInput(location, relative);
+ var packageName = Parser.mostName(className);
+ var simpleClassName = Parser.lastName(className);
+ for (var f : FileStore.list(packageName)) {
+ if (f.getFileName().toString().equals(simpleClassName + kind.extension)) return new SourceFileObject(f);
+ }
+ return null;
}
return delegate.getJavaFileForInput(location, className, kind);
}
@@ -131,26 +109,11 @@ class SourceFileManager implements StandardJavaFileManager {
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
- var relative = relativeName;
- if (!packageName.isEmpty()) {
- relative = packageName.replace('.', '/') + '/' + relative;
- }
- return findFileForInput(location, relative);
+ return null;
}
return delegate.getFileForInput(location, packageName, relativeName);
}
- private JavaFileObject findFileForInput(Location location, String relative) {
- for (var root : sourcePath) {
- var absolute = root.resolve(relative);
- if (!SourcePath.isJavaFile(absolute)) return null;
- if (Files.exists(absolute)) {
- return new SourceFileObject(absolute);
- }
- }
- return null;
- }
-
@Override
public JavaFileObject getJavaFileForOutput(
Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
@@ -204,7 +167,7 @@ class SourceFileManager implements StandardJavaFileManager {
public boolean contains(Location location, FileObject file) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
var source = (SourceFileObject) file;
- return contains(sourcePath, source);
+ return FileStore.contains(source.path);
} else {
return delegate.contains(location, file);
}
diff --git a/src/main/java/org/javacs/SourceFileObject.java b/src/main/java/org/javacs/SourceFileObject.java
index 46d652d..d0b32a6 100644
--- a/src/main/java/org/javacs/SourceFileObject.java
+++ b/src/main/java/org/javacs/SourceFileObject.java
@@ -27,7 +27,7 @@ class SourceFileObject implements JavaFileObject {
}
SourceFileObject(Path path, String contents) {
- if (!SourcePath.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
+ if (!FileStore.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
this.path = path;
this.contents = contents;
}
diff --git a/src/main/java/org/javacs/SourcePath.java b/src/main/java/org/javacs/SourcePath.java
deleted file mode 100644
index 9c17252..0000000
--- a/src/main/java/org/javacs/SourcePath.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package org.javacs;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
-import java.util.logging.Logger;
-
-class SourcePath {
- private final Set<Path> workspaceRoots;
- /** All .java files under workspaceRoot */
- private final Set<Path> allJavaFiles;
- /** Source roots, computed from allJavaFiles by looking at package declarations. */
- private Set<Path> sourceRoots = new HashSet<>();
-
- public SourcePath(Set<Path> workspaceRoots) {
- this.workspaceRoots = Collections.unmodifiableSet(workspaceRoots);
- this.sourceRoots = sourcePath(workspaceRoots);
- this.allJavaFiles = allJavaFilesInDirs(sourceRoots);
- }
-
- /**
- * A .java file has been created. Update allJavaFiles, but don't recalculate sourceRoots until the file is saved,
- * because the user hasn't had a change to type in a package name yet.
- */
- boolean create(Set<Path> javaFiles) {
- if (javaFiles.isEmpty()) return false;
- for (var f : javaFiles) {
- if (isJavaFile(f)) {
- allJavaFiles.add(f);
- }
- }
- return checkSourceRoots();
- }
-
- /**
- * A .java file has been deleted. If the file is the last one in a particular source root, we should remove that
- * source root.
- */
- boolean delete(Set<Path> javaFiles) {
- if (javaFiles.isEmpty()) return false;
- allJavaFiles.removeAll(javaFiles);
- return checkSourceRoots();
- }
-
- /**
- * A .java file has been edited and saved. If the package declaration has been changed, we may need to recalculate
- * sourceRoots.
- */
- // TODO batch this
- boolean update(Path javaFile) {
- if (!isJavaFile(javaFile)) return false;
- // Check if file is in an existing source root
- for (var root : sourceRoots) {
- if (!javaFile.startsWith(root)) continue;
- var dir = javaFile.getParent();
- var expected = root.relativize(dir).toString().replace('/', '.');
- var parse = Parser.parse(javaFile);
- var found = Objects.toString(parse.getPackageName(), "");
- if (!expected.equals(found)) {
- LOG.info(String.format("%s is in %s not %s", javaFile.getFileName(), found, expected));
- return checkSourceRoots();
- }
- return false;
- }
-
- // Apparently, a new source root needs to be created
- LOG.info(String.format("%s is not in the source path", javaFile));
- return checkSourceRoots();
- }
-
- Set<Path> sourceRoots() {
- return Collections.unmodifiableSet(sourceRoots);
- }
-
- Set<Path> allJavaFiles() {
- return Collections.unmodifiableSet(allJavaFiles);
- }
-
- private boolean checkSourceRoots() {
- var newSourceRoots = sourcePath(workspaceRoots);
- if (!newSourceRoots.equals(sourceRoots)) {
- LOG.info(String.format("Source path has changed from %s to %s", sourceRoots, newSourceRoots));
- sourceRoots = newSourceRoots;
- return true;
- }
- return false;
- }
-
- static boolean isJavaFile(Path file) {
- var name = file.getFileName().toString();
- // We hide module-info.java from javac, because when javac sees module-info.java
- // it goes into "module mode" and starts looking for classes on the module class path.
- // This becomes evident when javac starts recompiling *way too much* on each task,
- // because it doesn't realize there are already up-to-date .class files.
- // The better solution would be for java-language server to detect the presence of module-info.java,
- // and go into its own "module mode" where it infers a module source path and a module class path.
- return name.endsWith(".java") && !name.equals("module-info.java");
- }
-
- static boolean isJavaFile(URI uri) {
- return uri.getScheme().equals("file") && isJavaFile(Paths.get(uri));
- }
-
- private static Set<Path> allJavaFilesInDirs(Set<Path> dirs) {
- var all = new HashSet<Path>();
- for (var dir : dirs) {
- try {
- Files.walk(dir).filter(SourcePath::isJavaFile).forEach(all::add);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- return all;
- }
-
- private static Set<Path> sourcePath(Set<Path> workspaceRoots) {
- LOG.info("Searching for source roots in " + workspaceRoots);
-
- var certaintyThreshold = 10;
- var sourceRoots = new HashMap<Path, Integer>();
- fileLoop:
- for (var file : allJavaFilesInDirs(workspaceRoots)) {
- // First, check if we already have a high-confidence source root containing file
- for (var root : sourceRoots.keySet()) {
- var confidence = sourceRoots.get(root);
- if (file.startsWith(root) && confidence > certaintyThreshold) {
- continue fileLoop;
- }
- }
- // Otherwise, parse the file and look at its package declaration
- var parse = Parser.parse(file);
- var packageName = Objects.toString(parse.getPackageName(), "");
- var packagePath = packageName.replace('.', File.separatorChar);
- // If file has no package declaration, don't try to guess a source root
- // This is probably a new file that the user will add a package declaration to later
- if (packagePath.isEmpty()) {
- LOG.warning("Ignoring file with missing package declaration " + file);
- continue fileLoop;
- }
- // If path to file contradicts package declaration, give up
- var dir = file.getParent();
- if (!dir.endsWith(packagePath)) {
- LOG.warning("Java source file " + file + " is not in " + packagePath);
- continue fileLoop;
- }
- // Otherwise, use the package declaration to guess the source root
- var up = Paths.get(packagePath).getNameCount();
- var sourceRoot = dir;
- for (int i = 0; i < up; i++) {
- sourceRoot = sourceRoot.getParent();
- }
- // Increment our confidence in sourceRoot as a source root
- var count = sourceRoots.getOrDefault(sourceRoot, 0);
- sourceRoots.put(sourceRoot, count + 1);
- }
- return sourceRoots.keySet();
- }
-
- private static final Logger LOG = Logger.getLogger("main");
-}