Compute code actions synchronously

This commit is contained in:
Akash Yadav
2022-04-02 23:08:09 +05:30
parent 9c03b8067c
commit 4b438982bc
14 changed files with 370 additions and 104 deletions
+1
View File
@@ -68,6 +68,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_editor_bottom_sheet.xml" value="0.19572649572649573" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_editor_code_actions.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_editor_text_actions.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_filetree_item.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_reference_attr_editor.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_resource_input_field.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_string_attr_editor.xml" value="0.25" />
@@ -20,8 +20,10 @@
package com.itsaky.androidide.lsp;
import android.view.View;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.gson.Gson;
import com.itsaky.androidide.EditorActivity;
@@ -49,7 +51,7 @@ import com.itsaky.lsp.models.Location;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.models.TextEdit;
import com.itsaky.toaster.Toaster;
import io.github.rosemoe.sora.text.Content;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
@@ -57,6 +59,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import io.github.rosemoe.sora.text.Content;
/** AndroidIDE specific implementation of the LanguageClient */
public class IDELanguageClientImpl implements ILanguageClient {
@@ -221,6 +225,11 @@ public class IDELanguageClientImpl implements ILanguageClient {
*/
public void performCodeAction(IDEEditor editor, CodeActionItem action) {
if (activity() == null || editor == null || action == null) {
LOG.error(
"Unable to perform code action",
"activity=" + activity(),
"editor=" + editor,
"action=" + action);
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
return;
}
@@ -229,15 +238,19 @@ public class IDELanguageClientImpl implements ILanguageClient {
progress.setSubMessageEnabled(false);
progress.setWelcomeTextEnabled(false);
progress.setCancelable(false);
progress.setMessage(activity().getString(R.string.msg_performing_fixes));
progress.setMessage(activity().getString(R.string.msg_performing_actions));
progress.show(activity().getSupportFragmentManager(), "quick_fix_progress");
new TaskExecutor()
.executeAsyncProvideError(
() -> performCodeActionAsync(editor, action),
(a, b) -> {
progress.dismiss();
if (a == null || b != null || !a) {
(result, throwable) -> {
ThreadUtils.runOnUiThread(progress::dismiss);
if (result == null || throwable != null || !result) {
LOG.error(
"Unable to perform code action",
"result=" + result,
"throwable=" + throwable);
StudioApp.getInstance()
.toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
} else {
@@ -282,6 +295,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
}
private Boolean performCodeActionAsync(final IDEEditor editor, final CodeActionItem action) {
LOG.debug("Performing code action:", action);
final var changes = action.getChanges();
if (changes.isEmpty()) {
return Boolean.FALSE;
@@ -19,20 +19,27 @@ package com.itsaky.androidide.views.editor;
import android.annotation.SuppressLint;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.view.menu.MenuBuilder;
import com.itsaky.androidide.utils.Logger;
import io.github.rosemoe.sora.event.ClickEvent;
import io.github.rosemoe.sora.event.SelectionChangeEvent;
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow;
import com.itsaky.lsp.models.CodeActionItem;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import io.github.rosemoe.sora.event.ClickEvent;
import io.github.rosemoe.sora.event.SelectionChangeEvent;
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow;
/**
* Text action mode actions for the editor.
*
@@ -41,6 +48,7 @@ import java.util.stream.Collectors;
public class EditorTextActionMode implements IDEEditor.ITextActionPresenter {
private static final Logger LOG = Logger.newInstance("EditorTextActionMode");
private final List<CodeActionItem> codeActions = new ArrayList<>(0);
private final Set<IDEEditor.TextAction> registeredActions = new TreeSet<>();
private IDEEditor editor;
private ActionMode actionMode;
@@ -125,6 +133,18 @@ public class EditorTextActionMode implements IDEEditor.ITextActionPresenter {
this.unsubscribeEvents = true;
}
@Override
public void updateCodeActions(@NonNull List<CodeActionItem> actions) {
this.codeActions.clear();
this.codeActions.addAll(actions);
}
@NonNull
@Override
public List<CodeActionItem> getActions() {
return codeActions;
}
public void clearRegisteredActions() {
this.registeredActions.clear();
}
@@ -154,7 +174,7 @@ public class EditorTextActionMode implements IDEEditor.ITextActionPresenter {
mode.setTitle(android.R.string.selectTextMode);
for (var action : registeredActions) {
if (canShowAction(action)) {
if (canShowAction(editor, action)) {
menu.add(0, action.id, 0, action.titleId)
.setIcon(action.icon)
.setShowAsActionFlags(
@@ -218,15 +238,4 @@ public class EditorTextActionMode implements IDEEditor.ITextActionPresenter {
}
return false;
}
private boolean canShowAction(@NonNull IDEEditor.TextAction action) {
// all the actions are visible by default
// so we need to get a confirmation from the editor
if (action.visible) {
return editor.shouldShowTextAction(action.id);
}
return false;
}
}
@@ -22,14 +22,25 @@ import android.graphics.drawable.GradientDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.TextActionItemAdapter;
import com.itsaky.androidide.databinding.LayoutEditorActionsBinding;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.models.CodeActionItem;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import io.github.rosemoe.sora.event.HandleStateChangeEvent;
import io.github.rosemoe.sora.event.ScrollEvent;
import io.github.rosemoe.sora.event.SelectionChangeEvent;
@@ -38,12 +49,6 @@ import io.github.rosemoe.sora.event.Unsubscribe;
import io.github.rosemoe.sora.widget.CodeEditor;
import io.github.rosemoe.sora.widget.EditorTouchEventHandler;
import io.github.rosemoe.sora.widget.base.EditorPopupWindow;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* Presents text actions in a popup widow.
@@ -58,6 +63,7 @@ public class EditorTextActionWindow extends EditorPopupWindow
private static final long DELAY = 200;
protected final List<SubscriptionReceipt<?>> subscriptionReceipts;
private final List<CodeActionItem> codeActions = new ArrayList<>(0);
private final Set<IDEEditor.TextAction> registeredActions = new TreeSet<>();
private IDEEditor editor;
private LayoutEditorActionsBinding binding;
@@ -125,7 +131,7 @@ public class EditorTextActionWindow extends EditorPopupWindow
final var actions =
this.registeredActions.stream()
.filter(this::canShowAction)
.filter(action -> canShowAction(editor, action))
.collect(Collectors.toList());
this.binding.textActions.setAdapter(
new TextActionItemAdapter(actions, this::performTextAction));
@@ -144,6 +150,18 @@ public class EditorTextActionWindow extends EditorPopupWindow
this.unsubscribeEvents();
}
@Override
public void updateCodeActions(@NonNull List<CodeActionItem> actions) {
codeActions.clear();
codeActions.addAll(actions);
}
@NonNull
@Override
public List<CodeActionItem> getActions() {
return codeActions;
}
@NonNull
private Drawable createBackground() {
final var drawable = new GradientDrawable();
@@ -166,7 +184,7 @@ public class EditorTextActionWindow extends EditorPopupWindow
final var dp16 = dp8 * 2;
final var actions =
this.registeredActions.stream()
.filter(this::canShowAction)
.filter(action -> canShowAction(editor, action))
.collect(Collectors.toList());
actionsList.setAdapter(new TextActionItemAdapter(actions, this::performTextAction));
this.binding
@@ -186,17 +204,6 @@ public class EditorTextActionWindow extends EditorPopupWindow
super.show();
}
private boolean canShowAction(@NonNull IDEEditor.TextAction action) {
// all the actions are visible by default
// so we need to get a confirmation from the editor
if (action.visible) {
return editor.shouldShowTextAction(action.id);
}
return false;
}
private void subscribeToEvents() {
this.subscriptionReceipts.add(
this.editor.subscribeEvent(SelectionChangeEvent.class, this::onSelectionChanged));
@@ -16,6 +16,8 @@
*/
package com.itsaky.androidide.views.editor;
import static com.itsaky.androidide.views.editor.IDEEditor.TextAction.QUICKFIX;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
@@ -23,10 +25,12 @@ import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
@@ -58,17 +62,23 @@ import com.itsaky.lsp.models.SignatureHelp;
import com.itsaky.lsp.models.SignatureHelpParams;
import com.itsaky.lsp.util.DiagnosticUtil;
import com.itsaky.toaster.Toaster;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.event.SelectionChangeEvent;
import io.github.rosemoe.sora.event.Unsubscribe;
import io.github.rosemoe.sora.widget.CodeEditor;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.jetbrains.annotations.Contract;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.event.SelectionChangeEvent;
import io.github.rosemoe.sora.event.Unsubscribe;
import io.github.rosemoe.sora.widget.CodeEditor;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
public class IDEEditor extends CodeEditor {
@@ -856,7 +866,7 @@ public class IDEEditor extends CodeEditor {
actionPresenter.registerAction(
new TextAction(
createTextActionDrawable(R.drawable.ic_quickfix),
R.string.msg_fix_diagnostic,
R.string.msg_code_actions,
TextAction.QUICKFIX,
index++));
@@ -957,29 +967,6 @@ public class IDEEditor extends CodeEditor {
case TextAction.COMMENT_LINE:
case TextAction.UNCOMMENT_LINE:
return commentUncomment;
case TextAction.QUICKFIX:
if (getEditorLanguage() instanceof IDELanguage
&& mLanguageServer != null
&& mLanguageClient != null) {
final var lang = (IDELanguage) getEditorLanguage();
final var diagnostics = lang.getDiagnostics();
if (diagnostics.isEmpty()) {
return false;
}
final var diagnostic =
DiagnosticUtil.binarySearchDiagnostic(
diagnostics, getCursorAsLSPPosition());
if (diagnostic == null) {
LOG.info(
"No diagnostic found at cursor position. Skipping quickfix lookup...");
return false;
}
return true;
}
return false;
}
return true;
@@ -1022,7 +1009,7 @@ public class IDEEditor extends CodeEditor {
expandSelection();
break;
case TextAction.QUICKFIX:
showCodeActions();
showCodeActions(mTextActionPresenter.getActions());
getTextActionPresenter().dismiss();
break;
}
@@ -1051,15 +1038,15 @@ public class IDEEditor extends CodeEditor {
final var lang = (IDELanguage) getEditorLanguage();
final var diagnostics = lang.getDiagnostics();
if (diagnostics.isEmpty()) {
LOG.error("Cannot show code actions. No diagnostic items available.");
return;
LOG.error(
"No diagnostic items available. Code actions will be shown based on cursor position.");
}
final var diagnostic =
DiagnosticUtil.binarySearchDiagnostic(diagnostics, getCursorAsLSPPosition());
if (diagnostic == null) {
LOG.info("No diagnostic found at cursor position. Skipping code action lookup...");
return;
if (!diagnostics.isEmpty() && diagnostic == null) {
LOG.info(
"No diagnostic found at cursor position. Code actions will be shown based on cursor position.");
}
final var future = codeActions(Collections.singletonList(diagnostic));
@@ -1079,7 +1066,7 @@ public class IDEEditor extends CodeEditor {
}));
}
private void showCodeActions(List<CodeActionItem> actions) {
public void showCodeActions(List<CodeActionItem> actions) {
final var titles = actions.stream().map(CodeActionItem::getTitle).toArray(String[]::new);
final var builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle("Select code action...");
@@ -1195,7 +1182,6 @@ public class IDEEditor extends CodeEditor {
* @author Akash Yadav
*/
public interface ITextActionPresenter {
/**
* Bind the action presenter with the given editor instance.
*
@@ -1230,6 +1216,108 @@ public class IDEEditor extends CodeEditor {
* events and release any held resources.
*/
void destroy();
/**
* Update the list of computed code actions.
*
* @param actions The new list of code actions.
*/
void updateCodeActions(@NonNull List<CodeActionItem> actions);
/**
* Get the computed code actions.
*
* @return The list of code actions.
*/
@NonNull
List<CodeActionItem> getActions();
default boolean canShowAction(
@NonNull IDEEditor editor, @NonNull IDEEditor.TextAction action) {
if (action.id == QUICKFIX) {
try {
// If code actions are not returned within 150ms, hide the action.
final var future = computeCodeActions(editor);
final var result = future.get(150, TimeUnit.MILLISECONDS);
updateCodeActions(
result == null ? Collections.emptyList() : result.getActions());
return !getActions().isEmpty();
} catch (Throwable th) {
if (!(th instanceof TimeoutException)) {
LOG.error("Unable to calculate code actions", th);
}
return false;
}
}
// all the actions are visible by default
// so we need to get a confirmation from the editor
if (action.visible) {
return editor.shouldShowTextAction(action.id);
}
return false;
}
default void checkForCodeActions(IDEEditor editor) {
computeCodeActions(editor)
.whenComplete(
((codeActionResult, throwable) -> {
if (throwable != null) {
LOG.error("Error computing code actions", throwable);
}
final var codeAction = findAction(QUICKFIX);
final List<CodeActionItem> actions =
codeActionResult != null
? codeActionResult.getActions()
: Collections.emptyList();
updateCodeActions(actions);
if (codeAction != null) {
codeAction.visible =
codeActionResult != null
&& !codeActionResult.getActions().isEmpty();
}
invalidateActions();
}));
}
default CompletableFuture<CodeActionResult> computeCodeActions(IDEEditor editor) {
return CompletableFuture.supplyAsync(
() -> {
if (editor.getFile() == null) {
throw new CompletionException(
new NullPointerException(
"Cannot compute code actions. "
+ "No file is set to editor."));
}
final var file = editor.getFile().toPath();
final var range = editor.getCursorRange();
List<DiagnosticItem> diagnostics =
editor.getEditorLanguage() instanceof IDELanguage
? ((IDELanguage) editor.getEditorLanguage())
.getDiagnostics()
: Collections.emptyList();
final var diagnostic =
DiagnosticUtil.binarySearchDiagnostic(
diagnostics, editor.getCursorAsLSPPosition());
diagnostics =
diagnostic == null
? Collections.emptyList()
: Collections.singletonList(diagnostic);
final var params = new CodeActionParams(file, range, diagnostics);
LOG.debug("Requesting code actions for params:", params);
try {
return editor.codeActions(params).get();
} catch (Throwable e) {
throw new CompletionException(e);
}
});
}
}
/**
+2 -3
View File
@@ -30,10 +30,9 @@
<string name="get_started">Loslegen</string>
<string name="nothing_here">Nichts hier &#x2026;</string>
<!-- LSP, Diagnostics & Completions -->
<string name="msg_fix_diagnostic">[Schnelle L&#xF6;sung]</string>
<string name="msg_cannot_perform_fix">Schnellkorrektur kann nicht durchgef&#xFC;hrt werden</string>
<string name="msg_performing_fixes">Korrekturen durchf&#xFC;hren&#x2026;</string>
<string name="msg_code_actions">Schnellkorrekturen</string>
<string name="msg_performing_actions">Korrekturen durchf&#xFC;hren&#x2026;</string>
<string name="msg_code_actions">Aktionen</string>
<string name="msg_api_info_since">Seit API %d</string>
<string name="msg_api_info_removed">In API %d entfernt</string>
<string name="msg_api_info_deprecated">Veraltet in API %d</string>
+2 -3
View File
@@ -34,8 +34,7 @@
<!-- LSP, Diagnostics & Completions -->
<string name="msg_cannot_perform_fix">Impossible de lancer QuickFix</string>
<string name="msg_performing_fixes">Réglage de quelques bugs&#8230;</string>
<string name="msg_code_actions">Quickfixes</string>
<string name="msg_performing_actions">Réglage de quelques bugs&#8230;</string>
<string name="msg_api_info_since">Depuis l\'API %d</string>
<string name="msg_api_info_removed">Supprimé depuis l\'API %d</string>
<string name="msg_api_info_deprecated">Obsolète depuis l\'API %d</string>
@@ -357,7 +356,7 @@
<string name="hint_string_value">String value</string>
<string name="hint_string_resource">String resource</string>
<string name="hint_resource_reference">any resource reference</string>
<string name="msg_fix_diagnostic">QuickFix</string>
<string name="msg_code_actions">Actions</string>
<string name="title_format_code">Formater code</string>
<string name="msg_swipe_up">Balayez vers le haut pour accéder au contenue de la console, des logs et autres.</string>
<string name="msg_ide_crashed">AndroidIDE a crashé</string>
+1 -3
View File
@@ -27,14 +27,12 @@
<string name="app_logs">एप लॉग्स</string>
<string name="ide_logs">IDE लॉग्स</string>
<string name="label_splash_image">स्प्लेश इमेज</string>
<string name="cancel">रद्द करे</string>
<string name="get_started">शुरु करे</string>
<string name="nothing_here">यहाँ कुछ भी नहीं &#8230;</string>
<!-- LSP, Diagnostics & Completions -->
<string name="msg_fix_diagnostic">[क्विक फिक्स]</string>
<string name="msg_cannot_perform_fix">कोड क्रिया करने में असमर्थ </string>
<string name="msg_performing_fixes">सुधार कर रहे है &#8230;</string>
<string name="msg_performing_actions">सुधार कर रहे है &#8230;</string>
<string name="msg_code_actions">कोड क्रियाएं</string>
<string name="msg_api_info_since">एपीआई %d से उपलब्ध है</string>
<string name="msg_api_info_removed">एपीआई %d में हटाया गया</string>
+1 -1
View File
@@ -32,7 +32,7 @@
<!-- LSP, Diagnostics & Completions -->
<string name="msg_cannot_perform_fix">Tidak dapat menampilkan perbaikan cepat (QuickFix)</string>
<string name="msg_performing_fixes">Menampilkan perbaikan&#8230;</string>
<string name="msg_performing_actions">Menampilkan perbaikan&#8230;</string>
<string name="msg_code_actions">Quickfixes</string>
<string name="msg_api_info_since">Sejak API %d</string>
<string name="msg_api_info_removed">Dihapus pada API %d</string>
+2 -6
View File
@@ -29,17 +29,13 @@
<string name="app_logs">Лог приложения</string>
<string name="ide_logs">Лог IDE</string>
<string name="label_splash_image">Splash image</string>
<string name="cancel">Отмена</string>
<string name="get_started">Давайте начнём!</string>
<string name="nothing_here">Тут ничего &#8230;</string>
<!-- LSP, Diagnostics & Completions -->
<string name="msg_sdk_info_load_failed">Ошибка загрузки информации о SDK</string>
<string name="msg_fix_diagnostic">[Быстрое Решение]</string>
<string name="msg_no_code_actions">Быстрого Решения не найдено</string>
<string name="msg_cannot_perform_fix">Невозможно применить Быстрое Решение</string>
<string name="msg_performing_fixes">Применение изменений&#8230;</string>
<string name="msg_code_actions">Быстрые Решения</string>
<string name="msg_performing_actions">Применение изменений&#8230;</string>
<string name="msg_code_actions">действия</string>
<string name="msg_api_info_since">Начиная с API %d</string>
<string name="msg_api_info_removed">Удалено в API %d</string>
<string name="msg_api_info_deprecated">Устаревшее с API %d</string>
+2 -3
View File
@@ -31,10 +31,9 @@
<string name="nothing_here">这里啥也没有 &#8230;</string>
<!-- LSP, Diagnostics & Completions -->
<string name="msg_fix_diagnostic">[快速修复]</string>
<string name="msg_code_actions">行动</string>
<string name="msg_cannot_perform_fix">无法执行快速修复</string>
<string name="msg_performing_fixes">正在修复&#8230;</string>
<string name="msg_code_actions">快速修复</string>
<string name="msg_performing_actions">正在修复&#8230;</string>
<string name="msg_api_info_since">在API %d 添加</string>
<string name="msg_api_info_removed">在API %d 移除</string>
<string name="msg_api_info_deprecated">在API %d 弃用</string>
+2 -3
View File
@@ -32,8 +32,7 @@
<!-- LSP, Diagnostics & Completions -->
<string name="msg_cannot_perform_fix">Unable to perform quick fix</string>
<string name="msg_performing_fixes">Performing fixes&#8230;</string>
<string name="msg_code_actions">Quickfixes</string>
<string name="msg_performing_actions">Performing code action&#8230;</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>
@@ -343,7 +342,7 @@
<string name="hint_string_value">String value</string>
<string name="hint_string_resource">String resource</string>
<string name="hint_resource_reference">Any resource reference</string>
<string name="msg_fix_diagnostic">QuickFix</string>
<string name="msg_code_actions">Actions</string>
<string name="title_format_code">Format code</string>
<string name="idepref_java_useGoogleStyle_title">Use Google Java Style code formatting</string>
<string name="idepref_java_useGoogleStyle_summary">Use Google Java Style code formatting configuration for formatting Java source code.</string>
@@ -17,8 +17,13 @@
package com.itsaky.lsp.util;
import androidx.annotation.NonNull;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import java.util.Collections;
import java.util.List;
/**
@@ -28,17 +33,167 @@ import java.util.List;
*/
public class DiagnosticUtil {
/**
* Binary search all the diagnostic items in the given range.
*
* @param diagnostics The diagnostics to search items from.
* @param range The range to look for diagnostics in.
* @return The list of found diagnostics.
*/
public static List<DiagnosticItem> findDiagnosticsInRange(
List<DiagnosticItem> diagnostics, Range range) {
if (diagnostics == null || range == null || diagnostics.isEmpty()) {
return Collections.emptyList();
}
final var left = findLeftBound(diagnostics, range.getStart());
final var right = findRightBound(diagnostics, range.getEnd());
if (left < 0 || right >= diagnostics.size()) {
return Collections.emptyList();
}
return diagnostics.subList(left, right);
}
/**
* Find the leftmost diagnostic element containing the given position.
*
* @param diagnostics The list of diagnostics.
* @param position The position to search for.
* @return The index of the diagnostic item.
*/
private static int findLeftBound(List<DiagnosticItem> diagnostics, @NonNull Position position) {
final var index =
binarySearchDiagnosticPosition(
diagnostics, position.getLine(), position.getColumn());
if (index == -1) {
return -1;
}
if (index == 0) {
return index;
}
var left = index - 1;
var i = left;
do {
left = i;
i = binarySearchDiagnosticPositionInRange(diagnostics, position, 0, left);
} while (i != -1);
return left;
}
/**
* Find the rightmost diagnostic element containing the given position.
*
* @param diagnostics The list of diagnostics.
* @param position The position to search for.
* @return The index of the diagnostic item.
*/
private static int findRightBound(
List<DiagnosticItem> diagnostics, @NonNull Position position) {
final var index =
binarySearchDiagnosticPosition(
diagnostics, position.getLine(), position.getColumn());
if (index == -1) {
return -1;
}
if (index == diagnostics.size() - 1) {
return index;
}
var right = index + 1;
var i = right;
do {
right = i;
i =
binarySearchDiagnosticPositionInRange(
diagnostics, position, right, diagnostics.size() - 1);
} while (i != -1);
return right;
}
/**
* Binary search the position of the diagnostic item within the given list bounds containing the
* given position.
*
* @param diagnostics The list of diagnostics.
* @param position The position that will be used to check against the diagnostic range.
* @param start The start index of the item in the diagnostics list.
* @param end The end index of the item in the diagnostic list.
* @return The index of the found diagnostic.
*/
private static int binarySearchDiagnosticPositionInRange(
List<DiagnosticItem> diagnostics, Position position, int start, int end) {
var left = start;
var right = end;
while (left <= right) {
final var mid = (left + right) / 2;
final var d = diagnostics.get(mid);
final var r = d.getRange();
final var c = r.containsForBinarySearch(position);
if (c < 0) {
left = mid - 1;
} else if (c > 1) {
right = mid + 1;
} else {
return mid;
}
}
return -1;
}
/**
* Binary search the diagnostic item which contains the given line and column.
*
* @param diagnostics The list of diagnostics.
* @param position The position to search for.
* @return The diagnostic item.
*/
public static DiagnosticItem binarySearchDiagnostic(
List<DiagnosticItem> diagnostics, Position position) {
return binarySearchDiagnostic(diagnostics, position.getLine(), position.getColumn());
}
/**
* Binary search the diagnostic item which contains the given line and column.
*
* @param diagnostics The list of diagnostics.
* @param line The line to search for.
* @param column The column to search for.
* @return The diagnostic item.
*/
public static DiagnosticItem binarySearchDiagnostic(
List<DiagnosticItem> diagnostics, int line, int column) {
if (diagnostics.isEmpty()) {
final var index = binarySearchDiagnosticPosition(diagnostics, line, column);
if (index == -1) {
return null;
}
return diagnostics.get(index);
}
/**
* Binary search the diagnostic item which contains the given line and column.
*
* @param diagnostics The list of diagnostics.
* @param line The line to search for.
* @param column The column to search for.
* @return The index of the found diagnostic item.
*/
public static int binarySearchDiagnosticPosition(
List<DiagnosticItem> diagnostics, int line, int column) {
if (diagnostics.isEmpty()) {
return -1;
}
final var pos = new Position(line, column);
int left = 0;
int right = diagnostics.size() - 1;
@@ -53,10 +208,10 @@ public class DiagnosticUtil {
} else if (c > 0) {
left = mid + 1;
} else {
return d;
return mid;
}
}
return null;
return -1;
}
}
}
@@ -18,11 +18,13 @@
package com.itsaky.lsp.java.rewrite;
import androidx.annotation.NonNull;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.models.CodeActionItem;
import com.itsaky.lsp.models.CodeActionKind;
import com.itsaky.lsp.models.DocumentChange;
import com.itsaky.lsp.models.TextEdit;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
@@ -49,7 +51,7 @@ public abstract class Rewrite {
public List<CodeActionItem> asCodeActions(CompilerProvider compiler, String title) {
final Map<Path, TextEdit[]> edits = rewrite(compiler);
if (edits == null) {
if (edits == null || edits.isEmpty()) {
return Collections.emptyList();
}