mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Improved the 'Generate setters/getters' action' and added it to editor code actions
This commit is contained in:
@@ -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
|
||||
|
||||
+266
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user