Show quickfixes when diagnostic is selected

This commit is contained in:
Akash Yadav
2021-10-19 16:31:40 +05:30
parent 268bd20003
commit 1124d28013
8 changed files with 366 additions and 139 deletions
Binary file not shown.
@@ -856,8 +856,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public void openFile(File file) {
openFile(file, null);
public EditorFragment openFile(File file) {
return openFile(file, null);
}
public EditorFragment openFile(File file, org.eclipse.lsp4j.Range selection) {
@@ -865,7 +865,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
int i = mPagerAdapter.openFile(file, selection, this);
if(i >= 0 && !mBinding.tabs.getTabAt(i).isSelected())
mBinding.tabs.getTabAt(i).select();
mBinding.editorDrawerLayout.closeDrawer(GravityCompat.END);
if(mBinding.editorDrawerLayout.isDrawerOpen(GravityCompat.END)) {
mBinding.editorDrawerLayout.closeDrawer(GravityCompat.END);
}
invalidateOptionsMenu();
try {
return mPagerAdapter.getFrag(i);
@@ -214,7 +214,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
}
public interface FileActionListener {
public void openFile(File file);
public EditorFragment openFile(File file);
public void showFileOptions(File file, TreeNode node);
}
}
@@ -16,16 +16,20 @@ import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.SemanticHighlight;
import com.itsaky.lsp.services.IDELanguageClient;
import com.itsaky.lsp.services.IDELanguageServer;
import com.itsaky.toaster.Toaster;
import io.github.rosemoe.editor.text.Content;
import io.github.rosemoe.editor.widget.CodeEditor;
import java.io.File;
@@ -37,7 +41,6 @@ import java.util.Map;
import java.util.stream.Collectors;
import java9.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
@@ -51,8 +54,7 @@ import org.eclipse.lsp4j.ShowDocumentParams;
import org.eclipse.lsp4j.ShowDocumentResult;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.eclipse.lsp4j.TextEdit;
/**
* AndroidIDE specific implementation of the LanguageClient
@@ -341,9 +343,43 @@ public abstract class AbstractLanguageClient implements IDELanguageClient {
.collect(Collectors.toList())
);
}
/**
* Perform the given {@link CodeAction}
*
* @param editor The {@link CodeEditor} that invoked the code action request.
* This is required to reduce the time finding the code action from the edits.
* @param action The action to perform
*/
public void performCodeAction(CodeEditor editor, CodeAction action) {
if(activity() == null || editor == null || action == null) {
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
return;
}
final ProgressSheet progress = new ProgressSheet();
progress.setSubMessageEnabled(false);
progress.setWelcomeTextEnabled(false);
progress.setCancelable(false);
progress.setMessage(activity().getString(R.string.msg_performing_fixes));
progress.show(activity().getSupportFragmentManager(), "quick_fix_progress");
new TaskExecutor().executeAsyncProvideError(() -> performCodeActionAsync(editor, action), (a, b) -> {
final Boolean complete = a;
final Throwable error = b;
progress.dismiss();
if(complete == null || error != null || !complete.booleanValue()) {
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
LOG.error("Quickfix error", error);
return;
}
});
}
/**
* Usually called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show a specific document in EditorActivity and select the specified range
* Usually called {@link CodeEditor} to show a specific document in EditorActivity and select the specified range
*/
@Override
public CompletableFuture<ShowDocumentResult> showDocument(ShowDocumentParams params) {
@@ -425,6 +461,58 @@ public abstract class AbstractLanguageClient implements IDELanguageClient {
return groups;
}
private Boolean performCodeActionAsync(final CodeEditor editor, final CodeAction action) {
final Map <String, List<TextEdit>> edits = action.getEdit().getChanges();
if(edits == null || edits.isEmpty()) {
return Boolean.FALSE;
}
for(Map.Entry<String, List<TextEdit>> entry : edits.entrySet()) {
final File file = new File(URI.create(entry.getKey()));
if(!file.exists()) continue;
for(TextEdit edit : entry.getValue()) {
final String editorFilepath = editor.getFile() == null ? "" : editor.getFile().getAbsolutePath();
if(file.getAbsolutePath().equals(editorFilepath)) {
// Edit is in the same editor which requested the code action
editInEditor(editor, edit);
} else {
EditorFragment openedFrag = activity().getPagerAdapter().findEditorByFile(file);
if(openedFrag != null && openedFrag.getEditor() != null) {
// Edit is in another 'opened' file
editInEditor(openedFrag.getEditor(), edit);
} else {
// Edit is in some other file which is not opened
// We should open that file and perform the edit
openedFrag = activity().openFile(file);
if(openedFrag != null && openedFrag.getEditor() != null) {
editInEditor(openedFrag.getEditor(), edit);
}
}
}
}
}
return Boolean.TRUE;
}
private void editInEditor (final CodeEditor editor, final TextEdit edit) {
final Range range = edit.getRange();
final int startLine = range.getStart().getLine();
final int startCol = range.getStart().getCharacter();
final int endLine = range.getEnd().getLine();
final int endCol = range.getEnd().getCharacter();
activity().runOnUiThread(() -> {
if(startLine == endLine && startCol == endCol) {
editor.getText().insert(startLine, startCol, edit.getNewText());
} else {
editor.getText().replace(startLine, startCol, endLine, endCol, edit.getNewText());
}
});
}
public DiagnosticsAdapter newDiagnosticsAdapter() {
return new DiagnosticsAdapter(mapAsGroup(this.diagnostics), activity());
}
@@ -157,7 +157,7 @@ public class BinaryHeap {
public void update(int id, int newValue) {
int position = idToPosition.get(id, 0);
if (position == 0) {
throw new IllegalArgumentException("trying to update with an invalid id");
return;
}
int origin = nodes[position].data;
nodes[position].data = newValue;
@@ -177,7 +177,7 @@ public class BinaryHeap {
public void remove(int id) {
int position = idToPosition.get(id, 0);
if (position == 0) {
throw new IllegalArgumentException("trying to remove with an invalid id");
return;
}
idToPosition.delete(id);
//Replace removed node with last node
@@ -876,6 +876,15 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
return mLanguageServer;
}
/**
* Get the language client attached to this Editor
*
* @return The language client currently attached
*/
public AbstractLanguageClient getLanguageClient() {
return mLanguageClient;
}
/**
* Sets up this editor according to the capabilities of the current language server
*/
@@ -4835,7 +4844,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
* @return A {@link CompletableFuture}. May return {@code null}.
*/
public CompletableFuture<List<Either<Command, CodeAction>>> requestCodeActions() {
if(mLanguageServer == null && mLanguageClient == null) return null;
if(mLanguageServer == null || mLanguageClient == null) return null;
final List<Diagnostic> diagnostics = findDiagnosticsContainingLine(getCursor().getLeftLine());
return mCodeActionProvider.codeActions(mLanguageServer, getDocumentIdentifier(), getCursorRange(), diagnostics);
@@ -22,11 +22,27 @@ import android.graphics.drawable.Drawable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.toaster.Toaster;
import io.github.rosemoe.editor.util.IntPair;
import java.util.List;
import java9.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import android.view.View;
import android.widget.TextView;
import com.blankj.utilcode.util.SizeUtils;
import java.util.stream.Collectors;
import android.view.Gravity;
/**
* Action Mode style text action panel for editor
@@ -41,7 +57,7 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
EditorTextActionModeStarter(CodeEditor editor) {
mEditor = editor;
}
@SuppressLint("RestrictedApi")
@Override
public void onBeginTextSelect() {
@@ -49,145 +65,253 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
return;
}
mActionMode = ((AppCompatActivity) mEditor.getContext()).startSupportActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_SELECT_TEXT;
if(menu instanceof MenuBuilder){
MenuBuilder builder = (MenuBuilder) menu;
builder.setOptionalIconsVisible(true);
}
actionMode.setTitle(android.R.string.selectTextMode);
TypedArray array = mEditor.getContext().getTheme().obtainStyledAttributes(new int[]{
android.R.attr.actionModeSelectAllDrawable,
android.R.attr.actionModeCutDrawable,
android.R.attr.actionModeCopyDrawable,
android.R.attr.actionModePasteDrawable,
});
menu.add(0, 0, 0, mEditor.getContext().getString(android.R.string.selectAll))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(0));
if(mEditor.isEditable()) {
menu.add(0, 1, 0, mEditor.getContext().getString(android.R.string.cut))
private List<Either<Command, CodeAction>> fixes;
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_SELECT_TEXT;
if (menu instanceof MenuBuilder) {
MenuBuilder builder = (MenuBuilder) menu;
builder.setOptionalIconsVisible(true);
}
actionMode.setTitle(android.R.string.selectTextMode);
TypedArray array = mEditor.getContext().getTheme().obtainStyledAttributes(new int[]{
android.R.attr.actionModeSelectAllDrawable,
android.R.attr.actionModeCutDrawable,
android.R.attr.actionModeCopyDrawable,
android.R.attr.actionModePasteDrawable,
});
final CompletableFuture<List<Either<Command, CodeAction>>> future = mEditor.requestCodeActions();
if (future != null) {
future.whenComplete((a, b) -> {
fixes = a;
});
menu.add(0, 8, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.msg_code_actions))
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER)
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_quickfix));
}
menu.add(0, 0, 0, mEditor.getContext().getString(android.R.string.selectAll))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(1));
}
.setIcon(array.getDrawable(0));
menu.add(0, 2, 0, mEditor.getContext().getString(android.R.string.copy))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(2));
if(mEditor.isEditable()) {
menu.add(0, 3, 0, mEditor.getContext().getString(android.R.string.paste))
if (mEditor.isEditable()) {
menu.add(0, 1, 0, mEditor.getContext().getString(android.R.string.cut))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(1));
}
menu.add(0, 2, 0, mEditor.getContext().getString(android.R.string.copy))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(3));
}
if(mEditor.isGotoDefinitionEnabled()) {
menu.add(0, 4, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition))
.setIcon(array.getDrawable(2));
if (mEditor.isEditable()) {
menu.add(0, 3, 0, mEditor.getContext().getString(android.R.string.paste))
.setShowAsActionFlags(2)
.setIcon(array.getDrawable(3));
}
if (mEditor.isGotoDefinitionEnabled()) {
menu.add(0, 4, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition))
.setShowAsActionFlags(1)
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition));
}
if (mEditor.isFindReferencesEnabled()) {
menu.add(0, 5, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references))
.setShowAsActionFlags(1)
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_find_references));
}
menu.add(0, 6, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_comment_line))
.setShowAsActionFlags(1)
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition));
}
if(mEditor.isFindReferencesEnabled()) {
menu.add(0, 5, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references))
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_comment_line));
menu.add(0, 7, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_uncomment_line))
.setShowAsActionFlags(1)
.setIcon(createDrawable( com.itsaky.androidide.R.drawable.ic_find_references));
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_uncomment_line));
array.recycle();
return true;
}
private Drawable createDrawable(int icon) {
final Drawable d = ContextCompat.getDrawable(mEditor.getContext(), icon);
d.setColorFilter(ContextCompat.getColor(mEditor.getContext(), com.itsaky.androidide.R.color.secondaryColor), PorterDuff.Mode.SRC_ATOP);
return d;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
if (mEditor.getFile() == null) return false;
final String name = mEditor.getFile().getName();
final boolean isJava = name.endsWith(".java") || name.endsWith(".gradle");
final boolean isXml = name.endsWith(".xml");
final MenuItem def = menu.findItem(4);
final MenuItem ref = menu.findItem(5);
final MenuItem comment = menu.findItem(6);
final MenuItem uncomment = menu.findItem(7);
final MenuItem quickfix = menu.findItem(8);
comment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
uncomment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
/**
* Thesw menu items may, or may not be added
* So we need to check if its null or not
* before we perform any further actions
*/
if (def != null) {
def.getIcon().setAlpha(isJava ? 255 : 76);
def.setVisible(isJava);
}
if (ref != null) {
ref.getIcon().setAlpha(isJava ? 255 : 76);
ref.setVisible(isJava);
}
if (quickfix != null) {
quickfix.getIcon().setAlpha(isJava ? 255 : 76);
quickfix.setVisible(isJava && fixes != null && !fixes.isEmpty());
}
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
switch (menuItem.getItemId()) {
case 0:
mEditor.selectAll();
break;
case 1:
mEditor.cutText();
break;
case 2:
mEditor.copyText();
break;
case 3:
mEditor.pasteText();
break;
case 4:
mEditor.findDefinition();
break;
case 5:
mEditor.findReferences();
break;
case 6:
mEditor.commentLine();
break;
case 7:
mEditor.uncommentLine();
break;
case 8 :
if (fixes == null || fixes.isEmpty()) {
StudioApp.getInstance().toast(com.itsaky.androidide.R.string.msg_no_code_actions, Toaster.Type.INFO);
break;
}
showFixes();
break;
}
if (menuItem.getItemId() != 0) {
onExit();
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_NONE;
mActionMode = null;
mEditor.setSelection(mEditor.getCursor().getLeftLine(), mEditor.getCursor().getLeftColumn());
}
void showFixes() {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mEditor.getContext(), com.itsaky.androidide.R.style.AppTheme_MaterialAlertDialog);
builder.setTitle(com.itsaky.androidide.R.string.msg_code_actions);
builder.setView(createFixesListView());
builder.show();
}
ListView createFixesListView() {
final ListView list = new ListView(mEditor.getContext());
final QuickFixAdapter adapter = new QuickFixAdapter(
fixes.stream()
.filter(e -> e != null && e.isRight())
.map(e -> e.getRight())
.collect(Collectors.toList())
);
list.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
list.setAdapter(adapter);
list.setOnItemClickListener((a, b, c, d) -> {
performCodeAction(adapter.getItem(c));
});
return list;
}
class QuickFixAdapter extends BaseAdapter {
menu.add(0, 6, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_comment_line))
.setShowAsActionFlags(1)
.setIcon(createDrawable( com.itsaky.androidide.R.drawable.ic_comment_line));
private final List<CodeAction> actions;
private final int dp16 = SizeUtils.dp2px(16);
private final int dp4 = SizeUtils.dp2px(4);
private final int dp48 = SizeUtils.dp2px(48);
menu.add(0, 7, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_uncomment_line))
.setShowAsActionFlags(1)
.setIcon(createDrawable( com.itsaky.androidide.R.drawable.ic_uncomment_line));
array.recycle();
return true;
}
private Drawable createDrawable(int icon) {
final Drawable d = ContextCompat.getDrawable(mEditor.getContext(), icon);
d.setColorFilter(ContextCompat.getColor(mEditor.getContext(), com.itsaky.androidide.R.color.secondaryColor), PorterDuff.Mode.SRC_ATOP);
return d;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
if(mEditor.getFile() == null) return false;
final String name = mEditor.getFile().getName();
final boolean isJava = name.endsWith(".java") || name.endsWith(".gradle");
final boolean isXml = name.endsWith(".xml");
final MenuItem def = menu.findItem(4);
final MenuItem ref = menu.findItem(5);
final MenuItem comment = menu.findItem(6);
final MenuItem uncomment = menu.findItem(7);
comment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
uncomment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
/**
* Thesw menu items may, or may not be added
* So we need to check if its null or not
* before we perform any further actions
*/
if(def != null) {
def.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76);
}
if(ref != null) {
ref.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76);
}
public QuickFixAdapter(List<CodeAction> actions) {
this.actions = actions;
}
return true;
}
@Override
public int getCount() {
return actions.size();
}
@Override
public CodeAction getItem(int position) {
return actions.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
convertView = createItemTextView(position, parent);
}
return convertView;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
switch (menuItem.getItemId()) {
case 0:
mEditor.selectAll();
break;
case 1:
mEditor.cutText();
break;
case 2:
mEditor.copyText();
break;
case 3:
mEditor.pasteText();
break;
case 4:
mEditor.findDefinition();
break;
case 5:
mEditor.findReferences();
break;
case 6:
mEditor.commentLine();
break;
case 7:
mEditor.uncommentLine();
break;
private View createItemTextView(int position, ViewGroup parent) {
final CodeAction action = getItem(position);
final TextView text = new TextView(parent.getContext());
final String title = action.getTitle();
text.setText(title);
text.setPaddingRelative(dp16, dp4, dp16, dp4);
text.setMinHeight(dp48);
text.setGravity(Gravity.CENTER_VERTICAL);
return text;
}
}
if(menuItem.getItemId() != 0) {
onExit();
void performCodeAction(CodeAction action) {
if(mEditor.getLanguageClient() != null) {
mEditor.getLanguageClient().performCodeAction(mEditor, action);
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_NONE;
mActionMode = null;
mEditor.setSelection(mEditor.getCursor().getLeftLine(), mEditor.getCursor().getLeftColumn());
}
});
});
}
@Override
+4 -2
View File
@@ -1,10 +1,12 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="title_confirm_changes">Confirm changes</string>
<string name="msg_confirm_changes">These files will be modified:\n%s</string>
<string name="msg_no_code_actions">No code actions found</string>
<string name="msg_no_code_actions">No quickfix found</string>
<string name="msg_cannot_perform_fix">Unable to perform quick fix</string>
<string name="msg_performing_fixes">Performing fixes&#8230;</string>
<string name="msg_analyzing_actions">Analyzing code actions</string>
<string name="msg_service_not_started">Services has not been started yet</string>
<string name="msg_code_actions">Code Actions</string>
<string name="msg_code_actions">Quickfixes</string>
<string name="msg_api_info_since">Since API %d</string>
<string name="msg_api_info_removed">Removed in API %d</string>
<string name="msg_api_info_deprecated">Deprecated in API %d</string>