From 4b438982bc7bcaf6be2941b583a27cf0864ce4a5 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 2 Apr 2022 23:08:09 +0530 Subject: [PATCH] Compute code actions synchronously --- .idea/misc.xml | 1 + .../androidide/lsp/IDELanguageClientImpl.java | 24 ++- .../views/editor/EditorTextActionMode.java | 39 +++-- .../views/editor/EditorTextActionWindow.java | 45 +++-- .../androidide/views/editor/IDEEditor.java | 164 ++++++++++++++---- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values-fr/strings.xml | 5 +- app/src/main/res/values-hi/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 8 +- app/src/main/res/values-zh/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- .../com/itsaky/lsp/util/DiagnosticUtil.java | 163 ++++++++++++++++- .../com/itsaky/lsp/java/rewrite/Rewrite.java | 4 +- 14 files changed, 370 insertions(+), 104 deletions(-) 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(); }