Improved the 'Generate setters/getters' action' and added it to editor code actions

This commit is contained in:
Akash Yadav
2022-04-12 21:31:29 +05:30
parent a525bdf0d0
commit b64fb89759
6 changed files with 285 additions and 92 deletions
@@ -36,6 +36,7 @@ import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.java.JavaLanguageServer;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.util.DiagnosticUtil;
import com.itsaky.lsp.xml.XMLLanguageServer;
@@ -118,6 +119,7 @@ public class ActionsPopupMenu extends PopupMenu implements ActionsRegistry.Actio
CodeEditor.class, editor); // For LSP actions, as they cannot access IDEEditor class
data.put(File.class, editor.getFile());
data.put(DiagnosticItem.class, getDiagnosticAtCursor());
data.put(Range.class, editor.getCursorRange());
data.put(
JavaLanguageServer.class,
(JavaLanguageServer) StudioApp.getInstance().getJavaLanguageServer());
@@ -113,26 +113,6 @@ public class CursorCodeActionProvider {
@NonNull TreeMap<String, Rewrite> rewrites,
@NonNull CompileTask task) {
// 1-based line and column index
final int startLine = range.getStart().getLine() + 1;
final int startColumn = range.getStart().getColumn() + 1;
final int endLine = range.getEnd().getLine() + 1;
final int endColumn = range.getEnd().getColumn() + 1;
final LineMap lines = task.root().getLineMap();
final long start = lines.getPosition(startLine, startColumn);
final long end = lines.getPosition(endLine, endColumn);
if (start != -1 && isBlankLine(task.root(), start) && !isInMethod(task, start)) {
rewrites.putAll(overrideInheritedMethods(task, file, start));
}
if (start != -1 && end != -1) {
final List<TreePath> variables = findVariables(task, start, end);
if (!variables.isEmpty()) {
rewrites.putAll(createSettersAndGetters(file, task, variables));
}
}
List<CodeActionItem> actions = new ArrayList<>();
for (String title : rewrites.keySet()) {
final Rewrite rewrite = rewrites.get(title);
@@ -156,7 +136,7 @@ public class CursorCodeActionProvider {
private Map<String, ? extends Rewrite> createSettersAndGetters(
@NonNull Path file, CompileTask task, List<TreePath> variables) {
return Collections.singletonMap(
"Create setters/getters", new GenerateSettersAndGetters(file, task, variables));
"Create setters/getters", new GenerateSettersAndGetters(file, variables));
}
@NonNull
@@ -0,0 +1,266 @@
/*
* 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 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.compiler.CompileTask
import com.itsaky.lsp.java.utils.EditHelper
import com.itsaky.lsp.java.utils.JavaPoetUtils.Companion.print
import com.itsaky.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.lsp.models.Range
import com.itsaky.toaster.Toaster
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.TypeName
import com.sun.source.tree.ClassTree
import com.sun.source.tree.Tree
import com.sun.source.tree.VariableTree
import com.sun.source.util.TreePath
import com.sun.source.util.Trees
import io.github.rosemoe.sora.widget.CodeEditor
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import javax.lang.model.element.Modifier
import javax.lang.model.element.VariableElement
/** @author Akash Yadav */
class GenerateSettersAndGettersAction : BaseCodeAction() {
override val id: String = "lsp_java_generateSettersAndGetters"
override var label: String = "Generate setters/getters"
private val log = Logger.newInstance(javaClass.simpleName)
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
}
val typeFinder = FindTypeDeclarationAt(task.task)
var type = typeFinder.scan(task.root(file), start)
if (type == null) {
type = typeFinder.scan(task.root(file), end)
}
if (type == null) {
return@get false
}
val fieldNames =
type.members
.filter { it.kind == Tree.Kind.VARIABLE } // Collect fields
.map { it as VariableTree } // Map the fields as VariableTree
.filter {
!it.modifiers.flags.contains(Modifier.STATIC)
} // Remove all fields that are static
.map { "${it.name}: ${it.type}" } // Get the names
log.debug("Found ${fieldNames.size} variables in class ${type.simpleName}")
return@get fieldNames
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is List<*>) {
log.warn("Unable to generate setters/getters")
return
}
if (result.isEmpty()) {
BaseApplication.getBaseInstance().toast("No fields found", Toaster.Type.INFO)
return
}
@Suppress("UNCHECKED_CAST") val names = (result as List<String>).toTypedArray()
val checkedNames = mutableSetOf<String>()
val builder = newDialogBuilder(data)
builder.setTitle("Select fields")
builder.setMultiChoiceItems(names, BooleanArray(result.size)) { _, which, checked ->
checkedNames.apply {
val item = names[which]
if (checked) {
add(item)
} else {
remove(item)
}
}
}
builder.setPositiveButton("Ok") { dialog, _ ->
dialog.dismiss()
if (checkedNames.isEmpty()) {
BaseApplication.getBaseInstance().toast("No fields selected", Toaster.Type.ERROR)
return@setPositiveButton
}
CompletableFuture.runAsync { generateForFields(data, checkedNames) }.whenComplete {
_,
error,
->
if (error != null) {
log.error("Unable to generate setters and getters", error)
return@whenComplete
}
}
}
builder.setNegativeButton("Cancel", null)
builder.show()
}
private fun generateForFields(data: ActionData, names: MutableSet<String>) {
val server = data[JavaLanguageServer::class.java]!!
val range = data[Range::class.java]!!
val file = requirePath(data)
server.compiler.compile(file).run { 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()) {
throw CompletionException(
RuntimeException("Unable to find position for the given selection range"))
}
val typeFinder = FindTypeDeclarationAt(task.task)
var type = typeFinder.scan(task.root(file), start)
if (type == null) {
type = typeFinder.scan(task.root(file), end)
}
if (type == null) {
throw CompletionException(
RuntimeException("Unable to find class declaration within cursor range"))
}
val fields =
type.members
.filter { it.kind == Tree.Kind.VARIABLE }
.map { it as VariableTree }
.filter { !it.modifiers.flags.contains(Modifier.STATIC) }
.toMutableList()
fields.removeIf { !names.contains("${it.name}: ${it.type}") }
log.debug("Creating setters/getters for fields", fields.map { it.name })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.storedPath, it) })
}
}
private fun generateForFields(
data: ActionData,
task: CompileTask,
type: ClassTree,
paths: List<TreePath>,
) {
val file = requirePath(data)
val editor = data[CodeEditor::class.java]!!
val trees = Trees.instance(task.task)
val insert = EditHelper.insertAtEndOfClass(task.task, task.root(file), type)
val sb = StringBuilder()
for (path in paths) {
val element = trees.getElement(path) ?: continue
if (element !is VariableElement) {
continue
}
val leaf = path.leaf
val indent = EditHelper.indent(task.task, task.root(file), leaf) + 4
sb.append(createGetter(element, indent))
if (!element.modifiers.contains(Modifier.FINAL)) {
sb.append(createSetter(element, indent))
}
}
editor.text.insert(insert.line, insert.column, sb)
}
private fun createGetter(variable: VariableElement, indent: Int): String {
val name: String = variable.simpleName.toString()
val builder = MethodSpec.methodBuilder(createName(name, "get"))
builder
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.get(variable.asType()))
.addStatement("return \$N", name)
val imports = mutableSetOf<String>()
var text = "\n" + print(builder.build(), imports)
text = text.replace("\n", "\n${EditHelper.repeatSpaces(indent)}")
text = "$text\n"
return text
}
private fun createSetter(variable: VariableElement, indent: Int): String {
val name: String = variable.simpleName.toString()
val builder = MethodSpec.methodBuilder(createName(name, "set"))
builder
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeName.get(variable.asType()), name)
.addStatement("this.\$N = \$N", name, name)
val imports = mutableSetOf<String>()
var text = "\n" + print(builder.build(), imports)
text = text.replace("\n", "\n${EditHelper.repeatSpaces(indent)}")
text = "$text\n"
return text
}
private fun createName(name: String, prefix: String): String {
val sb = StringBuilder(name)
sb.setCharAt(0, Character.toUpperCase(sb[0]))
sb.insert(0, prefix)
return sb.toString()
}
}
@@ -34,5 +34,6 @@ class JavaCodeActionsMenu : CodeActionsMenu() {
addAction(CreateMissingMethodAction())
addAction(SuppressUncheckedWarningAction())
addAction(AddThrowsAction())
addAction(GenerateSettersAndGettersAction())
}
}
@@ -53,93 +53,30 @@ import javax.lang.model.element.VariableElement;
public class GenerateSettersAndGetters extends Rewrite {
private final Path file;
private final CompileTask task;
private final List<TreePath> variables;
public GenerateSettersAndGetters(Path file, CompileTask task, List<TreePath> variables) {
Objects.requireNonNull(task);
public GenerateSettersAndGetters(Path file, List<TreePath> variables) {
Objects.requireNonNull(variables);
this.file = file;
this.task = task;
this.variables = variables;
}
@Override
public Map<Path, TextEdit[]> rewrite(CompilerProvider compiler) {
final List<TextEdit> edits = new ArrayList<>();
final Trees trees = Trees.instance(task.task);
for (TreePath path : variables) {
final Element element = trees.getElement(path);
if (element == null || element.getKind() != ElementKind.FIELD) {
continue;
}
return compiler.compile(file)
.get(
task -> {
final Trees trees = Trees.instance(task.task);
final VariableElement variable = (VariableElement) element;
final Tree leaf = path.getLeaf();
addGetter(compiler, variable, leaf, edits);
if (!variable.getModifiers().contains(Modifier.FINAL)) {
addSetter(compiler, variable, leaf, edits);
}
}
return Collections.singletonMap(file, edits.toArray(new TextEdit[0]));
return Collections.singletonMap(file, edits.toArray(new TextEdit[0]));
});
}
@Override
protected void applyCommands(@NonNull CodeActionItem action) {
action.setCommand(new Command("Format code", Command.FORMAT_CODE));
}
private void addSetter(
CompilerProvider compiler,
@NonNull VariableElement variable,
Tree leaf,
@NonNull List<TextEdit> edits) {
final int indent = EditHelper.indent(task.task, task.root(), leaf) + 4;
final Position insertAt = EditHelper.insertAfter(task.task, task.root(), leaf);
final String name = variable.getSimpleName().toString();
MethodSpec.Builder builder = MethodSpec.methodBuilder(createName(name, "set"));
builder.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeName.get(variable.asType()), name)
.addStatement("this.$N = $N", name, name);
final Set<String> imports = new TreeSet<>();
String text = JavaPoetUtils.print(builder.build(), imports);
text = text.replace("\n", "\n".concat(EditHelper.repeatSpaces(indent)));
text = text.concat("\n");
edits.add(new TextEdit(new Range(insertAt, insertAt), text));
}
private void addGetter(
CompilerProvider compiler,
@NonNull VariableElement variable,
Tree leaf,
@NonNull List<TextEdit> edits) {
final int indent = EditHelper.indent(task.task, task.root(), leaf) + 4;
final Position insertAt = EditHelper.insertAfter(task.task, task.root(), leaf);
final String name = variable.getSimpleName().toString();
MethodSpec.Builder builder = MethodSpec.methodBuilder(createName(name, "get"));
builder.addModifiers(Modifier.PUBLIC)
.returns(TypeName.get(variable.asType()))
.addStatement("return $N", name);
final Set<String> imports = new TreeSet<>();
String text = JavaPoetUtils.print(builder.build(), imports);
text = text.replace("\n", "\n".concat(EditHelper.repeatSpaces(indent)));
text = text.concat("\n");
edits.add(new TextEdit(new Range(insertAt, insertAt), text));
}
@NonNull
private String createName(String name, String prefix) {
final StringBuilder sb = new StringBuilder(name);
sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
sb.insert(0, prefix);
return sb.toString();
}
}
@@ -17,6 +17,8 @@
package com.itsaky.lsp.java.utils;
import androidx.annotation.NonNull;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.java.rewrite.AddImport;
import com.itsaky.lsp.models.Position;
@@ -147,6 +149,11 @@ public class EditHelper {
public static int indent(
final JavacTask task, final CompilationUnitTree root, final Tree leaf) {
SourcePositions pos = Trees.instance(task).getSourcePositions();
return indent(root, leaf, pos);
}
private static int indent(
@NonNull CompilationUnitTree root, Tree leaf, @NonNull SourcePositions pos) {
LineMap lines = root.getLineMap();
long startClass = pos.getStartPosition(root, leaf);
long startLine = lines.getStartPosition(lines.getLineNumber(startClass));