Implemented CodeActionProvider

This commit is contained in:
Akash Yadav
2022-01-17 09:19:28 +05:30
parent 118c6d9377
commit 39feaea3a6
40 changed files with 2501 additions and 264 deletions
@@ -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<DiagnosticItem>)
data class CodeActionParams(var file: Path, var range: Range, var diagnostics: List<DiagnosticItem>)
data class CodeActionResult (var actions: List <CodeActionItem>) {
constructor() : this (ArrayList())
@@ -27,8 +29,24 @@ data class CodeActionResult (var actions: List <CodeActionItem>) {
data class CodeActionItem (
var title: String,
var changes: List<DocumentChange>)
var changes: List<DocumentChange>,
var kind: CodeActionKind
) {
constructor() : this ("", Collections.emptyList(), CodeActionKind.None)
}
data class DocumentChange (var file: Path, var edits: List<TextEdit>)
enum class CodeActionKind {
QuickFix,
None
}
data class TextEdit (var range: Range, var newText: String)
data class DocumentChange (var file: Path?, var edits: List<TextEdit>) {
constructor() : this (null, Collections.emptyList())
}
data class TextEdit (var range: Range, var newText: String) {
companion object {
@JvmField val NONE: TextEdit = TextEdit(Range.NONE, "");
}
}
@@ -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)
}
}
@@ -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<K, V> {
public class Cache<K, V> {
private static class Key<K> {
final Path file;
final K key;
@@ -41,11 +41,11 @@ class Cache<K, V> {
private final Map<Key, Value> 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<K>(file, k);
if (!map.containsKey(key)) return true;
@@ -57,14 +57,14 @@ class Cache<K, V> {
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<K>(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<K>(file, k);
if (!map.containsKey(key)) {
throw new IllegalArgumentException(k + " is not in map " + map);
@@ -58,7 +58,7 @@ public class FileStore {
}
}
static void setWorkspaceRoots (Set<Path> newRoots) {
public static void setWorkspaceRoots (Set<Path> 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<Path> {
public static class FindJavaSources extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (attrs.isSymbolicLink()) {
@@ -100,11 +100,11 @@ public class FileStore {
}
}
static Collection<Path> all () {
public static Collection<Path> all () {
return javaSources.keySet ();
}
static List<Path> list (String packageName) {
public static List<Path> list (String packageName) {
List<Path> 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<Path> activeDocuments () {
public static Set<Path> 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<Path> findDeclaringFile (@NonNull TypeElement el) {
public static Optional<Path> findDeclaringFile (@NonNull TypeElement el) {
String qualifiedName = el.getQualifiedName ().toString ();
String packageName = StringSearch.mostName (qualifiedName);
String className = StringSearch.lastName (qualifiedName);
@@ -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<ClassTree, Long> {
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;
}
}
@@ -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<Void, List<String>> {
private List<CharSequence> qualifiedName = new ArrayList<>();
@Override
public Void visitCompilationUnit(CompilationUnitTree root, List<String> found) {
String name = Objects.toString(root.getPackageName(), "");
qualifiedName.add(name);
return super.visitCompilationUnit(root, found);
}
@Override
public Void visitClass(ClassTree type, List<String> found) {
qualifiedName.add(type.getSimpleName());
found.add(String.join(".", qualifiedName));
super.visitClass(type, found);
qualifiedName.remove(qualifiedName.size() - 1);
return null;
}
}
@@ -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;
@@ -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;
@@ -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();
}
@@ -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.</b>
*/
class ReusableCompiler {
private static final Logger LOG = Logger.instance ("main");
private static final JavacTool systemProvider = JavacTool.create();
private List<String> currentOptions = new ArrayList<>();
private static final JavacTool systemProvider = JavacTool.create ();
private List<String> 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<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> 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<String> 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<String> arguments;
ReusableContext(List<String> arguments) {
super();
ReusableContext (List<String> 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
}
<T> void drop(Key<T> k) {
ht.remove(k);
<T> void drop (Key<T> k) {
ht.remove (k);
}
<T> void drop(Class<T> c) {
ht.remove(key(c));
<T> void drop (Class<T> 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<JavaCompiler> 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<Log> 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<JavaFileObject>() {
new DiagnosticListener<JavaFileObject> () {
DiagnosticListener<JavaFileObject> cachedListener;
@Override
@DefinedBy(Api.COMPILER)
@SuppressWarnings("unchecked")
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
public void report (Diagnostic<? extends JavaFileObject> diagnostic) {
if (cachedListener == null) {
cachedListener = context.get(DiagnosticListener.class);
cachedListener = context.get (DiagnosticListener.class);
}
cachedListener.report(diagnostic);
cachedListener.report (diagnostic);
}
};
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Rewrite> 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<CodeActionItem> 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<String, Rewrite> 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<String, Rewrite> 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<CodeActionItem> 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<CodeActionItem> actions = new ArrayList<>();
for (DiagnosticItem d : params.getDiagnostics()) {
List<CodeActionItem> 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<CodeActionItem> 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<CodeActionItem> 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("<init>") && !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<CodeActionItem> createQuickFix (String title, Rewrite rewrite) {
final Map<Path, TextEdit[]> 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");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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();
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ExecutableElement> overloads = methodOverloads(task, invoke);
List<SignatureInformation> signatures = new ArrayList<SignatureInformation>();
List<SignatureInformation> 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<ExecutableElement> overloads = constructorOverloads(task, invoke);
List<SignatureInformation> signatures = new ArrayList<SignatureInformation>();
List<SignatureInformation> 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<ExecutableElement> list = new ArrayList<ExecutableElement>();
List<ExecutableElement> list = new ArrayList<> ();
Predicate<CharSequence> 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<ExecutableElement> list = new ArrayList<ExecutableElement>();
List<ExecutableElement> 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<ExecutableElement> list = new ArrayList<ExecutableElement>();
List<ExecutableElement> 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<ParameterInformation> parameters(@NonNull ExecutableElement method) {
List<ParameterInformation> list = new ArrayList<ParameterInformation>();
List<ParameterInformation> list = new ArrayList<> ();
for (VariableElement p : method.getParameters()) {
list.add(parameter(p));
}
@@ -249,7 +264,7 @@ public class SignatureProvider implements ISignatureHelpProvider {
@NonNull
private List<ParameterInformation> parametersFromSource(MethodTree source) {
List<ParameterInformation> list = new ArrayList<ParameterInformation>();
List<ParameterInformation> list = new ArrayList<> ();
for (VariableTree p : source.getParameters()) {
ParameterInformation info = new ParameterInformation ();
info.setLabel (p.getType () + " " + p.getName ());
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
}
@@ -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<Path, TextEdit[]> 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) {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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;
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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<VariableTree> 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<VariableTree> fieldsNeedingInitialization(ClassTree typeTree) {
List<VariableTree> fields = new ArrayList<> ();
for (Tree member : typeTree.getMembers()) {
if (!(member instanceof VariableTree)) continue;
VariableTree field = (VariableTree) member;
if (field.getInitializer() != null) continue;
Set<Modifier> 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<VariableTree> fields) {
StringJoiner join = new StringJoiner(", ");
for (VariableTree f : fields) {
join.add(extract(task, f.getType()) + " " + f.getName());
}
return join.toString();
}
private String generateInitializers (List<VariableTree> 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");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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<JavaFileObject> 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");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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<JavaFileObject> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Path, TextEdit[]> 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);
}
}
}
@@ -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<Path, TextEdit[]> rewrite(CompilerProvider compiler);
/** CANCELLED signals that the rewrite couldn't be completed. */
TextEdit CANCELLED = new TextEdit (Range.NONE, "");
Map<Path, TextEdit[]> CANCELLED = Collections.emptyMap ();
Rewrite NOT_SUPPORTED = new RewriteNotSupported();
}
@@ -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<Path, TextEdit[]> rewrite(CompilerProvider compiler) {
return CANCELLED;
}
}
@@ -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 ();
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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(
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Scope> fastScopes (Scope start) {
public static List<Scope> fastScopes (Scope start) {
List<Scope> scopes = new ArrayList<Scope> ();
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<Element> scopeMembers (CompileTask task, Scope inner, Predicate<CharSequence> filter) {
public static List<Element> scopeMembers (CompileTask task, Scope inner, Predicate<CharSequence> filter) {
Trees trees = Trees.instance (task.task);
Elements elements = task.task.getElements ();
boolean isStatic = false;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<VariableTree, Integer> {
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreePath, Long> {
public class FindCompletionsAt extends TreePathScanner<TreePath, Long> {
private final JavacTask task;
private CompilationUnitTree root;
FindCompletionsAt (JavacTask task) {
public FindCompletionsAt (JavacTask task) {
this.task = task;
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreePath, Long> {
public class FindInvocationAt extends TreePathScanner<TreePath, Long> {
private final JavacTask task;
private CompilationUnitTree root;
FindInvocationAt (JavacTask task) {
public FindInvocationAt (JavacTask task) {
this.task = task;
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<MethodInvocationTree, Integer> {
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
* <pre>
* foo();
* field.foo();
* Class.foo();
* Class.field.foo();
* </pre>
*/
@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. <pre> int x = foo(); </pre>
*/
@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.
* <pre>
* int x;
* ...
* x = foo();
* </pre>
*/
@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");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<MethodTree, Long> {
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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.*;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ClassTree, Long> {
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ClassTree, String> {
public class FindTypeDeclarationNamed extends TreeScanner<ClassTree, String> {
private List<CharSequence> qualifiedName = new ArrayList<>();
@Override
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Void, List<String>> {
private List<CharSequence> qualifiedName = new ArrayList<>();
@Override
public Void visitCompilationUnit(CompilationUnitTree root, List<String> found) {
String name = Objects.toString(root.getPackageName(), "");
qualifiedName.add(name);
return super.visitCompilationUnit(root, found);
}
@Override
public Void visitClass(ClassTree type, List<String> found) {
qualifiedName.add(type.getSimpleName());
found.add(String.join(".", qualifiedName));
super.visitClass(type, found);
qualifiedName.remove(qualifiedName.size() - 1);
return null;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.java.visitors;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;