Added some more code actions

This commit is contained in:
Akash Yadav
2022-04-11 22:26:38 +05:30
parent a00c6ec227
commit 0d21603bdf
9 changed files with 229 additions and 244 deletions
@@ -164,7 +164,7 @@ class DefaultActionsRegistry : ActionsRegistry() {
}
}
}
private fun notifyActionExec(action: ActionItem, result: Any) {
for (listener in listeners) {
listener.onExec(action, result)
@@ -1,50 +0,0 @@
/*
* 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 androidx.annotation.NonNull;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.models.CodeActionItem;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.Range;
import java.nio.file.Path;
import java.util.List;
/**
* @author Akash Yadav
*/
public interface ActionProvider {
/**
* Provide code actions.
*
* @param compiler The compiler.
* @param file The file.
* @param range The cursor range.
* @param diagnostics The diagnostics within the cursor range.
* @return The list of code actions.
*/
@NonNull
List<CodeActionItem> provideActions(
@NonNull CompilerProvider compiler,
@NonNull Path file,
@NonNull Range range,
@NonNull List<DiagnosticItem> diagnostics);
}
@@ -0,0 +1,76 @@
/*
* 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.utils.Logger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.java.rewrite.AddException
import com.itsaky.lsp.java.utils.CodeActionUtils
import com.itsaky.lsp.models.DiagnosticItem
/** @author Akash Yadav */
class AddThrowsAction : BaseCodeAction() {
override val id = "lsp_java_addThrows"
override var label: String = "Add 'throws'"
private val diagnosticCode = "compiler.err.unreported.exception.need.to.catch.or.throw"
private val log = Logger.newInstance(javaClass.simpleName)
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
val needsThrow = CodeActionUtils.findMethod(task, diagnostic.range)
val exceptionName = CodeActionUtils.extractExceptionName(diagnostic.message)
return@get AddException(
needsThrow.className,
needsThrow.methodName,
needsThrow.erasedParameterTypes,
exceptionName)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is AddException) {
log.warn("Unable to add 'throws' expression")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
}
}
@@ -83,17 +83,15 @@ import javax.lang.model.util.Elements;
*
* @author Akash Yadav
*/
public class CursorCodeActionProvider implements ActionProvider {
public class CursorCodeActionProvider {
private static final Logger LOG = newInstance("JavaCursorCodeActionProvider");
@NonNull
@Override
public List<CodeActionItem> provideActions(
public List<CodeActionItem> provideActions (
@NonNull CompilerProvider compiler,
@NonNull Path file,
@NonNull Range range,
@NonNull List<DiagnosticItem> diagnostics) {
@NonNull Range range) {
logStarted(file, range);
Instant started = Instant.now();
@@ -1,177 +0,0 @@
/*
* 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 static com.itsaky.androidide.utils.Logger.newInstance;
import static com.itsaky.lsp.java.utils.CodeActionUtils.extractExceptionName;
import static com.itsaky.lsp.java.utils.CodeActionUtils.findClassNeedingConstructor;
import static com.itsaky.lsp.java.utils.CodeActionUtils.findMethod;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.java.compiler.CompileTask;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.java.compiler.SynchronizedTask;
import com.itsaky.lsp.java.rewrite.AddException;
import com.itsaky.lsp.java.rewrite.AddSuppressWarningAnnotation;
import com.itsaky.lsp.java.rewrite.GenerateRecordConstructor;
import com.itsaky.lsp.java.rewrite.Rewrite;
import com.itsaky.lsp.java.utils.MethodPtr;
import com.itsaky.lsp.models.CodeActionItem;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.Range;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
/**
* Provides code actions for diagnostics.
*
* @author Akash Yadav
*/
public class DiagnosticsCodeActionProvider implements ActionProvider {
private static final Logger LOG = newInstance("JavaDiagnosticsCodeActionProvider");
@NonNull
@Override
public List<CodeActionItem> provideActions(
@NonNull CompilerProvider compiler,
@NonNull Path file,
@NonNull Range range,
@NonNull List<DiagnosticItem> diagnostics) {
LOG.info(
String.format(
Locale.getDefault(),
"Check %d diagnostics for quick fixes...",
diagnostics.size()));
Instant started = Instant.now();
final SynchronizedTask synchronizedTask = compiler.compile(file);
final List<Pair<String, Rewrite>> actions =
synchronizedTask.get(task -> computeActions(compiler, file, diagnostics, task));
final List<CodeActionItem> result = new ArrayList<>();
for (Pair<String, Rewrite> pair : actions) {
try {
final CodeActionItem action = pair.second.asCodeActions(compiler, pair.first);
if (action != null) {
result.add(action);
}
} catch (Throwable throwable) {
LOG.error("Unable to create rewrite for quickfix", throwable);
}
}
LOG.debug(
String.format(
Locale.ROOT,
"Created %d code actions in %dms",
result.size(),
Instant.now().toEpochMilli() - started.toEpochMilli()));
return result;
}
@NonNull
private List<Pair<String, Rewrite>> computeActions(
CompilerProvider compiler,
@NonNull Path file,
@NonNull List<DiagnosticItem> diagnostics,
CompileTask task) {
List<Pair<String, Rewrite>> rewrites = new ArrayList<>();
for (DiagnosticItem d : diagnostics) {
List<Pair<String, Rewrite>> pairs =
codeActionForDiagnostic(compiler, task, file, d).stream()
.filter(pair -> !TextUtils.isEmpty(pair.first) && pair.second != null)
.collect(Collectors.toList());
rewrites.addAll(pairs);
}
return rewrites;
}
@NonNull
private List<Pair<String, Rewrite>> codeActionForDiagnostic(
CompilerProvider compiler, CompileTask task, Path file, @NonNull DiagnosticItem d) {
switch (d.getCode()) {
case "compiler.warn.unchecked.call.mbr.of.raw.type":
return handleUnchecked(task, d);
case "compiler.err.unreported.exception.need.to.catch.or.throw":
return handleUnreportedException(task, d);
case "compiler.err.var.not.initialized.in.default.constructor":
return handleVarNotInitialized(task, d);
default:
return Collections.singletonList(Pair.create("", null));
}
}
@NonNull
private List<Pair<String, Rewrite>> handleVarNotInitialized(
CompileTask task, @NonNull DiagnosticItem d) {
Rewrite rewrite;
String title;
final String needsConstructor = findClassNeedingConstructor(task, d.getRange());
if (needsConstructor == null) {
return Collections.singletonList(Pair.create("", null));
}
rewrite = new GenerateRecordConstructor(needsConstructor);
title = "Generate constructor";
return Collections.singletonList(Pair.create(title, rewrite));
}
@NonNull
private List<Pair<String, Rewrite>> handleUnreportedException(
CompileTask task, @NonNull DiagnosticItem d) {
Rewrite rewrite;
String title;
final MethodPtr needsThrow = findMethod(task, d.getRange());
final String exceptionName = extractExceptionName(d.getMessage());
rewrite =
new AddException(
needsThrow.className,
needsThrow.methodName,
needsThrow.erasedParameterTypes,
exceptionName);
title = "Add 'throws'";
return Collections.singletonList(Pair.create(title, rewrite));
}
@NonNull
private List<Pair<String, Rewrite>> handleUnchecked(
CompileTask task, @NonNull DiagnosticItem d) {
Rewrite rewrite;
String title;
final MethodPtr warnedMethod = findMethod(task, d.getRange());
rewrite =
new AddSuppressWarningAnnotation(
warnedMethod.className,
warnedMethod.methodName,
warnedMethod.erasedParameterTypes);
title = "Suppress 'unchecked' warning";
return Collections.singletonList(Pair.create(title, rewrite));
}
}
@@ -0,0 +1,73 @@
/*
* 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.utils.Logger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.java.rewrite.GenerateRecordConstructor
import com.itsaky.lsp.java.utils.CodeActionUtils
import com.itsaky.lsp.models.DiagnosticItem
/** @author Akash Yadav */
class GenerateConstructorAction : BaseCodeAction() {
override val id = "lsp_java_generateConstructor"
override var label: String = "Generate constructor"
private val diagnosticCode = "compiler.err.var.not.initialized.in.default.constructor"
private val log = Logger.newInstance(javaClass.simpleName)
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
val needsConstructor =
CodeActionUtils.findClassNeedingConstructor(task, diagnostic.range)
?: return@get false
return@get GenerateRecordConstructor(needsConstructor)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is GenerateRecordConstructor) {
log.warn("Unable to generate constructor")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
}
}
@@ -32,5 +32,7 @@ class JavaCodeActionsMenu : CodeActionsMenu() {
addAction(RemoveMethodAction())
addAction(RemoveUnusedThrowsAction())
addAction(CreateMissingMethodAction())
addAction(SuppressUncheckedWarningAction())
addAction(AddThrowsAction())
}
}
@@ -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.actions
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.Logger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.java.rewrite.AddSuppressWarningAnnotation
import com.itsaky.lsp.java.utils.CodeActionUtils
import com.itsaky.lsp.models.DiagnosticItem
/** @author Akash Yadav */
class SuppressUncheckedWarningAction : BaseCodeAction() {
override val id = "lsp_java_suppressUncheckedWarning"
override var label: String = "Suppress 'unchecked' warning"
private val diagnosticCode = "compiler.warn.unchecked.call.mbr.of.raw.type"
private val log = Logger.newInstance(javaClass.simpleName)
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
val warnedMethod = CodeActionUtils.findMethod(task, diagnostic.range)
return@get AddSuppressWarningAnnotation(
warnedMethod.className, warnedMethod.methodName, warnedMethod.erasedParameterTypes)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is AddSuppressWarningAnnotation) {
log.warn("Unable to suppress 'unchecked' warning")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
}
}
@@ -20,9 +20,7 @@ package com.itsaky.lsp.java.providers;
import androidx.annotation.NonNull;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.java.actions.ActionProvider;
import com.itsaky.lsp.java.actions.CursorCodeActionProvider;
import com.itsaky.lsp.java.actions.DiagnosticsCodeActionProvider;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.models.CodeActionParams;
import com.itsaky.lsp.models.CodeActionResult;
@@ -38,15 +36,8 @@ public class CodeActionProvider {
@NonNull
public CodeActionResult codeActions(@NonNull CodeActionParams params) {
final ActionProvider actionProvider;
if (params.getDiagnostics().isEmpty()) {
actionProvider = new CursorCodeActionProvider();
} else {
actionProvider = new DiagnosticsCodeActionProvider();
}
return new CodeActionResult(
actionProvider.provideActions(
compiler, params.getFile(), params.getRange(), params.getDiagnostics()));
new CursorCodeActionProvider()
.provideActions(compiler, params.getFile(), params.getRange()));
}
}