diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt index 1894da96b..ea7ca1fe6 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt @@ -18,8 +18,10 @@ package com.itsaky.lsp.models import java.nio.file.Path +import java.util.* +import kotlin.collections.ArrayList -data class CodeActionParams(var cursor: Position, var diagnostics: List) +data class CodeActionParams(var file: Path, var range: Range, var diagnostics: List) data class CodeActionResult (var actions: List ) { constructor() : this (ArrayList()) @@ -27,8 +29,24 @@ data class CodeActionResult (var actions: List ) { data class CodeActionItem ( var title: String, - var changes: List) + var changes: List, + var kind: CodeActionKind +) { + constructor() : this ("", Collections.emptyList(), CodeActionKind.None) +} -data class DocumentChange (var file: Path, var edits: List) +enum class CodeActionKind { + QuickFix, + + None +} -data class TextEdit (var range: Range, var newText: String) \ No newline at end of file +data class DocumentChange (var file: Path?, var edits: List) { + constructor() : this (null, Collections.emptyList()) +} + +data class TextEdit (var range: Range, var newText: String) { + companion object { + @JvmField val NONE: TextEdit = TextEdit(Range.NONE, ""); + } +} \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt index 5e63bbef4..d800dcfef 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt @@ -23,7 +23,7 @@ data class Location (var file: Path, var range: Range) data class Position(var line: Int, var column: Int) { companion object { - @JvmField val NONE = Position (0, 0) + @JvmField val NONE = Position (-1, -1) } } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/Cache.java b/lsp/java/src/main/java/com/itsaky/lsp/java/Cache.java index d82d22256..c9fbc872b 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/Cache.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/Cache.java @@ -7,7 +7,7 @@ import java.util.Map; import java.util.Objects; /** Cache maps a file + an arbitrary key to a value. When the file is modified, the mapping expires. */ -class Cache { +public class Cache { private static class Key { final Path file; final K key; @@ -41,11 +41,11 @@ class Cache { private final Map map = new HashMap<>(); - boolean has(Path file, K k) { + public boolean has(Path file, K k) { return !needs(file, k); } - boolean needs(Path file, K k) { + public boolean needs(Path file, K k) { // If key is not in map, it needs to be loaded Key key = new Key(file, k); if (!map.containsKey(key)) return true; @@ -57,14 +57,14 @@ class Cache { return value.created.isBefore(modified); } - void load(Path file, K k, V v) { + public void load(Path file, K k, V v) { // TODO limit total size of cache Key key = new Key(file, k); Value value = new Value(v); map.put(key, value); } - V get(Path file, K k) { + public V get(Path file, K k) { Key key = new Key(file, k); if (!map.containsKey(key)) { throw new IllegalArgumentException(k + " is not in map " + map); diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FileStore.java b/lsp/java/src/main/java/com/itsaky/lsp/java/FileStore.java index 614d5da2f..3af5fe0f3 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FileStore.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/FileStore.java @@ -58,7 +58,7 @@ public class FileStore { } } - static void setWorkspaceRoots (Set newRoots) { + public static void setWorkspaceRoots (Set newRoots) { for (Path root : workspaceRoots) { if (!newRoots.contains (root)) { workspaceRoots.removeIf (f -> f.startsWith (root)); @@ -81,7 +81,7 @@ public class FileStore { } } - static class FindJavaSources extends SimpleFileVisitor { + public static class FindJavaSources extends SimpleFileVisitor { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (attrs.isSymbolicLink()) { @@ -100,11 +100,11 @@ public class FileStore { } } - static Collection all () { + public static Collection all () { return javaSources.keySet (); } - static List list (String packageName) { + public static List list (String packageName) { List list = new ArrayList<> (); for (Path file : javaSources.keySet ()) { if (Objects.requireNonNull (javaSources.get (file)).packageName.equals (packageName)) { @@ -140,11 +140,11 @@ public class FileStore { return dir; } - static boolean contains (Path file) { + public static boolean contains (Path file) { return isJavaFile (file) && javaSources.containsKey (file); } - static Instant modified (Path file) { + public static Instant modified (Path file) { // If file is open, use last in-memory modification time if (activeDocuments.containsKey (file)) { return Objects.requireNonNull (activeDocuments.get (file)).modified; @@ -157,7 +157,7 @@ public class FileStore { return Objects.requireNonNull (javaSources.get (file)).modified; } - static String packageName (Path file) { + public static String packageName (Path file) { // If we've never checked before, look up package name on disk if (!javaSources.containsKey (file)) { readInfoFromDisk (file); @@ -201,15 +201,15 @@ public class FileStore { return list; } - static void externalCreate (Path file) { + public static void externalCreate (Path file) { readInfoFromDisk (file); } - static void externalChange (Path file) { + public static void externalChange (Path file) { readInfoFromDisk (file); } - static void externalDelete (Path file) { + public static void externalDelete (Path file) { javaSources.remove (file); } @@ -226,7 +226,7 @@ public class FileStore { } } - static void open (DocumentOpenEvent params) { + public static void open (DocumentOpenEvent params) { Path document = params.getOpenedFile (); if (!isJavaFile (document)) { LOG.error ("Opened file is not a java file. Ignoring..."); @@ -236,7 +236,7 @@ public class FileStore { activeDocuments.put (document, new VersionedContent (params.getText (), params.getVersion ())); } - static void change (DocumentChangeEvent params) { + public static void change (DocumentChangeEvent params) { final Path document = params.getChangedFile (); if (!isJavaFile (document)) { LOG.error ("Changed file is not a java file. Ignoring..."); @@ -248,7 +248,7 @@ public class FileStore { activeDocuments.put (document, new VersionedContent (newText, params.getVersion ())); } - static void close (DocumentCloseEvent params) { + public static void close (DocumentCloseEvent params) { final Path document = params.getClosedFile (); if (!isJavaFile (document)) { LOG.error ("Closed file is not a java file. Ignoring..."); @@ -257,7 +257,7 @@ public class FileStore { activeDocuments.remove (document); } - static Set activeDocuments () { + public static Set activeDocuments () { return activeDocuments.keySet (); } @@ -284,7 +284,7 @@ public class FileStore { } } - static InputStream inputStream (Path file) { + public static InputStream inputStream (Path file) { if (activeDocuments.containsKey (file)) { String string = Objects.requireNonNull (activeDocuments.get (file)).content; byte[] bytes = string.getBytes (); @@ -302,7 +302,7 @@ public class FileStore { } } - static BufferedReader bufferedReader (Path file) { + public static BufferedReader bufferedReader (Path file) { if (activeDocuments.containsKey (file)) { String string = Objects.requireNonNull (activeDocuments.get (file)).content; return new BufferedReader (new StringReader (string)); @@ -318,14 +318,14 @@ public class FileStore { } } - static BufferedReader lines (Path file) { + public static BufferedReader lines (Path file) { return bufferedReader (file); } /** * Convert from line/column (1-based) to offset (0-based) */ - static int offset (String contents, int line, int column) { + public static int offset (String contents, int line, int column) { line--; column--; int cursor = 0; @@ -338,7 +338,7 @@ public class FileStore { return cursor + column; } - static boolean isJavaFile (@NonNull Path file) { + public static boolean isJavaFile (@NonNull Path file) { String 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. @@ -349,11 +349,11 @@ public class FileStore { return name.endsWith (".java") && !Files.isDirectory (file) && !name.equals ("module-info.java"); } - static boolean isJavaFile (@NonNull URI uri) { + public static boolean isJavaFile (@NonNull URI uri) { return uri.getScheme ().equals ("file") && isJavaFile (Paths.get (uri)); } - static Optional findDeclaringFile (@NonNull TypeElement el) { + public static Optional findDeclaringFile (@NonNull TypeElement el) { String qualifiedName = el.getQualifiedName ().toString (); String packageName = StringSearch.mostName (qualifiedName); String className = StringSearch.lastName (qualifiedName); diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationAt.java deleted file mode 100644 index 8a94a621f..000000000 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationAt.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.itsaky.lsp.java; - -import com.sun.source.tree.*; -import com.sun.source.util.JavacTask; -import com.sun.source.util.SourcePositions; -import com.sun.source.util.TreeScanner; -import com.sun.source.util.Trees; - -public class FindTypeDeclarationAt extends TreeScanner { - private final SourcePositions pos; - private CompilationUnitTree root; - - public FindTypeDeclarationAt(JavacTask task) { - pos = Trees.instance(task).getSourcePositions(); - } - - @Override - public ClassTree visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - return super.visitCompilationUnit(t, find); - } - - @Override - public ClassTree visitClass(ClassTree t, Long find) { - ClassTree smaller = super.visitClass(t, find); - if (smaller != null) { - return smaller; - } - if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { - return t; - } - return null; - } - - @Override - public ClassTree reduce(ClassTree a, ClassTree b) { - if (a != null) return a; - return b; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarations.java b/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarations.java deleted file mode 100644 index ea1fa458a..000000000 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarations.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.itsaky.lsp.java; - -import com.sun.source.tree.*; -import com.sun.source.util.TreeScanner; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -class FindTypeDeclarations extends TreeScanner> { - private List qualifiedName = new ArrayList<>(); - - @Override - public Void visitCompilationUnit(CompilationUnitTree root, List found) { - String name = Objects.toString(root.getPackageName(), ""); - qualifiedName.add(name); - return super.visitCompilationUnit(root, found); - } - - @Override - public Void visitClass(ClassTree type, List found) { - qualifiedName.add(type.getSimpleName()); - found.add(String.join(".", qualifiedName)); - super.visitClass(type, found); - qualifiedName.remove(qualifiedName.size() - 1); - return null; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaCompilerService.java b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaCompilerService.java index 0d9fbb605..383c853ff 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaCompilerService.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaCompilerService.java @@ -2,6 +2,7 @@ package com.itsaky.lsp.java; import com.itsaky.androidide.utils.Logger; import com.itsaky.lsp.java.utils.Extractors; +import com.itsaky.lsp.java.visitors.FindTypeDeclarations; import com.sun.source.tree.CompilationUnitTree; import java.io.BufferedReader; diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java index 2198c7922..fa2041f34 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java @@ -30,8 +30,8 @@ import com.itsaky.lsp.api.ILanguageServer; import com.itsaky.lsp.api.IReferenceProvider; import com.itsaky.lsp.api.IServerSettings; import com.itsaky.lsp.api.ISignatureHelpProvider; -import com.itsaky.lsp.java.completion.JavaCompletionProvider; -import com.itsaky.lsp.java.completion.SignatureProvider; +import com.itsaky.lsp.java.providers.JavaCompletionProvider; +import com.itsaky.lsp.java.providers.SignatureProvider; import com.itsaky.lsp.java.models.JavaServerConfiguration; import com.itsaky.lsp.models.DocumentChangeEvent; import com.itsaky.lsp.models.DocumentCloseEvent; diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/JsonHelper.java b/lsp/java/src/main/java/com/itsaky/lsp/java/JsonHelper.java deleted file mode 100644 index 7dc8509af..000000000 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/JsonHelper.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.itsaky.lsp.java; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -public class JsonHelper { - public static final Gson GSON = new GsonBuilder().create(); -} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/ReusableCompiler.java b/lsp/java/src/main/java/com/itsaky/lsp/java/ReusableCompiler.java index bbd511f34..fd035562b 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/ReusableCompiler.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/ReusableCompiler.java @@ -44,12 +44,14 @@ import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.DefinedBy; import com.sun.tools.javac.util.DefinedBy.Api; import com.sun.tools.javac.util.Log; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; + import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileManager; @@ -77,205 +79,210 @@ import javax.tools.JavaFileObject; * code and its internal interfaces are subject to change or deletion without notice. */ class ReusableCompiler { - + private static final Logger LOG = Logger.instance ("main"); - private static final JavacTool systemProvider = JavacTool.create(); - - private List currentOptions = new ArrayList<>(); + private static final JavacTool systemProvider = JavacTool.create (); + + private List currentOptions = new ArrayList<> (); private ReusableContext currentContext; private boolean checkedOut; - + + private static Borrow lastBorrow; + /** * Creates a new task as if by {@link javax.tools.JavaCompiler#getTask} and runs the provided worker with it. The * task is only valid while the worker is running. The internal structures may be reused from some previous * compilation. * - * @param fileManager a file manager; if {@code null} use the compiler's standard filemanager + * @param fileManager a file manager; if {@code null} use the compiler's standard filemanager * @param diagnosticListener a diagnostic listener; if {@code null} use the compiler's default method for reporting - * diagnostics - * @param options compiler options, {@code null} means no options - * @param classes names of classes to be processed by annotation processing, {@code null} means no class names - * @param compilationUnits the compilation units to compile, {@code null} means no compilation units + * diagnostics + * @param options compiler options, {@code null} means no options + * @param classes names of classes to be processed by annotation processing, {@code null} means no class names + * @param compilationUnits the compilation units to compile, {@code null} means no compilation units * @return an object representing the compilation - * @throws RuntimeException if an unrecoverable error occurred in a user supplied component. The {@linkplain - * Throwable#getCause() cause} will be the error in user code. + * @throws RuntimeException if an unrecoverable error occurred in a user supplied component. The {@linkplain + * Throwable#getCause() cause} will be the error in user code. * @throws IllegalArgumentException if any of the options are invalid, or if any of the given compilation units are - * of other kind than {@linkplain JavaFileObject.Kind#SOURCE source} + * of other kind than {@linkplain JavaFileObject.Kind#SOURCE source} */ - Borrow getTask( + Borrow getTask ( JavaFileManager fileManager, DiagnosticListener diagnosticListener, Iterable options, Iterable classes, Iterable compilationUnits) { - if (checkedOut) { - throw new RuntimeException("Compiler is already in-use!"); + if (lastBorrow != null && checkedOut) { + // If the compiler is in use, try to cleanup the last task + lastBorrow.close (); } checkedOut = true; List opts = - StreamSupport.stream(options.spliterator(), false).collect(Collectors.toCollection(ArrayList::new)); - if (!opts.equals(currentOptions)) { - LOG.warn(String.format("Options changed from %s to %s, creating new compiler", options, opts)); + StreamSupport.stream (options.spliterator (), false).collect (Collectors.toCollection (ArrayList::new)); + if (!opts.equals (currentOptions)) { + LOG.warn (String.format ("Options changed from %s to %s, creating new compiler", options, opts)); currentOptions = opts; - currentContext = new ReusableContext(opts); + currentContext = new ReusableContext (opts); } JavacTaskImpl task = (JavacTaskImpl) - systemProvider.getTask( + systemProvider.getTask ( null, fileManager, diagnosticListener, opts, classes, compilationUnits, currentContext); - - task.addTaskListener(currentContext); - - return new Borrow(task, currentContext); + + task.addTaskListener (currentContext); + + return new Borrow (task, currentContext); } - + class Borrow implements AutoCloseable { final JavacTask task; boolean closed; - - Borrow(JavacTask task, ReusableContext ctx) { + + Borrow (JavacTask task, ReusableContext ctx) { this.task = task; } - + @Override - public void close() { - if (closed) return; + public void close () { + if (closed) { + return; + } // not returning the context to the pool if task crashes with an exception // the task/context may be in a broken state - currentContext.clear(); + currentContext.clear (); try { - Method method = JavacTaskImpl.class.getDeclaredMethod("cleanup"); - method.setAccessible(true); - method.invoke(task); + Method method = JavacTaskImpl.class.getDeclaredMethod ("cleanup"); + method.setAccessible (true); + method.invoke (task); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Unable to call cleanup() on JavacTaskImpl", e); + throw new RuntimeException ("Unable to call cleanup() on JavacTaskImpl", e); } checkedOut = false; closed = true; } } - + static class ReusableContext extends Context implements TaskListener { - + List arguments; - - ReusableContext(List arguments) { - super(); + + ReusableContext (List arguments) { + super (); this.arguments = arguments; - put(Log.logKey, ReusableLog.factory); - put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory); + put (Log.logKey, ReusableLog.factory); + put (JavaCompiler.compilerKey, ReusableJavaCompiler.factory); } - - void clear() { - drop(Arguments.argsKey); - drop(DiagnosticListener.class); - drop(Log.outKey); - drop(Log.errKey); - drop(JavaFileManager.class); - drop(JavacTask.class); - drop(JavacTrees.class); - drop(JavacElements.class); - - if (ht.get(Log.logKey) instanceof ReusableLog) { + + void clear () { + drop (Arguments.argsKey); + drop (DiagnosticListener.class); + drop (Log.outKey); + drop (Log.errKey); + drop (JavaFileManager.class); + drop (JavacTask.class); + drop (JavacTrees.class); + drop (JavacElements.class); + + if (ht.get (Log.logKey) instanceof ReusableLog) { // log already init-ed - not first round - ((ReusableLog) Log.instance(this)).clear(); - Enter.instance(this).newRound(); - ((ReusableJavaCompiler) ReusableJavaCompiler.instance(this)).clear(); - Types.instance(this).newRound(); - Check.instance(this).newRound(); - Modules.instance(this).newRound(); - Annotate.instance(this).newRound(); - CompileStates.instance(this).clear(); - MultiTaskListener.instance(this).clear(); + ((ReusableLog) Log.instance (this)).clear (); + Enter.instance (this).newRound (); + ((ReusableJavaCompiler) ReusableJavaCompiler.instance (this)).clear (); + Types.instance (this).newRound (); + Check.instance (this).newRound (); + Modules.instance (this).newRound (); + Annotate.instance (this).newRound (); + CompileStates.instance (this).clear (); + MultiTaskListener.instance (this).clear (); } } - + @Override @DefinedBy(Api.COMPILER_TREE) - public void finished(TaskEvent e) { + public void finished (TaskEvent e) { // do nothing } - + @Override @DefinedBy(Api.COMPILER_TREE) - public void started(TaskEvent e) { + public void started (TaskEvent e) { // do nothing } - - void drop(Key k) { - ht.remove(k); + + void drop (Key k) { + ht.remove (k); } - - void drop(Class c) { - ht.remove(key(c)); + + void drop (Class c) { + ht.remove (key (c)); } - + /** * Reusable JavaCompiler; exposes a method to clean up the component from leftovers associated with previous * compilations. */ static class ReusableJavaCompiler extends JavaCompiler { - + static final Factory factory = ReusableJavaCompiler::new; - - ReusableJavaCompiler(Context context) { - super(context); + + ReusableJavaCompiler (Context context) { + super (context); } - + @Override - public void close() { + public void close () { // do nothing } - - void clear() { - newRound(); + + void clear () { + newRound (); } - + @Override - protected void checkReusable() { + protected void checkReusable () { // do nothing - it's ok to reuse the compiler } } - + /** * Reusable Log; exposes a method to clean up the component from leftovers associated with previous * compilations. */ static class ReusableLog extends Log { - + static final Factory factory = ReusableLog::new; - + Context context; - - ReusableLog(Context context) { - super(context); + + ReusableLog (Context context) { + super (context); this.context = context; } - - void clear() { - recorded.clear(); - sourceMap.clear(); + + void clear () { + recorded.clear (); + sourceMap.clear (); nerrors = 0; nwarnings = 0; // Set a fake listener that will lazily lookup the context for the 'real' listener. Since // this field is never updated when a new task is created, we cannot simply reset the field // or keep old value. This is a hack to workaround the limitations in the current infrastructure. diagListener = - new DiagnosticListener() { + new DiagnosticListener () { DiagnosticListener cachedListener; - + @Override @DefinedBy(Api.COMPILER) @SuppressWarnings("unchecked") - public void report(Diagnostic diagnostic) { + public void report (Diagnostic diagnostic) { if (cachedListener == null) { - cachedListener = context.get(DiagnosticListener.class); + cachedListener = context.get (DiagnosticListener.class); } - cachedListener.report(diagnostic); + cachedListener.report (diagnostic); } }; } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java new file mode 100644 index 000000000..c1852f173 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java @@ -0,0 +1,415 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.providers; + +import androidx.annotation.NonNull; + +import com.itsaky.androidide.utils.Logger; +import com.itsaky.lsp.api.ICodeActionProvider; +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.rewrite.AddException; +import com.itsaky.lsp.java.rewrite.AddImport; +import com.itsaky.lsp.java.rewrite.AddSuppressWarningAnnotation; +import com.itsaky.lsp.java.rewrite.ConvertFieldToBlock; +import com.itsaky.lsp.java.rewrite.ConvertVariableToStatement; +import com.itsaky.lsp.java.rewrite.CreateMissingMethod; +import com.itsaky.lsp.java.rewrite.GenerateRecordConstructor; +import com.itsaky.lsp.java.rewrite.ImplementAbstractMethods; +import com.itsaky.lsp.java.rewrite.OverrideInheritedMethod; +import com.itsaky.lsp.java.rewrite.RemoveClass; +import com.itsaky.lsp.java.rewrite.RemoveException; +import com.itsaky.lsp.java.rewrite.RemoveMethod; +import com.itsaky.lsp.java.rewrite.Rewrite; +import com.itsaky.lsp.java.visitors.FindMethodDeclarationAt; +import com.itsaky.lsp.java.visitors.FindTypeDeclarationAt; +import com.itsaky.lsp.models.CodeActionItem; +import com.itsaky.lsp.models.CodeActionKind; +import com.itsaky.lsp.models.CodeActionParams; +import com.itsaky.lsp.models.CodeActionResult; +import com.itsaky.lsp.models.DiagnosticItem; +import com.itsaky.lsp.models.DocumentChange; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +public class CodeActionProvider implements ICodeActionProvider { + + private final CompilerProvider compiler; + + public CodeActionProvider(CompilerProvider compiler) { + this.compiler = compiler; + } + + @NonNull + @Override + public CodeActionResult codeActions (@NonNull CodeActionParams params) { + if (params.getDiagnostics ().isEmpty ()) { + return codeActionsForCursor (params); + } else { + return new CodeActionResult (codeActionForDiagnostics (params)); + } + } + + public CodeActionResult codeActionsForCursor(CodeActionParams params) { + final Path file = params.getFile (); + LOG.info(String.format(Locale.getDefault (), "Find code actions at %s(%d)...", file.getFileName (), params.getRange().getStart().getLine() + 1)); + Instant started = Instant.now(); + // TODO this get-map / convert-to-CodeAction split is an ugly workaround of the fact that we need a new compile + // task to generate the code actions + // If we switch to resolving code actions asynchronously using Command, that will fix this problem. + final TreeMap rewrites = new TreeMap<> (); + try (final CompileTask task = compiler.compile(file)) { + long elapsed = Duration.between(started, Instant.now()).toMillis(); + LOG.info(String.format(Locale.getDefault (), "...compiled in %d ms", elapsed)); + final LineMap lines = task.root().getLineMap(); + final long cursor = lines.getPosition(params.getRange().getStart().getLine() + 1, params.getRange().getStart().getColumn () + 1); + rewrites.putAll(overrideInheritedMethods(task, file, cursor)); + } + + List actions = new ArrayList<> (); + for (String title : rewrites.keySet()) { + // TODO are these all quick fixes? + actions.addAll(createQuickFix(title, rewrites.get(title))); + } + long elapsed = Duration.between(started, Instant.now()).toMillis(); + LOG.info(String.format(Locale.getDefault (), "...created %d actions in %d ms", actions.size(), elapsed)); + return new CodeActionResult (actions); + } + + private Map overrideInheritedMethods(CompileTask task, Path file, long cursor) { + if (!isBlankLine(task.root(), cursor)) return Collections.emptyMap (); + if (isInMethod(task, cursor)) return Collections.emptyMap (); + + MethodTree methodTree = new FindMethodDeclarationAt(task.task).scan(task.root(), cursor); + if (methodTree != null) return Collections.emptyMap (); + + final TreeMap actions = new TreeMap<> (); + final Trees trees = Trees.instance(task.task); + final ClassTree classTree = new FindTypeDeclarationAt(task.task).scan(task.root(), cursor); + if (classTree == null) return Collections.emptyMap (); + final TreePath classPath = trees.getPath(task.root(), classTree); + final Elements elements = task.task.getElements(); + final TypeElement classElement = (TypeElement) trees.getElement(classPath); + for (Element member : elements.getAllMembers(classElement)) { + if (member.getModifiers().contains(Modifier.FINAL)) continue; + if (member.getKind() != ElementKind.METHOD) continue; + final ExecutableElement method = (ExecutableElement) member; + final TypeElement methodSource = (TypeElement) member.getEnclosingElement(); + if (methodSource.getQualifiedName().contentEquals("java.lang.Object")) continue; + if (methodSource.equals(classElement)) continue; + final MethodPtr ptr = new MethodPtr (task.task, method); + + final Rewrite rewrite = + new OverrideInheritedMethod ( + ptr.className, ptr.methodName, ptr.erasedParameterTypes, file, (int) cursor); + final String title = "Override '" + method.getSimpleName() + "' from " + ptr.className; + + actions.put(title, rewrite); + } + return actions; + } + + private boolean isInMethod(CompileTask task, long cursor) { + MethodTree method = new FindMethodDeclarationAt(task.task).scan(task.root(), cursor); + return method != null; + } + + private boolean isBlankLine(CompilationUnitTree root, long cursor) { + LineMap lines = root.getLineMap(); + long line = lines.getLineNumber(cursor); + long start = lines.getStartPosition(line); + CharSequence contents; + try { + contents = root.getSourceFile().getCharContent(true); + } catch (IOException e) { + throw new RuntimeException(e); + } + for (long i = start; i < cursor; i++) { + if (!Character.isWhitespace(contents.charAt((int) i))) { + return false; + } + } + return true; + } + + public List codeActionForDiagnostics(CodeActionParams params) { + LOG.info(String.format(Locale.getDefault (), "Check %d diagnostics for quick fixes...", params.getDiagnostics().size())); + Instant started = Instant.now(); + Path file = params.getFile (); + try (final CompileTask task = compiler.compile(file)) { + List actions = new ArrayList<>(); + for (DiagnosticItem d : params.getDiagnostics()) { + List newActions = codeActionForDiagnostic(task, file, d); + actions.addAll(newActions); + } + long elapsed = Duration.between(started, Instant.now()).toMillis(); + LOG.info(String.format(Locale.getDefault (), "...created %d quick fixes in %d ms", actions.size(), elapsed)); + return actions; + } + } + + private List codeActionForDiagnostic(CompileTask task, Path file, DiagnosticItem d) { + // TODO this should be done asynchronously using executeCommand + switch (d.getCode()) { + case "unused_local": + final Rewrite toStatement = new ConvertVariableToStatement (file, findPosition(task, d.getRange().getStart())); + return createQuickFix("Convert to statement", toStatement); + case "unused_field": + final Rewrite toBlock = new ConvertFieldToBlock (file, findPosition(task, d.getRange().getStart())); + return createQuickFix("Convert to block", toBlock); + case "unused_class": + final Rewrite removeClass = new RemoveClass (file, findPosition(task, d.getRange().getStart())); + return createQuickFix("Remove class", removeClass); + case "unused_method": + final MethodPtr unusedMethod = findMethod(task, d.getRange()); + final Rewrite removeMethod = new RemoveMethod (unusedMethod.className, unusedMethod.methodName, unusedMethod.erasedParameterTypes); + return createQuickFix("Remove method", removeMethod); + case "unused_throws": + final CharSequence shortExceptionName = extractRange(task, d.getRange()); + final String notThrown = extractNotThrownExceptionName(d.getMessage()); + final MethodPtr methodWithExtraThrow = findMethod(task, d.getRange()); + final Rewrite removeThrow = + new RemoveException ( + methodWithExtraThrow.className, + methodWithExtraThrow.methodName, + methodWithExtraThrow.erasedParameterTypes, + notThrown); + return createQuickFix("Remove '" + shortExceptionName + "'", removeThrow); + case "compiler.warn.unchecked.call.mbr.of.raw.type": + final MethodPtr warnedMethod = findMethod(task, d.getRange()); + final Rewrite suppressWarning = + new AddSuppressWarningAnnotation ( + warnedMethod.className, warnedMethod.methodName, warnedMethod.erasedParameterTypes); + return createQuickFix("Suppress 'unchecked' warning", suppressWarning); + case "compiler.err.unreported.exception.need.to.catch.or.throw": + final MethodPtr needsThrow = findMethod(task, d.getRange()); + final String exceptionName = extractExceptionName(d.getMessage()); + final Rewrite addThrows = + new AddException ( + needsThrow.className, + needsThrow.methodName, + needsThrow.erasedParameterTypes, + exceptionName); + return createQuickFix("Add 'throws'", addThrows); + case "compiler.err.cant.resolve.location": + CharSequence simpleName = extractRange(task, d.getRange()); + List allImports = new ArrayList<>(); + for (String qualifiedName : compiler.publicTopLevelTypes()) { + if (qualifiedName.endsWith("." + simpleName)) { + String title = "Import '" + qualifiedName + "'"; + final Rewrite addImport = new AddImport (file, qualifiedName); + allImports.addAll(createQuickFix(title, addImport)); + } + } + return allImports; + case "compiler.err.var.not.initialized.in.default.constructor": + final String needsConstructor = findClassNeedingConstructor(task, d.getRange()); + if (needsConstructor == null) return Collections.emptyList (); + final Rewrite generateConstructor = new GenerateRecordConstructor (needsConstructor); + return createQuickFix("Generate constructor", generateConstructor); + case "compiler.err.does.not.override.abstract": + final CompilationUnitTree root = task.root(); + final LineMap lines = root.getLineMap(); + final FindTypeDeclarationAt treeFinder = newClassFinder(task); + final Range range = d.getRange(); + final long position = lines.getPosition(range.getStart().getLine() + 1, range.getStart().getColumn () + 1); + final ClassTree tree = treeFinder.scan(root, position); + final Rewrite implementAbstracts = new ImplementAbstractMethods (file, tree, treeFinder.getStoredTreePath()); + return createQuickFix("Implement abstract methods", implementAbstracts); + case "compiler.err.cant.resolve.location.args": + final Rewrite missingMethod = new CreateMissingMethod (file, findPosition(task, d.getRange().getStart())); + return createQuickFix("Create missing method", missingMethod); + default: + return Collections.emptyList (); + } + } + + private int findPosition(CompileTask task, Position position) { + final LineMap lines = task.root().getLineMap(); + return (int) lines.getPosition(position.getLine() + 1, position.getColumn () + 1); + } + + private String findClassNeedingConstructor(CompileTask task, Range range) { + final ClassTree type = findClassTree(task, range); + if (type == null || hasConstructor(task, type)) return null; + return qualifiedName(task, type); + } + + private String findClass(CompileTask task, Range range) { + final ClassTree type = findClassTree(task, range); + if (type == null) return null; + return qualifiedName(task, type); + } + + private FindTypeDeclarationAt newClassFinder (CompileTask task) { + return new FindTypeDeclarationAt(task.task); + } + + private ClassTree findClassTree(CompileTask task, Range range) { + final long position = task.root().getLineMap().getPosition(range.getStart().getLine() + 1, range.getStart().getColumn () + 1); + return newClassFinder(task).scan(task.root(), position); + } + + private String qualifiedName(CompileTask task, ClassTree tree) { + final Trees trees = Trees.instance(task.task); + final TreePath path = trees.getPath(task.root(), tree); + final TypeElement type = (TypeElement) trees.getElement(path); + return type.getQualifiedName().toString(); + } + + private boolean hasConstructor(CompileTask task, ClassTree type) { + for (Tree member : type.getMembers()) { + if (member instanceof MethodTree) { + MethodTree method = (MethodTree) member; + if (isConstructor(task, method)) { + return true; + } + } + } + return false; + } + + private boolean isConstructor(CompileTask task, MethodTree method) { + return method.getName().contentEquals("") && !synthetic (task, method); + } + + private boolean synthetic (CompileTask task, MethodTree method) { + return Trees.instance(task.task).getSourcePositions().getStartPosition(task.root(), method) != -1; + } + + private MethodPtr findMethod(CompileTask task, Range range) { + final Trees trees = Trees.instance(task.task); + final long position = task.root().getLineMap().getPosition(range.getStart().getLine() + 1, range.getStart().getColumn () + 1); + final MethodTree tree = new FindMethodDeclarationAt (task.task).scan(task.root(), position); + final TreePath path = trees.getPath(task.root(), tree); + final ExecutableElement method = (ExecutableElement) trees.getElement(path); + return new MethodPtr (task.task, method); + } + + static class MethodPtr { + String className, methodName; + String[] erasedParameterTypes; + + MethodPtr(JavacTask task, ExecutableElement method) { + final Types types = task.getTypes(); + final TypeElement parent = (TypeElement) method.getEnclosingElement(); + className = parent.getQualifiedName().toString(); + methodName = method.getSimpleName().toString(); + erasedParameterTypes = new String[method.getParameters().size()]; + for (int i = 0; i < erasedParameterTypes.length; i++) { + final VariableElement param = method.getParameters().get(i); + final TypeMirror type = param.asType(); + final TypeMirror erased = types.erasure(type); + erasedParameterTypes[i] = erased.toString(); + } + } + } + + private static final Pattern NOT_THROWN_EXCEPTION = Pattern.compile("^'((\\w+\\.)*\\w+)' is not thrown"); + + private String extractNotThrownExceptionName(String message) { + final Matcher matcher = NOT_THROWN_EXCEPTION.matcher(message); + if (!matcher.find()) { + LOG.warn(String.format("`%s` doesn't match `%s`", message, NOT_THROWN_EXCEPTION)); + return ""; + } + return matcher.group(1); + } + + private static final Pattern UNREPORTED_EXCEPTION = Pattern.compile("unreported exception ((\\w+\\.)*\\w+)"); + + private String extractExceptionName(String message) { + final Matcher matcher = UNREPORTED_EXCEPTION.matcher(message); + if (!matcher.find()) { + LOG.warn (String.format("`%s` doesn't match `%s`", message, UNREPORTED_EXCEPTION)); + return ""; + } + return matcher.group(1); + } + + private CharSequence extractRange(CompileTask task, Range range) { + CharSequence contents; + try { + contents = task.root().getSourceFile().getCharContent(true); + } catch (IOException e) { + throw new RuntimeException(e); + } + int start = (int) task.root().getLineMap().getPosition(range.getStart().getLine() + 1, + range.getStart().getColumn () + 1); + int end = (int) task.root().getLineMap().getPosition(range.getEnd().getLine() + 1, + range.getEnd().getColumn () + 1); + return contents.subSequence(start, end); + } + + private List createQuickFix (String title, Rewrite rewrite) { + final Map edits = rewrite.rewrite(compiler); + + if (edits == Rewrite.CANCELLED) { + return Collections.emptyList (); + } + + CodeActionItem action = new CodeActionItem (); + action.setKind(CodeActionKind.QuickFix); + action.setTitle(title); + for (final Path file : edits.keySet()) { + TextEdit[] textEdits = edits.get (file); + if (textEdits == null) { + continue; + } + final DocumentChange change = new DocumentChange (); + change.setEdits (Arrays.asList (textEdits)); + action.getChanges ().add (change); + } + return Collections.singletonList (action); + } + + private static final Logger LOG = Logger.instance ("main"); +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/JavaCompletionProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCompletionProvider.java similarity index 96% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/JavaCompletionProvider.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCompletionProvider.java index 1d9cfefcb..a2989231a 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/JavaCompletionProvider.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCompletionProvider.java @@ -1,4 +1,23 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.providers; + +import static com.itsaky.lsp.java.utils.EditHelper.repeatSpaces; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,6 +33,9 @@ import com.itsaky.lsp.java.StringSearch; import com.itsaky.lsp.java.rewrite.AddImport; import com.itsaky.lsp.java.utils.EditHelper; import com.itsaky.lsp.java.utils.Extractors; +import com.itsaky.lsp.java.utils.ScopeHelper; +import com.itsaky.lsp.java.visitors.FindCompletionsAt; +import com.itsaky.lsp.java.visitors.PruneMethodBodies; import com.itsaky.lsp.models.Command; import com.itsaky.lsp.models.CompletionData; import com.itsaky.lsp.models.CompletionItem; @@ -41,6 +63,7 @@ import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -63,7 +86,6 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.NoType; -import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Types; @@ -100,7 +122,7 @@ public class JavaCompletionProvider implements ICompletionProvider { this.completingFile = file; this.cursor = task.root.getLineMap().getPosition(line, column); - StringBuilder contents = new PruneMethodBodies(task.task).scan(task.root, cursor); + StringBuilder contents = new PruneMethodBodies (task.task).scan(task.root, cursor); int endOfLine = endOfLine(contents, (int) cursor); contents.insert(endOfLine, ';'); CompletionResult list = compileAndComplete(file, contents.toString(), cursor); @@ -125,7 +147,7 @@ public class JavaCompletionProvider implements ICompletionProvider { boolean endsWithParen = endsWithParen(contents, (int) cursor); try (CompileTask task = compiler.compile(Collections.singletonList (source))) { LOG.info("...compiled in " + Duration.between(started, Instant.now()).toMillis() + "ms"); - TreePath path = new FindCompletionsAt(task.task).scan(task.root(), cursor); + TreePath path = new FindCompletionsAt (task.task).scan(task.root(), cursor); switch (path.getLeaf().getKind()) { case IDENTIFIER: return completeIdentifier(task, path, partial, endsWithParen); @@ -595,7 +617,7 @@ public class JavaCompletionProvider implements ICompletionProvider { } AddImport addImport = new AddImport(path, className); - return Collections.singletonList (addImport.rewrite(compiler)); + return Arrays.asList (Objects.requireNonNull (addImport.rewrite (compiler).get (path))); } private CompletionItem snippetItem(String label, String snippet) { @@ -697,7 +719,7 @@ public class JavaCompletionProvider implements ICompletionProvider { String insertText = built.toString (); insertText = insertText.replace ("\n", "\n" + repeatSpaces (indent)); - // TODO Auto-import required classes. + // TODO Auto-import required classes instead of specifying qualified names. CompletionItem item = new CompletionItem(); item.setLabel (built.name); item.setKind (CompletionItemKind.METHOD); @@ -728,14 +750,6 @@ public class JavaCompletionProvider implements ICompletionProvider { return null; } - private String repeatSpaces (int count) { - StringBuilder result = new StringBuilder (); - for (int i = 0; i < count; i++) { - result.append (" "); - } - return result.toString (); - } - @Nullable private CompletionData data(CompileTask task, Element element, int overloads) { CompletionData data = new CompletionData(); diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/SignatureProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/SignatureProvider.java similarity index 91% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/SignatureProvider.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/providers/SignatureProvider.java index 6a8748106..5cee7eb60 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/SignatureProvider.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/SignatureProvider.java @@ -1,20 +1,38 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.providers; import androidx.annotation.NonNull; import com.itsaky.lsp.api.ISignatureHelpProvider; import com.itsaky.lsp.java.CompileTask; import com.itsaky.lsp.java.CompilerProvider; -import com.itsaky.lsp.java.FindHelper; +import com.itsaky.lsp.java.utils.FindHelper; import com.itsaky.lsp.java.MarkdownHelper; import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.utils.ScopeHelper; import com.itsaky.lsp.java.utils.ShortTypePrinter; +import com.itsaky.lsp.java.visitors.FindInvocationAt; import com.itsaky.lsp.models.ParameterInformation; import com.itsaky.lsp.models.SignatureHelp; import com.itsaky.lsp.models.SignatureHelpParams; import com.itsaky.lsp.models.SignatureInformation; import com.sun.source.doctree.DocCommentTree; -import com.sun.source.doctree.DocTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; @@ -23,14 +41,11 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Scope; -import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.DocTrees; import com.sun.source.util.SourcePositions; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; -import com.sun.tools.javac.parser.DocCommentParser; -import com.sun.tools.javac.parser.Parser; import java.nio.file.Path; import java.util.ArrayList; @@ -76,12 +91,12 @@ public class SignatureProvider implements ISignatureHelpProvider { // TODO prune try (CompileTask task = compiler.compile(file)) { long cursor = task.root().getLineMap().getPosition(line, column); - TreePath path = new FindInvocationAt(task.task).scan(task.root(), cursor); + TreePath path = new FindInvocationAt (task.task).scan(task.root(), cursor); if (path == null) return NOT_SUPPORTED; if (path.getLeaf() instanceof MethodInvocationTree) { MethodInvocationTree invoke = (MethodInvocationTree) path.getLeaf(); List overloads = methodOverloads(task, invoke); - List signatures = new ArrayList(); + List signatures = new ArrayList<> (); for (ExecutableElement method : overloads) { SignatureInformation info = info(method); addSourceInfo(task, method, info); @@ -95,7 +110,7 @@ public class SignatureProvider implements ISignatureHelpProvider { if (path.getLeaf() instanceof NewClassTree) { NewClassTree invoke = (NewClassTree) path.getLeaf(); List overloads = constructorOverloads(task, invoke); - List signatures = new ArrayList(); + List signatures = new ArrayList<> (); for (ExecutableElement method : overloads) { SignatureInformation info = info(method); addSourceInfo(task, method, info); @@ -127,7 +142,7 @@ public class SignatureProvider implements ISignatureHelpProvider { Trees trees = Trees.instance(task.task); TreePath path = trees.getPath(task.root(), method); Scope scope = trees.getScope(path); - List list = new ArrayList(); + List list = new ArrayList<> (); Predicate filter = name -> method.getName().contentEquals(name); // TODO add static imports for (Element member : ScopeHelper.scopeMembers(task, scope, filter)) { @@ -150,7 +165,7 @@ public class SignatureProvider implements ISignatureHelpProvider { return Collections.emptyList (); } - List list = new ArrayList(); + List list = new ArrayList<> (); for (Element member : task.task.getElements().getAllMembers(type)) { if (member.getKind() != ElementKind.METHOD) continue; if (!member.getSimpleName().contentEquals(method.getIdentifier())) continue; @@ -179,7 +194,7 @@ public class SignatureProvider implements ISignatureHelpProvider { TreePath path = trees.getPath(task.root(), method.getIdentifier()); Scope scope = trees.getScope(path); TypeElement type = (TypeElement) trees.getElement(path); - List list = new ArrayList(); + List list = new ArrayList<> (); for (Element member : task.task.getElements().getAllMembers(type)) { if (member.getKind() != ElementKind.CONSTRUCTOR) continue; if (!trees.isAccessible(scope, member, (DeclaredType) type.asType())) continue; @@ -201,7 +216,7 @@ public class SignatureProvider implements ISignatureHelpProvider { @NonNull private List parameters(@NonNull ExecutableElement method) { - List list = new ArrayList(); + List list = new ArrayList<> (); for (VariableElement p : method.getParameters()) { list.add(parameter(p)); } @@ -249,7 +264,7 @@ public class SignatureProvider implements ISignatureHelpProvider { @NonNull private List parametersFromSource(MethodTree source) { - List list = new ArrayList(); + List list = new ArrayList<> (); for (VariableTree p : source.getParameters()) { ParameterInformation info = new ParameterInformation (); info.setLabel (p.getType () + " " + p.getName ()); diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddException.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddException.java new file mode 100644 index 000000000..3387b39d7 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddException.java @@ -0,0 +1,84 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import javax.lang.model.element.ExecutableElement; + +public class AddException implements Rewrite { + final String className, methodName; + final String[] erasedParameterTypes; + final String exceptionType; + + public AddException(String className, String methodName, String[] erasedParameterTypes, String exceptionType) { + this.className = className; + this.methodName = methodName; + this.erasedParameterTypes = erasedParameterTypes; + this.exceptionType = exceptionType; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + Path file = compiler.findTypeDeclaration(className); + if (file == CompilerProvider.NOT_FOUND) { + return CANCELLED; + } + + try (CompileTask task = compiler.compile(file)) { + Trees trees = Trees.instance(task.task); + ExecutableElement methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes); + MethodTree methodTree = trees.getTree(methodElement); + SourcePositions pos = trees.getSourcePositions(); + LineMap lines = task.root().getLineMap(); + long startBody = pos.getStartPosition(task.root(), methodTree.getBody()); + int line = (int) lines.getLineNumber(startBody); + int column = (int) lines.getColumnNumber(startBody); + Position insertPos = new Position(line - 1, column - 1); + String simpleName = exceptionType; + int lastDot = simpleName.lastIndexOf('.'); + if (lastDot != -1) { + simpleName = exceptionType.substring(lastDot + 1); + } + + String insertText; + if (methodTree.getThrows().isEmpty()) { + insertText = "throws " + simpleName + " "; + } else { + insertText = ", " + simpleName + " "; + } + TextEdit insertThrows = new TextEdit(new Range (insertPos, insertPos), insertText); + // TODO add import if needed + TextEdit[] edits = {insertThrows}; + return Collections.singletonMap (file, edits); + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddImport.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddImport.java index 93594d873..68a7654fe 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddImport.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddImport.java @@ -28,7 +28,9 @@ import com.sun.source.util.SourcePositions; import com.sun.source.util.Trees; import java.nio.file.Path; +import java.util.Collections; import java.util.List; +import java.util.Map; public class AddImport implements Rewrite { final Path file; @@ -40,11 +42,11 @@ public class AddImport implements Rewrite { } @Override - public TextEdit rewrite(CompilerProvider compiler) { + public Map rewrite(CompilerProvider compiler) { final ParseTask task = compiler.parse(file); Position point = insertPosition(task); String text = "import " + className + ";\n"; - return new TextEdit(new Range (point, point), text); + return Collections.singletonMap (file, new TextEdit[]{new TextEdit(new Range (point, point), text)}); } private Position insertPosition(ParseTask task) { diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddSuppressWarningAnnotation.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddSuppressWarningAnnotation.java new file mode 100644 index 000000000..ec0c695b5 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/AddSuppressWarningAnnotation.java @@ -0,0 +1,72 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import javax.lang.model.element.ExecutableElement; + +public class AddSuppressWarningAnnotation implements Rewrite { + final String className, methodName; + final String[] erasedParameterTypes; + + public AddSuppressWarningAnnotation(String className, String methodName, String[] erasedParameterTypes) { + this.className = className; + this.methodName = methodName; + this.erasedParameterTypes = erasedParameterTypes; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + Path file = compiler.findTypeDeclaration(className); + if (file == CompilerProvider.NOT_FOUND) { + return CANCELLED; + } + try (CompileTask task = compiler.compile(file)) { + Trees trees = Trees.instance(task.task); + ExecutableElement methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes); + MethodTree methodTree = trees.getTree(methodElement); + SourcePositions pos = trees.getSourcePositions(); + int startMethod = (int) pos.getStartPosition(task.root(), methodTree); + LineMap lines = task.root().getLineMap(); + int line = (int) lines.getLineNumber(startMethod); + int column = (int) lines.getColumnNumber(startMethod); + int startLine = (int) lines.getStartPosition(line); + String indent = EditHelper.repeatSpaces (startMethod - startLine); + String insertText = "@SuppressWarnings(\"unchecked\")\n" + indent; + Position insertPoint = new Position(line - 1, column - 1); + TextEdit insert = new TextEdit(new Range(insertPoint, insertPoint), insertText); + TextEdit[] edits = {insert}; + return Collections.singletonMap (file, edits); + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertFieldToBlock.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertFieldToBlock.java new file mode 100644 index 000000000..8a4209622 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertFieldToBlock.java @@ -0,0 +1,82 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import javax.lang.model.element.Modifier; + +public class ConvertFieldToBlock implements Rewrite { + final Path file; + final int position; + + public ConvertFieldToBlock(Path file, int position) { + this.file = file; + this.position = position; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + ParseTask task = compiler.parse(file); + Trees trees = Trees.instance(task.task); + SourcePositions pos = trees.getSourcePositions(); + LineMap lines = task.root.getLineMap(); + VariableTree variable = ConvertVariableToStatement.findVariable(task, position); + if (variable == null) { + return CANCELLED; + } + ExpressionTree expression = variable.getInitializer(); + if (!ConvertVariableToStatement.isExpressionStatement(expression)) { + return CANCELLED; + } + long start = pos.getStartPosition(task.root, variable); + long end = pos.getStartPosition(task.root, expression); + int startLine = (int) lines.getLineNumber(start); + int startColumn = (int) lines.getColumnNumber(start); + Position startPos = new Position (startLine - 1, startColumn - 1); + int endLine = (int) lines.getLineNumber(end); + int endColumn = (int) lines.getColumnNumber(end); + Position endPos = new Position(endLine - 1, endColumn - 1); + Range deleteLhs = new Range(startPos, endPos); + TextEdit fixLhs = new TextEdit(deleteLhs, "{ "); + if (variable.getModifiers().getFlags().contains(Modifier.STATIC)) { + fixLhs.setNewText("static { "); + } + long right = pos.getEndPosition(task.root, variable); + int rightLine = (int) lines.getLineNumber(right); + int rightColumn = (int) lines.getColumnNumber(right); + Position rightPos = new Position(rightLine - 1, rightColumn - 1); + Range insertRight = new Range(rightPos, rightPos); + TextEdit fixRhs = new TextEdit(insertRight, " }"); + TextEdit[] edits = {fixLhs, fixRhs}; + return Collections.singletonMap (file, edits); + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertVariableToStatement.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertVariableToStatement.java new file mode 100644 index 000000000..6f2463acd --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ConvertVariableToStatement.java @@ -0,0 +1,97 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.visitors.FindVariableAt; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +public class ConvertVariableToStatement implements Rewrite { + final Path file; + final int position; + + public ConvertVariableToStatement(Path file, int position) { + this.file = file; + this.position = position; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + final ParseTask task = compiler.parse(file); + final Trees trees = Trees.instance(task.task); + final SourcePositions pos = trees.getSourcePositions(); + final LineMap lines = task.root.getLineMap(); + final VariableTree variable = findVariable(task, position); + if (variable == null) { + return CANCELLED; + } + ExpressionTree expression = variable.getInitializer(); + if (expression == null) { + return CANCELLED; + } + if (!isExpressionStatement(expression)) { + return CANCELLED; + } + long start = pos.getStartPosition(task.root, variable); + long end = pos.getStartPosition(task.root, expression); + int startLine = (int) lines.getLineNumber(start); + int startColumn = (int) lines.getColumnNumber(start); + Position startPos = new Position(startLine - 1, startColumn - 1); + int endLine = (int) lines.getLineNumber(end); + int endColumn = (int) lines.getColumnNumber(end); + Position endPos = new Position(endLine - 1, endColumn - 1); + Range delete = new Range(startPos, endPos); + TextEdit edit = new TextEdit(delete, ""); + TextEdit[] edits = {edit}; + return Collections.singletonMap (file, edits); + } + + static VariableTree findVariable(ParseTask task, int position) { + return new FindVariableAt (task.task).scan(task.root, position); + } + + /** https://docs.oracle.com/javase/specs/jls/se13/html/jls-14.html#jls-14.8 */ + static boolean isExpressionStatement(Tree t) { + if(t == null) return false; + switch (t.getKind()) { + case ASSIGNMENT: + case PREFIX_INCREMENT: + case PREFIX_DECREMENT: + case POSTFIX_INCREMENT: + case POSTFIX_DECREMENT: + case METHOD_INVOCATION: + case NEW_CLASS: + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/CreateMissingMethod.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/CreateMissingMethod.java new file mode 100644 index 000000000..d012b42da --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/CreateMissingMethod.java @@ -0,0 +1,241 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.androidide.utils.Logger; +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.visitors.FindMethodCallAt; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.StringJoiner; + +public class CreateMissingMethod implements Rewrite { + final Path file; + final int position; + + int argCount = -1; + + public CreateMissingMethod(Path file, int position) { + this.file = file; + this.position = position; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + try (CompileTask task = compiler.compile(file)) { + final Trees trees = Trees.instance(task.task); + final FindMethodCallAt methodFinder = new FindMethodCallAt (task.task); + final MethodInvocationTree call = methodFinder.scan(task.root(), position); + if (call == null) { + return CANCELLED; + } + + final TreePath path = trees.getPath(task.root(), call); + final String returnType = methodFinder.getReturnType(); + Path sourceFile = file; + MethodTree currentMethod = surroundingMethod(path); + String insertText = "\n"; + + insertText += printMethodHeader(task, call, returnType, methodFinder.isMemberSelect(), (currentMethod.getModifiers().getFlags().contains(Modifier.STATIC) || methodFinder.isStaticAccess())) + " {\n" + + " // TODO: Implement this method\n" + + " " + createReturnStatement(returnType) + "\n" + + "}"; + + TextEdit[] edits; + if(methodFinder.isMemberSelect()) { + // Accessing method from another class + final CompilationUnitTree compilationUnit = methodFinder.getEnclosingTreePath().getCompilationUnit(); + final ClassTree enclosingClass = methodFinder.getEnclosingClass(); + final int indent = EditHelper.indent(task.task, compilationUnit, enclosingClass) + 4; + insertText = insertText.replaceAll("\n", "\n" + EditHelper.repeatSpaces (indent)); + insertText = insertText + "\n"; + final Position insertPoint = EditHelper.insertAtEndOfClass(task.task, compilationUnit, enclosingClass); + edits = new TextEdit[]{new TextEdit(new Range (insertPoint, insertPoint), insertText)}; + sourceFile = Paths.get (compilationUnit.getSourceFile ().toUri ()); + } else { + ClassTree surroundingClass = surroundingClass(path); + int indent = EditHelper.indent(task.task, task.root(), surroundingClass) + 4; + insertText = insertText.replaceAll("\n", "\n" + EditHelper.repeatSpaces (indent)); + insertText = insertText + "\n"; + Position insertPoint = EditHelper.insertAfter(task.task, task.root(), surroundingMethod(path)); + edits = new TextEdit[]{new TextEdit(new Range(insertPoint, insertPoint), insertText)}; + } + + if(file != null) + return Collections.singletonMap (sourceFile, edits); + else return null; + } + } + + private String createReturnStatement(String returnType) { + if(returnType == null) return ""; + String value; + switch (returnType) { + case "int" : + case "byte" : + case "short" : + case "long" : + case "char" : + value = "0"; + break; + case "float" : + value = "0f"; + break; + case "double" : + value = "0.0"; + break; + case "boolean" : + value = "false"; + break; + + // Finding type of variable declaration may result in an error + // We should then simply return empty return type + case "(ERROR)" : + return ""; // Directly return empty string + default : + value = "null"; + break; + + } + return String.format("return %s;", value); + } + + private ClassTree surroundingClass(TreePath call) { + while (call != null) { + if (call.getLeaf() instanceof ClassTree) { + return (ClassTree) call.getLeaf(); + } + call = call.getParentPath(); + } + throw new RuntimeException("No surrounding class"); + } + + private MethodTree surroundingMethod(TreePath call) { + while (call != null) { + if (call.getLeaf() instanceof MethodTree) { + return (MethodTree) call.getLeaf(); + } + call = call.getParentPath(); + } + throw new RuntimeException("No surrounding method"); + } + + private String printMethodHeader(CompileTask task, MethodInvocationTree call, String type, boolean isMemeberSelect, boolean isStatic) { + String methodName = extractMethodName(call.getMethodSelect()); + String returnType = type == null || "(ERROR)".equals(type) ? "void" : type; + LOG.info("Creating missing method with return type: " + returnType); + if (returnType.equals(methodName)) { + returnType = "_"; + } + String parameters = printParameters(task, call); + String modifiers = isMemeberSelect ? "public" : "private"; + if(isStatic) + modifiers += " static"; + return modifiers + " " + returnType + " " + methodName + "(" + parameters + ")"; + } + + private String printParameters(CompileTask task, MethodInvocationTree call) { + Trees trees = Trees.instance(task.task); + StringJoiner join = new StringJoiner(", "); + for (int i = 0; i < call.getArguments().size(); i++) { + TypeMirror type = trees.getTypeMirror(trees.getPath(task.root(), call.getArguments().get(i))); + String name = guessParameterName(call.getArguments().get(i), type); + String argType = EditHelper.printType(type); + join.add(String.format("final %s %s", argType, name)); + } + return join.toString(); + } + + private String extractMethodName(ExpressionTree method) { + if (method instanceof IdentifierTree) { + IdentifierTree id = (IdentifierTree) method; + return id.getName().toString(); + } else if (method instanceof MemberSelectTree) { + MemberSelectTree select = (MemberSelectTree) method; + return select.getIdentifier().toString(); + } else { + return "extractedMethod"; + } + } + + private String guessParameterName(Tree argument, TypeMirror type) { + String fromTree = guessParameterNameFromTree(argument); + if (!fromTree.isEmpty()) { + return fromTree; + } + + String fromType = guessParameterNameFromType(type); + if (!fromType.isEmpty()) { + return fromType; + } + + argCount++; + return "param" + argCount; + } + + private String guessParameterNameFromTree(Tree argument) { + if (argument instanceof IdentifierTree) { + IdentifierTree id = (IdentifierTree) argument; + return id.getName().toString(); + } else if (argument instanceof MemberSelectTree) { + MemberSelectTree select = (MemberSelectTree) argument; + return select.getIdentifier().toString(); + } else if (argument instanceof MemberReferenceTree) { + MemberReferenceTree reference = (MemberReferenceTree) argument; + return reference.getName().toString(); + } else { + return ""; + } + } + + private String guessParameterNameFromType(TypeMirror type) { + if (type instanceof DeclaredType) { + DeclaredType declared = (DeclaredType) type; + Name name = declared.asElement().getSimpleName(); + return "" + Character.toLowerCase(name.charAt(0)) + name.subSequence(1, name.length()); + } else { + return ""; + } + } + + private static final Logger LOG = Logger.instance ("main"); +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateRecordConstructor.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateRecordConstructor.java new file mode 100644 index 000000000..abdcccd54 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateRecordConstructor.java @@ -0,0 +1,151 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.androidide.utils.Logger; +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; + +public class GenerateRecordConstructor implements Rewrite { + final String className; + + public GenerateRecordConstructor(String className) { + this.className = className; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + LOG.info("Generate default constructor for " + className + "..."); + // TODO this needs to fall back on looking for inner classes and package-private classes + Path file = compiler.findTypeDeclaration(className); + try (CompileTask task = compiler.compile(file)) { + TypeElement typeElement = task.task.getElements().getTypeElement(className); + ClassTree typeTree = Trees.instance(task.task).getTree(typeElement); + List fields = fieldsNeedingInitialization(typeTree); + String parameters = generateParameters(task, fields); + String initializers = generateInitializers(fields); + StringBuilder buf = new StringBuilder (); + buf.append("\n"); + if (typeTree.getModifiers().getFlags().contains(Modifier.PUBLIC)) { + buf.append("public "); + } + + buf.append(simpleName(className)) + .append("(") + .append(parameters) + .append(") {\n ") + .append(initializers) + .append("\n}"); + String string = buf.toString(); + int indent = EditHelper.indent(task.task, task.root(), typeTree) + 4; + string = string.replaceAll("\n", "\n" + EditHelper.repeatSpaces (indent)); + string = string + "\n\n"; + Position insert = insertPoint(task, typeTree); + TextEdit[] edits = {new TextEdit(new Range (insert, insert), string)}; + return Collections.singletonMap (file, edits); + } + } + + private List fieldsNeedingInitialization(ClassTree typeTree) { + List fields = new ArrayList<> (); + for (Tree member : typeTree.getMembers()) { + if (!(member instanceof VariableTree)) continue; + VariableTree field = (VariableTree) member; + if (field.getInitializer() != null) continue; + Set flags = field.getModifiers().getFlags(); + if (flags.contains(Modifier.STATIC)) continue; + if (!flags.contains(Modifier.FINAL)) continue; + fields.add(field); + } + + return fields; + } + + private String generateParameters(CompileTask task, List fields) { + StringJoiner join = new StringJoiner(", "); + for (VariableTree f : fields) { + join.add(extract(task, f.getType()) + " " + f.getName()); + } + return join.toString(); + } + + private String generateInitializers (List fields) { + StringJoiner join = new StringJoiner("\n "); + for (VariableTree f : fields) { + join.add("this." + f.getName() + " = " + f.getName() + ";"); + } + return join.toString(); + } + + private CharSequence extract(CompileTask task, Tree typeTree) { + try { + CharSequence contents = task.root().getSourceFile().getCharContent(true); + SourcePositions pos = Trees.instance(task.task).getSourcePositions(); + int start = (int) pos.getStartPosition(task.root(), typeTree); + int end = (int) pos.getEndPosition(task.root(), typeTree); + return contents.subSequence(start, end); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String simpleName(String className) { + int dot = className.lastIndexOf('.'); + if (dot != -1) { + return className.substring(dot + 1); + } + return className; + } + + private Position insertPoint(CompileTask task, ClassTree typeTree) { + for (Tree member : typeTree.getMembers()) { + if (member.getKind() == Tree.Kind.METHOD) { + MethodTree method = (MethodTree) member; + if (method.getReturnType() == null) continue; + LOG.info("...insert constructor before " + method.getName()); + return EditHelper.insertBefore(task.task, task.root(), method); + } + } + LOG.info("...insert constructor at end of class"); + return EditHelper.insertAtEndOfClass(task.task, task.root(), typeTree); + } + + private static final Logger LOG = Logger.instance ("main"); +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ImplementAbstractMethods.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ImplementAbstractMethods.java new file mode 100644 index 000000000..1c0d40ad7 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/ImplementAbstractMethods.java @@ -0,0 +1,111 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.androidide.utils.Logger; +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +public class ImplementAbstractMethods implements Rewrite { + final Path file; + final ClassTree tree; + final TreePath path; + + public ImplementAbstractMethods(final Path file, final ClassTree tree, final TreePath path) { + Objects.requireNonNull(file); + Objects.requireNonNull(tree); + Objects.requireNonNull(path); + + this.file = file; + this.tree = tree; + this.path = path; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + StringJoiner insertText = new StringJoiner ("\n"); + try (CompileTask task = compiler.compile(file)) { + Elements elements = task.task.getElements(); + Types types = task.task.getTypes(); + Trees trees = Trees.instance(task.task); + TypeElement thisClass = (TypeElement) trees.getElement(this.path); + DeclaredType thisType = (DeclaredType) thisClass.asType(); + ClassTree thisTree = trees.getTree(thisClass); + int indent = EditHelper.indent(task.task, task.root(), thisTree) + 4; + for (Element member : elements.getAllMembers(thisClass)) { + if (member.getKind() == ElementKind.METHOD && member.getModifiers().contains(Modifier.ABSTRACT)) { + ExecutableElement method = (ExecutableElement) member; + MethodTree source = findSource(compiler, task, method); + if (source == null) { + LOG.warn("...couldn't find source for " + method); + } + ExecutableType parameterizedType = (ExecutableType) types.asMemberOf(thisType, method); + String text = EditHelper.printMethod(method, parameterizedType, source); + text = text.replaceAll("\n", "\n" + EditHelper.repeatSpaces (indent)); + insertText.add(text); + } + } + Position insert = EditHelper.insertAtEndOfClass(task.task, task.root(), thisTree); + TextEdit[] edits = {new TextEdit(new Range (insert, insert), insertText + "\n")}; + return Collections.singletonMap (file, edits); + } + } + + private MethodTree findSource(CompilerProvider compiler, CompileTask task, ExecutableElement method) { + TypeElement superClass = (TypeElement) method.getEnclosingElement(); + String superClassName = superClass.getQualifiedName().toString(); + String methodName = method.getSimpleName().toString(); + String[] erasedParameterTypes = FindHelper.erasedParameterTypes(task, method); + Optional sourceFile = compiler.findAnywhere(superClassName); + if (!sourceFile.isPresent ()) { + return null; + } + ParseTask parse = compiler.parse(sourceFile.get()); + return FindHelper.findMethod(parse, superClassName, methodName, erasedParameterTypes); + } + + private static final Logger LOG = Logger.instance ("main"); +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/OverrideInheritedMethod.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/OverrideInheritedMethod.java new file mode 100644 index 000000000..68c63333d --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/OverrideInheritedMethod.java @@ -0,0 +1,113 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.java.visitors.FindTypeDeclarationAt; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +public class OverrideInheritedMethod implements Rewrite { + final String superClassName, methodName; + final String[] erasedParameterTypes; + final Path file; + final int insertPosition; + + public OverrideInheritedMethod( + String superClassName, String methodName, String[] erasedParameterTypes, Path file, int insertPosition) { + this.superClassName = superClassName; + this.methodName = methodName; + this.erasedParameterTypes = erasedParameterTypes; + this.file = file; + this.insertPosition = insertPosition; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + Position insertPoint = insertNearCursor(compiler); + String insertText = insertText(compiler); + TextEdit[] edits = {new TextEdit(new Range (insertPoint, insertPoint), insertText)}; + return Collections.singletonMap (file, edits); + } + + private String insertText(CompilerProvider compiler) { + try (final CompileTask task = compiler.compile(file)) { + final Types types = task.task.getTypes(); + final Trees trees = Trees.instance(task.task); + final ExecutableElement superMethod = FindHelper.findMethod(task, superClassName, methodName, erasedParameterTypes); + final ClassTree thisTree = new FindTypeDeclarationAt (task.task).scan(task.root(), (long) insertPosition); + final TreePath thisPath = trees.getPath(task.root(), thisTree); + final TypeElement thisClass = (TypeElement) trees.getElement(thisPath); + final ExecutableType parameterizedType = (ExecutableType) types.asMemberOf((DeclaredType) thisClass.asType(), superMethod); + final int indent = EditHelper.indent(task.task, task.root(), thisTree) + 4; + final Optional sourceFile = compiler.findAnywhere(superClassName); + if (!sourceFile.isPresent ()) return ""; + final ParseTask parse = compiler.parse(sourceFile.get()); + final MethodTree source = FindHelper.findMethod(parse, superClassName, methodName, erasedParameterTypes); + String text = EditHelper.printMethod(superMethod, parameterizedType, source); + text = text.replaceAll("\n", "\n" + EditHelper.repeatSpaces (indent)); + text = text + "\n\n"; + return text; + } + } + + private Position insertNearCursor(CompilerProvider compiler) { + final ParseTask task = compiler.parse(file); + final ClassTree parent = new FindTypeDeclarationAt(task.task).scan(task.root, (long) insertPosition); + final Position next = nextMember(task, parent); + if (next != Position.NONE) { + return next; + } + return EditHelper.insertAtEndOfClass(task.task, task.root, parent); + } + + private Position nextMember(ParseTask task, ClassTree parent) { + final SourcePositions pos = Trees.instance(task.task).getSourcePositions(); + for (Tree member : parent.getMembers()) { + long start = pos.getStartPosition(task.root, member); + if (start > insertPosition) { + int line = (int) task.root.getLineMap().getLineNumber(start); + return new Position(line - 1, 0); + } + } + return Position.NONE; + } +} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveClass.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveClass.java new file mode 100644 index 000000000..2e036968d --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveClass.java @@ -0,0 +1,47 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.visitors.FindTypeDeclarationAt; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.ClassTree; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +public class RemoveClass implements Rewrite { + final Path file; + final int position; + + public RemoveClass(Path file, int position) { + this.file = file; + this.position = position; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + ParseTask task = compiler.parse(file); + final ClassTree type = new FindTypeDeclarationAt (task.task).scan(task.root, (long) position); + TextEdit[] edits = {EditHelper.removeTree(task.task, task.root, type)}; + return Collections.singletonMap (file, edits); + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveException.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveException.java new file mode 100644 index 000000000..a53ec8a98 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveException.java @@ -0,0 +1,181 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.models.Position; +import com.itsaky.lsp.models.Range; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; + +public class RemoveException implements Rewrite { + final String className, methodName; + final String[] erasedParameterTypes; + final String exceptionType; + + public RemoveException(String className, String methodName, String[] erasedParameterTypes, String exceptionType) { + this.className = className; + this.methodName = methodName; + this.erasedParameterTypes = erasedParameterTypes; + this.exceptionType = exceptionType; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + Path file = compiler.findTypeDeclaration(className); + try (CompileTask task = compiler.compile(file)) { + ExecutableElement methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes); + MethodTree methodTree = Trees.instance(task.task).getTree(methodElement); + if (methodTree.getThrows().size() == 1) { + TextEdit delete = removeEntireThrows(task.task, task.root(), methodTree); + if (delete == TextEdit.NONE) { + return CANCELLED; + } + + TextEdit[] edits = {delete}; + return Collections.singletonMap (file, edits); + } + TextEdit[] edits = {removeSingleException(task.task, task.root(), methodTree)}; + return Collections.singletonMap (file, edits); + } + } + + private static final Pattern THROWS = Pattern.compile("\\s*\\bthrows\\b"); + + private TextEdit removeEntireThrows(JavacTask task, CompilationUnitTree root, MethodTree method) { + Trees trees = Trees.instance(task); + SourcePositions pos = trees.getSourcePositions(); + int startMethod = (int) pos.getStartPosition(root, method); + CharSequence contents; + try { + contents = root.getSourceFile().getCharContent(true); + } catch (IOException e) { + throw new RuntimeException(e); + } + + Matcher matcher = THROWS.matcher(contents); + if (!matcher.find(startMethod)) { + return TextEdit.NONE; + } + + LineMap lines = root.getLineMap(); + int start = matcher.start(); + int startLine = (int) lines.getLineNumber(start); + int startColumn = (int) lines.getColumnNumber(start); + Position startPos = new Position(startLine - 1, startColumn - 1); + ExpressionTree lastException = method.getThrows().get(method.getThrows().size() - 1); + int end = (int) pos.getEndPosition(root, lastException); + int endLine = (int) lines.getLineNumber(end); + int endColumn = (int) lines.getColumnNumber(end); + Position endPos = new Position(endLine - 1, endColumn - 1); + return new TextEdit(new Range (startPos, endPos), ""); + } + + private TextEdit removeSingleException(JavacTask task, CompilationUnitTree root, MethodTree method) { + int i = findNamedException(task, root, method); + if (i == -1) { + return TextEdit.NONE; + } + + Trees trees = Trees.instance(task); + SourcePositions pos = trees.getSourcePositions(); + ExpressionTree exn = method.getThrows().get(i); + long start = pos.getStartPosition(root, exn); + long end = pos.getEndPosition(root, exn); + if (i == 0) { + end = removeTrailingComma(root, end); + } else { + start = removeLeadingComma(root, start); + } + + LineMap lines = root.getLineMap(); + int startLine = (int) lines.getLineNumber(start); + int startColumn = (int) lines.getColumnNumber(start); + Position startPos = new Position(startLine - 1, startColumn - 1); + int endLine = (int) lines.getLineNumber(end); + int endColumn = (int) lines.getColumnNumber(end); + Position endPos = new Position(endLine - 1, endColumn - 1); + return new TextEdit(new Range(startPos, endPos), ""); + } + + private int findNamedException(JavacTask task, CompilationUnitTree root, MethodTree method) { + Trees trees = Trees.instance(task); + for (int i = 0; i < method.getThrows().size(); i++) { + ExpressionTree e = method.getThrows().get(i); + TreePath path = trees.getPath(root, e); + DeclaredType type = (DeclaredType) trees.getTypeMirror(path); + TypeElement el = (TypeElement) type.asElement(); + if (el.getQualifiedName().contentEquals(exceptionType)) { + return i; + } + } + return -1; + } + + private int removeLeadingComma(CompilationUnitTree root, long start) { + CharSequence contents = contents(root); + for (int i = (int) start; i > 0; i--) { + if (contents.charAt(i) == ',') { + return i; + } + } + return -1; + } + + private int removeTrailingComma(CompilationUnitTree root, long end) { + CharSequence contents = contents(root); + for (int i = (int) end; i < contents.length(); i++) { + if (contents.charAt(i) == ',') { + if (contents.charAt(i + 1) == ' ') { + return i + 2; + } else { + return i + 1; + } + } + } + return -1; + } + + private CharSequence contents(CompilationUnitTree root) { + try { + return root.getSourceFile().getCharContent(true); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveMethod.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveMethod.java new file mode 100644 index 000000000..d03e4b4c5 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RemoveMethod.java @@ -0,0 +1,56 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.lsp.java.rewrite; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.CompilerProvider; +import com.itsaky.lsp.java.utils.EditHelper; +import com.itsaky.lsp.java.utils.FindHelper; +import com.itsaky.lsp.models.TextEdit; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.Trees; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import javax.lang.model.element.ExecutableElement; + +public class RemoveMethod implements Rewrite { + final String className, methodName; + final String[] erasedParameterTypes; + + public RemoveMethod(String className, String methodName, String[] erasedParameterTypes) { + this.className = className; + this.methodName = methodName; + this.erasedParameterTypes = erasedParameterTypes; + } + + @Override + public Map rewrite(CompilerProvider compiler) { + Path file = compiler.findTypeDeclaration(className); + if (file == CompilerProvider.NOT_FOUND) { + return CANCELLED; + } + try (CompileTask task = compiler.compile(file)) { + ExecutableElement methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes); + MethodTree methodTree = Trees.instance(task.task).getTree(methodElement); + TextEdit[] edits = {EditHelper.removeTree(task.task, task.root(), methodTree)}; + return Collections.singletonMap (file, edits); + } + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java index 0b3224f38..d19181b40 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java @@ -22,14 +22,15 @@ import com.itsaky.lsp.models.Range; import com.itsaky.lsp.models.TextEdit; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.Map; public interface Rewrite { /** Perform a rewrite across the entire codebase. */ - TextEdit rewrite(CompilerProvider compiler); + Map rewrite(CompilerProvider compiler); /** CANCELLED signals that the rewrite couldn't be completed. */ - TextEdit CANCELLED = new TextEdit (Range.NONE, ""); + Map CANCELLED = Collections.emptyMap (); Rewrite NOT_SUPPORTED = new RewriteNotSupported(); } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RewriteNotSupported.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RewriteNotSupported.java index 89b6fa4e1..461e1f167 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RewriteNotSupported.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/RewriteNotSupported.java @@ -20,10 +20,13 @@ package com.itsaky.lsp.java.rewrite; import com.itsaky.lsp.java.CompilerProvider; import com.itsaky.lsp.models.TextEdit; +import java.nio.file.Path; +import java.util.Map; + class RewriteNotSupported implements Rewrite { @Override - public TextEdit rewrite(CompilerProvider compiler) { + public Map rewrite(CompilerProvider compiler) { return CANCELLED; } } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java index 55ee22300..d9a63dece 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java @@ -42,13 +42,8 @@ import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeMirror; public class EditHelper { - final JavacTask task; - public EditHelper(JavacTask task) { - this.task = task; - } - - TextEdit removeTree(final CompilationUnitTree root, final Tree remove) { + public static TextEdit removeTree(final JavacTask task, final CompilationUnitTree root, final Tree remove) { SourcePositions pos = Trees.instance(task).getSourcePositions(); LineMap lines = root.getLineMap(); long start = pos.getStartPosition(root, remove); @@ -166,4 +161,12 @@ public class EditHelper { int column = (int) lines.getColumnNumber(end); return new Position(line - 1, column - 2); } + + public static String repeatSpaces (int count) { + StringBuilder result = new StringBuilder (); + for (int i = 0; i < count; i++) { + result.append (" "); + } + return result.toString (); + } } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FindHelper.java b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/FindHelper.java similarity index 88% rename from lsp/java/src/main/java/com/itsaky/lsp/java/FindHelper.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/utils/FindHelper.java index cc973d1d8..8ab1cae3b 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FindHelper.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/FindHelper.java @@ -1,5 +1,25 @@ -package com.itsaky.lsp.java; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.lsp.java.utils; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.ParseTask; +import com.itsaky.lsp.java.visitors.FindTypeDeclarationNamed; import com.itsaky.lsp.models.Location; import com.itsaky.lsp.models.Position; import com.itsaky.lsp.models.Range; @@ -68,7 +88,7 @@ public class FindHelper { } public static ClassTree findType(ParseTask task, String className) { - return new FindTypeDeclarationNamed().scan(task.root, className); + return new FindTypeDeclarationNamed ().scan(task.root, className); } public static ExecutableElement findMethod( diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/ScopeHelper.java b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/ScopeHelper.java similarity index 75% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/ScopeHelper.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/utils/ScopeHelper.java index 646ca8a5a..dc59e01ed 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/ScopeHelper.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/ScopeHelper.java @@ -1,4 +1,21 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.utils; import com.itsaky.lsp.java.CompileTask; import com.sun.source.tree.Scope; @@ -14,9 +31,9 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.util.Elements; -class ScopeHelper { +public class ScopeHelper { // TODO is this still necessary? Test speed. We could get rid of the extra static-imports step. - static List fastScopes (Scope start) { + public static List fastScopes (Scope start) { List scopes = new ArrayList (); for (Scope s = start; s != null; s = s.getEnclosingScope ()) { scopes.add (s); @@ -29,7 +46,7 @@ class ScopeHelper { return scopes.subList (0, scopes.size () - 2); } - static List scopeMembers (CompileTask task, Scope inner, Predicate filter) { + public static List scopeMembers (CompileTask task, Scope inner, Predicate filter) { Trees trees = Trees.instance (task.task); Elements elements = task.task.getElements (); boolean isStatic = false; diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FIndVariableAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FIndVariableAt.java new file mode 100644 index 000000000..cf8fa6a8f --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FIndVariableAt.java @@ -0,0 +1,58 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; + +public class FindVariableAt extends TreeScanner { + private final SourcePositions pos; + private CompilationUnitTree root; + + public FindVariableAt(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } + + @Override + public VariableTree visitCompilationUnit(CompilationUnitTree t, Integer find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + @Override + public VariableTree visitVariable(VariableTree t, Integer find) { + VariableTree smaller = super.visitVariable(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + return t; + } + return null; + } + + @Override + public VariableTree reduce(VariableTree r1, VariableTree r2) { + if (r1 != null) return r1; + return r2; + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindCompletionsAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindCompletionsAt.java similarity index 80% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindCompletionsAt.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindCompletionsAt.java index 836117f4b..01bde5217 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindCompletionsAt.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindCompletionsAt.java @@ -1,4 +1,21 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; import com.sun.source.tree.CaseTree; import com.sun.source.tree.CompilationUnitTree; @@ -14,11 +31,11 @@ import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; -class FindCompletionsAt extends TreePathScanner { +public class FindCompletionsAt extends TreePathScanner { private final JavacTask task; private CompilationUnitTree root; - FindCompletionsAt (JavacTask task) { + public FindCompletionsAt (JavacTask task) { this.task = task; } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindInvocationAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindInvocationAt.java similarity index 67% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindInvocationAt.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindInvocationAt.java index 1396e63ee..85b7fd4ba 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/FindInvocationAt.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindInvocationAt.java @@ -1,4 +1,21 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.MethodInvocationTree; @@ -9,12 +26,12 @@ import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; -class FindInvocationAt extends TreePathScanner { +public class FindInvocationAt extends TreePathScanner { private final JavacTask task; private CompilationUnitTree root; - FindInvocationAt (JavacTask task) { + public FindInvocationAt (JavacTask task) { this.task = task; } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodCallAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodCallAt.java new file mode 100644 index 000000000..3d9428e60 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodCallAt.java @@ -0,0 +1,247 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; + +import com.itsaky.androidide.utils.Logger; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; + +import javax.lang.model.element.Element; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +public class FindMethodCallAt extends TreePathScanner { + private final Trees trees; + private final SourcePositions pos; + private CompilationUnitTree root; + + private boolean isMemberSelect; + private boolean isStatic; + + private String returnType; + private ClassTree enclosingTree; + private TreePath enclosingTreePath; + private TypeElement declaredInTopLevel; + private TypeElement enclosingElement; + + public FindMethodCallAt(JavacTask task) { + this.trees = Trees.instance(task); + this.pos = trees.getSourcePositions(); + } + + public String getReturnType(){ + return returnType; + } + + public TypeElement getDeclaringType() { + return declaredInTopLevel; + } + + public TypeElement getEnclosingElement() { + return enclosingElement; + } + + public ClassTree getEnclosingClass() { + return enclosingTree; + } + + public TreePath getEnclosingTreePath() { + return enclosingTreePath; + } + + public boolean isMemberSelect() { + return isMemberSelect; + } + + public boolean isStaticAccess() { + return isStatic; + } + + @Override + public MethodInvocationTree visitCompilationUnit(CompilationUnitTree t, Integer find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + /** + * A method invocation + *
+     * 	foo();
+     * 	field.foo();
+     * 	Class.foo();
+     * 	Class.field.foo();
+     * 
+ */ + @Override + public MethodInvocationTree visitMethodInvocation(MethodInvocationTree t, Integer find) { + MethodInvocationTree smaller = super.visitMethodInvocation(t, find); + if (smaller != null) { + return smaller; + } + + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + if(t.getMethodSelect() instanceof MemberSelectTree) { + checkForQualifiedName((MemberSelectTree) t.getMethodSelect(), find); + } + return t; + } + return null; + } + + + /** + * Find method invocation while declaring a variable + * E.g.
 int x = foo(); 
+ */ + @Override + public MethodInvocationTree visitVariable(VariableTree tree, Integer find) { + if(tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if(start <= find && find <= end && tree.getInitializer() instanceof MethodInvocationTree) { + returnType = tree.getType().toString(); + return visitMethodInvocation((MethodInvocationTree) tree.getInitializer(), find); + } + } + return super.visitVariable(tree, find); + } + + /** + * Find method invocation while initializing a variable + * E.g. + *
+     * 	int x;
+     * 	...
+     * 	x = foo();
+     * 
+ */ + @Override + public MethodInvocationTree visitAssignment(AssignmentTree tree, Integer find) { + if(tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if(start <= find && find <= end && tree.getExpression() instanceof MethodInvocationTree) { + returnType = findType(); + return visitMethodInvocation((MethodInvocationTree) tree.getExpression(), find); + } + } + return super.visitAssignment(tree, find); + + } + + private String findType() { + if(this.trees != null && getCurrentPath() != null) { + TypeMirror typeMirror = this.trees.getTypeMirror(getCurrentPath()); + if(typeMirror != null + && typeMirror.getKind() != TypeKind.NONE + && typeMirror.getKind() != TypeKind.ERROR) + return typeMirror.toString(); + } + return null; + } + + @Override + public MethodInvocationTree reduce(MethodInvocationTree r1, MethodInvocationTree r2) { + if (r1 != null) return r1; + return r2; + } + + private void checkForQualifiedName(MemberSelectTree tree, Integer find) { + if(tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if(start <= find && find <= end) { + Element element = trees.getElement(trees.getPath(root, tree)); + + // Is this a static access? + isStatic = element instanceof TypeElement; + + // Find enclosing element to get the TypeElement from which this method is being called + this.enclosingElement = enclosingType(element); + + // find top level declaration to get the qualified name of the class + this.declaredInTopLevel = enclosingTopLevelType(enclosingElement); + + // Get the tree path of the enclosing element + this.enclosingTreePath = trees.getPath(enclosingElement); + + // Get the ClassTree of the enclosing element + // Will be needed in CreateMissingMethod.java for EditHelper + this.enclosingTree = enclosingClass(this.enclosingTreePath); + + isMemberSelect = enclosingElement != null + && declaredInTopLevel != null + && enclosingTreePath != null + && enclosingTree != null; + + LOG.info(String.format("checkForQualifiedName\nisStatic: %s\nenclosingElement: %s\ndeclaredInTopLevel: %s\nenclosingTreePath: %s\nenclosingTree: %s\nisMemberSelect: %s", "" + isStatic, "" + enclosingElement, "" + declaredInTopLevel, "" + enclosingTreePath, "" + enclosingTree, "" + isMemberSelect)); + } + } + } + + private ClassTree enclosingClass(TreePath path) { + while(path != null) { + if(path.getLeaf() instanceof ClassTree) { + return (ClassTree) path.getLeaf(); + } + + path = path.getParentPath(); + } + return null; + } + + private TypeElement enclosingTopLevelType (Element element) { + while (element != null) { + if(element instanceof TypeElement) { + TypeElement type = (TypeElement) element; + if(type.getNestingKind() == NestingKind.TOP_LEVEL) + return type; + } + + element = element.getEnclosingElement(); + } + return null; + } + + private TypeElement enclosingType (Element element) { + while (element != null) { + if(element instanceof TypeElement) { + TypeElement type = (TypeElement) element; + final ClassTree tree = trees.getTree (type); + if(tree != null) + return type; + } + + element = element.getEnclosingElement(); + } + return null; + } + + private static final Logger LOG = Logger.instance ("main"); +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodDeclarationAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodDeclarationAt.java new file mode 100644 index 000000000..b0b5541ed --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindMethodDeclarationAt.java @@ -0,0 +1,59 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; + +public class FindMethodDeclarationAt extends TreeScanner { + + private final SourcePositions pos; + private CompilationUnitTree root; + + public FindMethodDeclarationAt(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } + + @Override + public MethodTree visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + @Override + public MethodTree visitMethod(MethodTree t, Long find) { + MethodTree smaller = super.visitMethod(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + return t; + } + return null; + } + + @Override + public MethodTree reduce(MethodTree r1, MethodTree r2) { + if (r1 != null) return r1; + return r2; + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FindNameAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindNameAt.java similarity index 79% rename from lsp/java/src/main/java/com/itsaky/lsp/java/FindNameAt.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindNameAt.java index fdda288b8..d2e1328d5 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FindNameAt.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindNameAt.java @@ -1,5 +1,24 @@ -package com.itsaky.lsp.java; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.lsp.java.visitors; + +import com.itsaky.lsp.java.CompileTask; +import com.itsaky.lsp.java.utils.FindHelper; import com.sun.source.tree.*; import com.sun.source.util.*; diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationAt.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationAt.java new file mode 100644 index 000000000..09fe23259 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationAt.java @@ -0,0 +1,66 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; + +public class FindTypeDeclarationAt extends TreePathScanner { + private final SourcePositions pos; + private CompilationUnitTree root; + + private TreePath stored; + + public FindTypeDeclarationAt(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } + + @Override + public ClassTree visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + @Override + public ClassTree visitClass(ClassTree t, Long find) { + ClassTree smaller = super.visitClass(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + this.stored = getCurrentPath(); + return t; + } + return null; + } + + @Override + public ClassTree reduce(ClassTree a, ClassTree b) { + if (a != null) return a; + return b; + } + + public TreePath getStoredTreePath() { + return stored; + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationNamed.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationNamed.java similarity index 53% rename from lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationNamed.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationNamed.java index e66da083b..9234c24c9 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/FindTypeDeclarationNamed.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarationNamed.java @@ -1,4 +1,21 @@ -package com.itsaky.lsp.java; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; import com.sun.source.tree.*; import com.sun.source.util.TreeScanner; @@ -6,7 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -class FindTypeDeclarationNamed extends TreeScanner { +public class FindTypeDeclarationNamed extends TreeScanner { private List qualifiedName = new ArrayList<>(); @Override diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarations.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarations.java new file mode 100644 index 000000000..db4fd0500 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/FindTypeDeclarations.java @@ -0,0 +1,44 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; + +import com.sun.source.tree.*; +import com.sun.source.util.TreeScanner; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class FindTypeDeclarations extends TreeScanner> { + private List qualifiedName = new ArrayList<>(); + + @Override + public Void visitCompilationUnit(CompilationUnitTree root, List found) { + String name = Objects.toString(root.getPackageName(), ""); + qualifiedName.add(name); + return super.visitCompilationUnit(root, found); + } + + @Override + public Void visitClass(ClassTree type, List found) { + qualifiedName.add(type.getSimpleName()); + found.add(String.join(".", qualifiedName)); + super.visitClass(type, found); + qualifiedName.remove(qualifiedName.size() - 1); + return null; + } +} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/PruneMethodBodies.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/PruneMethodBodies.java similarity index 71% rename from lsp/java/src/main/java/com/itsaky/lsp/java/completion/PruneMethodBodies.java rename to lsp/java/src/main/java/com/itsaky/lsp/java/visitors/PruneMethodBodies.java index f13a65ce9..ab62c93d1 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/completion/PruneMethodBodies.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/PruneMethodBodies.java @@ -1,4 +1,21 @@ -package com.itsaky.lsp.java.completion; +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.lsp.java.visitors; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.MethodTree;