Added action to override multiple methods at once

This commit is contained in:
Akash Yadav
2022-04-14 20:37:03 +05:30
parent 173b0d1130
commit d94993740a
10 changed files with 427 additions and 67 deletions
+2
View File
@@ -4,6 +4,8 @@
<option name="SPACE_BEFORE_OPENING_ANGLE_BRACKET_IN_TYPE_PARAMETER" value="true" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
@@ -139,7 +139,7 @@ public class CursorCodeActionProvider {
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);
@@ -152,6 +152,13 @@ class GenerateSettersAndGettersAction : BaseCodeAction() {
->
if (error != null) {
log.error("Unable to generate setters and getters", error)
ThreadUtils.runOnUiThread {
BaseApplication.getBaseInstance()
.toast(
data[Context::class.java]!!.getString(
R.string.msg_cannot_generate_setters_getters),
Toaster.Type.ERROR)
}
return@whenComplete
}
}
@@ -201,7 +208,7 @@ class GenerateSettersAndGettersAction : BaseCodeAction() {
log.debug("Creating setters/getters for fields", fields.map { it.name })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.storedPath, it) })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
}
}
@@ -35,6 +35,7 @@ class JavaCodeActionsMenu : CodeActionsMenu() {
addAction(SuppressUncheckedWarningAction())
addAction(AddThrowsAction())
addAction(GenerateSettersAndGettersAction())
addAction(OverrideSuperclassMethodsAction())
addAction(GenerateConstructorAction())
}
}
@@ -0,0 +1,277 @@
/*
* 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.actions
import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.utils.Logger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.java.R
import com.itsaky.lsp.java.rewrite.AddImport
import com.itsaky.lsp.java.utils.EditHelper
import com.itsaky.lsp.java.utils.FindHelper
import com.itsaky.lsp.java.utils.JavaPoetUtils
import com.itsaky.lsp.java.utils.MethodPtr
import com.itsaky.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.lsp.models.Position
import com.itsaky.lsp.models.Range
import com.itsaky.toaster.Toaster
import com.sun.source.util.Trees
import io.github.rosemoe.sora.widget.CodeEditor
import java.util.*
import java.util.concurrent.CompletableFuture
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
/**
* Allows the user to override multiple methods from superclass at once.
*
* @author Akash Yadav
*/
class OverrideSuperclassMethodsAction : BaseCodeAction() {
override val titleTextRes: Int = R.string.action_override_superclass_methods
override val id: String = "lsp_java_overrideSuperclassMethods"
override var label: String = "Override superclass methods"
private val log = Logger.newInstance(javaClass.simpleName)
private var position: Long = -1
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, Range::class.java, CodeEditor::class.java)) {
markInvisible()
return
}
visible = true
enabled = true
}
override fun execAction(data: ActionData): Any {
val range = data[Range::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
// 1-based line and column index
val startLine = range.start.line + 1
val startColumn = range.start.column + 1
val endLine = range.end.line + 1
val endColumn = range.end.column + 1
val lines = task.root().lineMap
val start = lines.getPosition(startLine.toLong(), startColumn.toLong())
val end = lines.getPosition(endLine.toLong(), endColumn.toLong())
if (start == (-1).toLong() || end == (-1).toLong()) {
return@get false
}
this.position = start
val typeFinder = FindTypeDeclarationAt(task.task)
var type = typeFinder.scan(task.root(file), start)
if (type == null) {
type = typeFinder.scan(task.root(file), end)
position = end
}
if (type == null) {
return@get false
}
val overridable = mutableListOf<MethodPtr>()
val trees = Trees.instance(task.task)
val elements = task.task.elements
val classPath = typeFinder.path
val element = trees.getElement(classPath) as TypeElement
for (member in elements.getAllMembers(element)) {
if (member.modifiers.contains(Modifier.FINAL) ||
member.kind != ElementKind.METHOD) {
continue
}
val method = member as ExecutableElement
val methodSource = member.getEnclosingElement() as TypeElement
if (methodSource.qualifiedName.contentEquals("java.lang.Object") ||
methodSource == element) {
continue
}
val pointer = MethodPtr(task.task, method)
overridable.add(pointer)
}
return@get overridable
}
}
@Suppress("UNCHECKED_CAST")
override fun postExec(data: ActionData, result: Any) {
if (result !is List<*> || result.isEmpty() || position < 0) {
log.warn("Unable to find any overridable method")
BaseApplication.getBaseInstance()
.toast(
data[Context::class.java]!!.getString(R.string.msg_no_overridable_methods),
Toaster.Type.ERROR)
return
}
val methods = (result as List<MethodPtr>).sortedBy { it.methodName }
val checkedMethods = mutableListOf<MethodPtr>()
val names = mapMethodPointers(methods)
val builder = newDialogBuilder(data)
builder.setTitle(data[Context::class.java]!!.getString(R.string.msg_select_methods))
builder.setMultiChoiceItems(names, BooleanArray(names.size)) { _, which, isChecked ->
checkedMethods.apply {
if (isChecked) {
add(methods[which])
} else {
remove(methods[which])
}
}
}
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
if (checkedMethods.isEmpty()) {
BaseApplication.getBaseInstance()
.toast(
data[Context::class.java]!!.getString(R.string.msg_no_methods_selected),
Toaster.Type.ERROR)
return@setPositiveButton
}
CompletableFuture.runAsync { overrideMethods(data, checkedMethods) }.whenComplete {
_,
error,
->
if (error != null) {
log.error("An error occurred overriding methods")
ThreadUtils.runOnUiThread {
BaseApplication.getBaseInstance()
.toast(
data[Context::class.java]!!.getString(
R.string.msg_cannot_override_methods),
Toaster.Type.ERROR)
}
}
}
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
}
private fun overrideMethods(data: ActionData, checkedMethods: MutableList<MethodPtr>) {
val server = data[JavaLanguageServer::class.java]!!
val file = requirePath(data)
server.compiler.compile(file).run { task ->
val types = task.task.types
val trees = Trees.instance(task.task)
val sb = StringBuilder()
val imports = mutableSetOf<String>()
val typeFinder = FindTypeDeclarationAt(task.task)
val classTree = typeFinder.scan(task.root(), position)
val thisClass = trees.getElement(typeFinder.path) as TypeElement
val indent = EditHelper.indent(task.task, task.root(), classTree) + 4
for (pointer in checkedMethods) {
val superMethod =
FindHelper.findMethod(
task, pointer.className, pointer.methodName, pointer.erasedParameterTypes)
val thisDeclaredType = thisClass.asType() as DeclaredType
val spec = JavaPoetUtils.buildMethod(superMethod, types, thisDeclaredType).build()
sb.append("\n")
sb.append(JavaPoetUtils.print(spec, imports, false))
sb.replace(Regex(Regex.escape("\n")), "\n${EditHelper.repeatSpaces(indent)}")
sb.append("\n")
}
ThreadUtils.runOnUiThread {
performEdits(
data,
sb,
imports,
EditHelper.insertAtEndOfClass(task.task, task.root(file), classTree))
}
}
}
private fun performEdits(
data: ActionData,
sb: StringBuilder,
imports: MutableSet<String>,
position: Position
) {
val server = data[JavaLanguageServer::class.java]!!
val editor = data[CodeEditor::class.java]!!
val file = requirePath(data)
val text = editor.text
text.beginBatchEdit()
text.insert(position.line, position.column, sb)
for (name in imports) {
val rewrite = AddImport(file, name)
val edits = rewrite.rewrite(server.compiler)[file]
if (edits == null || edits.isEmpty()) {
continue
}
for (edit in edits) {
if (edit.range.start == edit.range.end) {
text.insert(edit.range.start.line, edit.range.start.column, edit.newText)
} else {
text.replace(
edit.range.start.line,
edit.range.start.column,
edit.range.end.line,
edit.range.end.column,
edit.newText)
}
}
}
text.endBatchEdit()
editor.formatCodeAsync()
}
private fun mapMethodPointers(methods: List<MethodPtr>): Array<CharSequence> {
return methods
.map { Arrays.toString(it.simplifiedErasedParameterTypes) }
.map {
val arr = it.toCharArray()
arr[0] = '('
arr[arr.size - 1] = ')'
String(arr)
}
.mapIndexed { index, params -> "${methods[index].methodName}$params" }
.toTypedArray()
}
}
@@ -19,6 +19,7 @@ package com.itsaky.lsp.java.providers;
import static com.itsaky.lsp.java.utils.EditHelper.addImportIfNeeded;
import static com.itsaky.lsp.java.utils.EditHelper.repeatSpaces;
import static com.itsaky.lsp.java.utils.JavaPoetUtils.buildMethod;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -46,7 +47,6 @@ import com.itsaky.lsp.models.CompletionItemKind;
import com.itsaky.lsp.models.CompletionParams;
import com.itsaky.lsp.models.CompletionResult;
import com.itsaky.lsp.models.InsertTextFormat;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.MethodSpec;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
@@ -76,7 +76,6 @@ import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -86,8 +85,6 @@ import javax.lang.model.element.TypeElement;
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.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Types;
@@ -886,32 +883,7 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
// Print the method details and the annotations
final int indent = EditHelper.indent(FileStore.contents(completingFile), (int) this.cursor);
final MethodSpec.Builder builder = MethodSpec.overriding(method, type, types);
final List<? extends AnnotationMirror> mirrors = method.getAnnotationMirrors();
if (mirrors != null && !mirrors.isEmpty()) {
for (final AnnotationMirror mirror : mirrors) {
if (mirror.getAnnotationType().getKind() != TypeKind.NULL) {
builder.addAnnotation(AnnotationSpec.get(mirror));
}
}
}
boolean addComment = true;
// Add super call if the method is not abstract
if (!method.getModifiers().contains(Modifier.ABSTRACT)) {
if (method.getReturnType() instanceof NoType) {
builder.addStatement(createSuperCall(builder));
} else {
addComment = false;
builder.addComment("TODO: Implement this method");
builder.addStatement("return " + createSuperCall(builder));
}
}
if (addComment) {
builder.addComment("TODO: Implement this method");
}
final MethodSpec.Builder builder = buildMethod(method, types, type);
final Set<String> imports = new HashSet<>();
final MethodSpec methodSpec = builder.build();
String insertText = JavaPoetUtils.print(methodSpec, imports, false);
@@ -946,28 +918,6 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
return item;
}
/**
* Create a superclass method invocation statement.
*
* @param builder The method builder.
* @return The super invocation statement string without ending ';'.
*/
@NonNull
private String createSuperCall(@NonNull MethodSpec.Builder builder) {
final StringBuilder sb = new StringBuilder();
sb.append("super.");
sb.append(builder.name);
sb.append("(");
for (int i = 0; i < builder.parameters.size(); i++) {
sb.append(builder.parameters.get(i).name);
if (i != builder.parameters.size() - 1) {
sb.append(", ");
}
}
sb.append(")");
return sb.toString();
}
@Nullable
private CompletionData data(CompileTask task, Element element, int overloads) {
CompletionData data = new CompletionData();
@@ -17,29 +17,88 @@
package com.itsaky.lsp.java.utils
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ImportCollectingCodeWriter
import com.squareup.javapoet.MethodSpec
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.NoType
import javax.lang.model.type.TypeKind
import javax.lang.model.util.Types
/**
* @author Akash Yadav
*/
/** @author Akash Yadav */
class JavaPoetUtils {
companion object {
@JvmStatic
fun print(method: MethodSpec, importsOut: MutableSet<String>, qualifiedNames: Boolean = true): String {
fun print(
method: MethodSpec,
importsOut: MutableSet<String>,
qualifiedNames: Boolean = true,
): String {
val sb = StringBuilder()
val writer = ImportCollectingCodeWriter(sb)
writer.setPrintQualifiedNames(qualifiedNames)
writer.isPrintQualifiedNames = qualifiedNames
writer.emit(method)
importsOut.addAll(writer.importClasses)
return sb.toString()
}
@JvmStatic
fun print(build: MethodSpec, imports: MutableSet<String>): String {
return print(build, imports, true)
}
@JvmStatic
fun buildMethod(method: ExecutableElement, types: Types, type: DeclaredType): MethodSpec.Builder {
val builder = MethodSpec.overriding(method, type, types)
val mirrors = method.annotationMirrors
if (mirrors != null && mirrors.isNotEmpty()) {
for (mirror in mirrors) {
if (mirror.annotationType.kind != TypeKind.NULL) {
builder.addAnnotation(AnnotationSpec.get(mirror))
}
}
}
var addComment = true
// Add super call if the method is not abstract
if (!method.modifiers.contains(Modifier.ABSTRACT)) {
if (method.returnType is NoType) {
builder.addStatement(createSuperCall(builder))
} else {
addComment = false
builder.addComment("TODO: Implement this method")
builder.addStatement("return " + createSuperCall(builder))
}
}
if (addComment) {
builder.addComment("TODO: Implement this method")
}
return builder
}
/**
* Create a superclass method invocation statement.
*
* @param builder The method builder.
* @return The super invocation statement string without ending ';'.
*/
private fun createSuperCall(builder: MethodSpec.Builder): String {
val sb = java.lang.StringBuilder()
sb.append("super.")
sb.append(builder.name)
sb.append("(")
for (i in builder.parameters.indices) {
sb.append(builder.parameters[i].name)
if (i != builder.parameters.size - 1) {
sb.append(", ")
}
}
sb.append(")")
return sb.toString()
}
}
}
}
@@ -19,11 +19,18 @@ package com.itsaky.lsp.java.utils;
import androidx.annotation.NonNull;
import com.squareup.javapoet.ImportCollectingCodeWriter;
import com.squareup.javapoet.TypeName;
import com.sun.source.util.JavacTask;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
@@ -34,6 +41,7 @@ public class MethodPtr {
public String className, methodName;
public String[] erasedParameterTypes;
public String[] simplifiedErasedParameterTypes;
public MethodPtr(@NonNull JavacTask task, @NonNull ExecutableElement method) {
final Types types = task.getTypes();
@@ -41,11 +49,61 @@ public class MethodPtr {
className = parent.getQualifiedName().toString();
methodName = method.getSimpleName().toString();
erasedParameterTypes = new String[method.getParameters().size()];
simplifiedErasedParameterTypes = new String[erasedParameterTypes.length];
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();
simplifiedErasedParameterTypes[i] = simplify(erased);
}
}
private String simplify(@NonNull TypeMirror type) {
if (type.getKind() == TypeKind.NULL) {
return type.toString();
}
final TypeName name = TypeName.get(type);
try {
return getSimpleName(name);
} catch (IOException e) {
return type.toString();
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MethodPtr)) {
return false;
}
MethodPtr methodPtr = (MethodPtr) o;
return Objects.equals(className, methodPtr.className)
&& Objects.equals(methodName, methodPtr.methodName)
&& Arrays.equals(erasedParameterTypes, methodPtr.erasedParameterTypes)
&& Arrays.equals(
simplifiedErasedParameterTypes, methodPtr.simplifiedErasedParameterTypes);
}
@Override
public int hashCode() {
int result = Objects.hash(className, methodName);
result = 31 * result + Arrays.hashCode(erasedParameterTypes);
result = 31 * result + Arrays.hashCode(simplifiedErasedParameterTypes);
return result;
}
@NonNull
private String getSimpleName(TypeName name) throws IOException {
final StringBuilder sb = new StringBuilder();
final ImportCollectingCodeWriter writer = new ImportCollectingCodeWriter(sb);
writer.setPrintQualifiedNames(false);
writer.emit(name);
return sb.toString();
}
}
@@ -29,7 +29,7 @@ public class FindTypeDeclarationAt extends TreePathScanner<ClassTree, Long> {
private final SourcePositions pos;
private CompilationUnitTree root;
private TreePath stored;
private TreePath path;
public FindTypeDeclarationAt(JavacTask task) {
pos = Trees.instance(task).getSourcePositions();
@@ -48,7 +48,7 @@ public class FindTypeDeclarationAt extends TreePathScanner<ClassTree, Long> {
return smaller;
}
if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) {
this.stored = getCurrentPath();
this.path = getCurrentPath();
return t;
}
return null;
@@ -60,7 +60,7 @@ public class FindTypeDeclarationAt extends TreePathScanner<ClassTree, Long> {
return b;
}
public TreePath getStoredPath() {
return stored;
public TreePath getPath() {
return path;
}
}
+6
View File
@@ -33,4 +33,10 @@
<string name="msg_select_fields">Select fields</string>
<string name="msg_no_fields_selected">No fields selected</string>
<string name="msg_no_fields_found">No fields found</string>
<string name="action_override_superclass_methods">Override superclass methods</string>
<string name="msg_no_methods_selected">No methods selected</string>
<string name="msg_cannot_override_methods">Unable to override methods</string>
<string name="msg_cannot_generate_setters_getters">Unable to generate setters and getters</string>
<string name="msg_select_methods">Select methods to override</string>
<string name="msg_no_overridable_methods">Unable to find any overridable method</string>
</resources>