diff --git a/.idea/misc.xml b/.idea/misc.xml
index d01bdfaf0..8e4f71cb0 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -68,6 +68,7 @@
+
diff --git a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java
index bf682b7b5..40b5a7f31 100755
--- a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java
+++ b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java
@@ -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;
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java
index a7295a2f9..3f1d80108 100644
--- a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java
@@ -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 codeActions = new ArrayList<>(0);
private final Set 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 actions) {
+ this.codeActions.clear();
+ this.codeActions.addAll(actions);
+ }
+
+ @NonNull
+ @Override
+ public List 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;
- }
}
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java
index d5190d3b6..2c3f064ae 100644
--- a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java
@@ -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> subscriptionReceipts;
+ private final List codeActions = new ArrayList<>(0);
private final Set 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 actions) {
+ codeActions.clear();
+ codeActions.addAll(actions);
+ }
+
+ @NonNull
+ @Override
+ public List 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));
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
index d49997369..e6fe47a1a 100644
--- a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
@@ -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 actions) {
+ public void showCodeActions(List 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 actions);
+
+ /**
+ * Get the computed code actions.
+ *
+ * @return The list of code actions.
+ */
+ @NonNull
+ List 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 actions =
+ codeActionResult != null
+ ? codeActionResult.getActions()
+ : Collections.emptyList();
+ updateCodeActions(actions);
+ if (codeAction != null) {
+ codeAction.visible =
+ codeActionResult != null
+ && !codeActionResult.getActions().isEmpty();
+ }
+
+ invalidateActions();
+ }));
+ }
+
+ default CompletableFuture 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 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);
+ }
+ });
+ }
}
/**
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 753fb437d..de791829f 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -30,10 +30,9 @@
Loslegen
Nichts hier …
- [Schnelle Lösung]
Schnellkorrektur kann nicht durchgeführt werden
- Korrekturen durchführen…
- Schnellkorrekturen
+ Korrekturen durchführen…
+ Aktionen
Seit API %d
In API %d entfernt
Veraltet in API %d
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index d7ff4688c..5b6156bd6 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -34,8 +34,7 @@
Impossible de lancer QuickFix
- Réglage de quelques bugs…
- Quickfixes
+ Réglage de quelques bugs…
Depuis l\'API %d
Supprimé depuis l\'API %d
Obsolète depuis l\'API %d
@@ -357,7 +356,7 @@
String value
String resource
any resource reference
- QuickFix
+ Actions
Formater code
Balayez vers le haut pour accéder au contenue de la console, des logs et autres.
AndroidIDE a crashé
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 7867af353..12391b03a 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -27,14 +27,12 @@
एप लॉग्स
IDE लॉग्स
स्प्लेश इमेज
- रद्द करे
शुरु करे
यहाँ कुछ भी नहीं …
- [क्विक फिक्स]
कोड क्रिया करने में असमर्थ
- सुधार कर रहे है …
+ सुधार कर रहे है …
कोड क्रियाएं
एपीआई %d से उपलब्ध है
एपीआई %d में हटाया गया
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 291b3df4a..55368b5f5 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -32,7 +32,7 @@
Tidak dapat menampilkan perbaikan cepat (QuickFix)
- Menampilkan perbaikan…
+ Menampilkan perbaikan…
Quickfixes
Sejak API %d
Dihapus pada API %d
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 2003fa9ac..779b704c6 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -29,17 +29,13 @@
Лог приложения
Лог IDE
Splash image
- Отмена
Давайте начнём!
Тут ничего …
- Ошибка загрузки информации о SDK
- [Быстрое Решение]
- Быстрого Решения не найдено
Невозможно применить Быстрое Решение
- Применение изменений…
- Быстрые Решения
+ Применение изменений…
+ действия
Начиная с API %d
Удалено в API %d
Устаревшее с API %d
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 20a9598f0..5bec3015b 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -31,10 +31,9 @@
这里啥也没有 …
- [快速修复]
+ 行动
无法执行快速修复
- 正在修复…
- 快速修复
+ 正在修复…
在API %d 添加
在API %d 移除
在API %d 弃用
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 919f7d089..c5620c6d7 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,8 +32,7 @@
Unable to perform quick fix
- Performing fixes…
- Quickfixes
+ Performing code action…
Since API %d
Removed in API %d
Deprecated in API %d
@@ -343,7 +342,7 @@
String value
String resource
Any resource reference
- QuickFix
+ Actions
Format code
Use Google Java Style code formatting
Use Google Java Style code formatting configuration for formatting Java source code.
diff --git a/lsp/api/src/main/java/com/itsaky/lsp/util/DiagnosticUtil.java b/lsp/api/src/main/java/com/itsaky/lsp/util/DiagnosticUtil.java
index 1b2d60745..c6e0f1a63 100644
--- a/lsp/api/src/main/java/com/itsaky/lsp/util/DiagnosticUtil.java
+++ b/lsp/api/src/main/java/com/itsaky/lsp/util/DiagnosticUtil.java
@@ -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 findDiagnosticsInRange(
+ List 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 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 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 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 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 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 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;
}
-}
+}
\ No newline at end of file
diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java
index 0571446fc..cf6f36036 100644
--- a/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java
+++ b/lsp/java/src/main/java/com/itsaky/lsp/java/rewrite/Rewrite.java
@@ -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 asCodeActions(CompilerProvider compiler, String title) {
final Map edits = rewrite(compiler);
- if (edits == null) {
+ if (edits == null || edits.isEmpty()) {
return Collections.emptyList();
}