diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java b/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java index 132a627de..bae46b853 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java @@ -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()); diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/actions/CursorCodeActionProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/CursorCodeActionProvider.java index a5799f92b..7dd6301b5 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/actions/CursorCodeActionProvider.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/CursorCodeActionProvider.java @@ -113,26 +113,6 @@ public class CursorCodeActionProvider { @NonNull TreeMap 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 variables = findVariables(task, start, end); - if (!variables.isEmpty()) { - rewrites.putAll(createSettersAndGetters(file, task, variables)); - } - } - List actions = new ArrayList<>(); for (String title : rewrites.keySet()) { final Rewrite rewrite = rewrites.get(title); @@ -156,7 +136,7 @@ public class CursorCodeActionProvider { private Map createSettersAndGetters( @NonNull Path file, CompileTask task, List variables) { return Collections.singletonMap( - "Create setters/getters", new GenerateSettersAndGetters(file, task, variables)); + "Create setters/getters", new GenerateSettersAndGetters(file, variables)); } @NonNull diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/actions/GenerateSettersAndGettersAction.kt b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/GenerateSettersAndGettersAction.kt new file mode 100644 index 000000000..4422ca31a --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/GenerateSettersAndGettersAction.kt @@ -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 . + */ + +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).toTypedArray() + + val checkedNames = mutableSetOf() + 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) { + 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, + ) { + 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() + 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() + 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() + } +} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/actions/JavaCodeActionsMenu.kt b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/JavaCodeActionsMenu.kt index 0fddee2bc..89094c911 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/actions/JavaCodeActionsMenu.kt +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/actions/JavaCodeActionsMenu.kt @@ -34,5 +34,6 @@ class JavaCodeActionsMenu : CodeActionsMenu() { addAction(CreateMissingMethodAction()) addAction(SuppressUncheckedWarningAction()) addAction(AddThrowsAction()) + addAction(GenerateSettersAndGettersAction()) } } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateSettersAndGetters.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateSettersAndGetters.java index d63303982..6e3e6c729 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateSettersAndGetters.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/GenerateSettersAndGetters.java @@ -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 variables; - public GenerateSettersAndGetters(Path file, CompileTask task, List variables) { - Objects.requireNonNull(task); + public GenerateSettersAndGetters(Path file, List variables) { Objects.requireNonNull(variables); this.file = file; - this.task = task; this.variables = variables; } @Override public Map rewrite(CompilerProvider compiler) { final List 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 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 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 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 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(); - } } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java index 1b54c156c..18d41bb5b 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/utils/EditHelper.java @@ -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));