fix(lsp): progress dialog for 'find references' action is shown indefinitely

This happens because the 'ReferenceProvider' in the Java LSP tries to start another compilation process while working with the current compilation task.
This results in a deadlock where the current task waits for the new compilation request to return and the new compilation request waits for the current task to finish. This commit fixes this issue.

Apart from fixing this issue, this commit also makes the 'Go to definition' and 'Find references' actions cancelable. Also, it eliminates the usage of the deprecated 'ProgressDialog' class for these two actions.
This commit is contained in:
Akash Yadav
2023-07-09 12:56:28 +05:30
parent 907c6b89a3
commit 764a6fa8c2
23 changed files with 322 additions and 205 deletions
@@ -35,6 +35,7 @@ import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.FlashType.ERROR
import com.itsaky.androidide.utils.FlashType.INFO
import com.itsaky.androidide.utils.FlashType.SUCCESS
import java.util.function.Consumer
const val DURATION_SHORT = 2000L
const val DURATION_LONG = 3500L
@@ -44,6 +45,7 @@ val COLOR_SUCCESS = Color.parseColor("#4CAF50")
val COLOR_ERROR = Color.parseColor("#f44336")
const val COLOR_INFO = Color.DKGRAY
@JvmOverloads
fun Activity.flashbarBuilder(
gravity: Flashbar.Gravity = TOP,
duration: Long = DURATION_SHORT,
@@ -101,6 +103,18 @@ fun Activity.flashInfo(@StringRes msg: Int) {
flashbarBuilder().infoIcon().message(msg).showOnUiThread()
}
@JvmOverloads
fun Activity.flashProgress(configure: Consumer<Flashbar.Builder>? = null, action: Consumer<Flashbar>) {
val builder = flashbarBuilder(gravity = TOP, duration = DURATION_INDEFINITE)
.showProgress(Flashbar.ProgressPosition.LEFT)
configure?.accept(builder)
val flashbar = builder.build()
flashbar.show()
action.accept(flashbar)
}
fun Flashbar.Builder.showOnUiThread() {
build().showOnUiThread()
}
@@ -21,6 +21,7 @@ import android.app.Activity
import androidx.annotation.StringRes
import com.blankj.utilcode.util.ActivityUtils
import com.itsaky.androidide.flashbar.Flashbar
import java.util.function.Consumer
fun flashbarBuilder(): Flashbar.Builder? {
return withActivity { flashbarBuilder() }
@@ -58,6 +59,11 @@ fun flashInfo(@StringRes msg: Int) {
withActivity { flashInfo(msg) }
}
@JvmOverloads
fun flashProgress(configure: Consumer<Flashbar.Builder>? = null, action: Consumer<Flashbar>) {
withActivity { flashProgress(configure, action) }
}
private fun <T> withActivity(action: Activity.() -> T?): T? {
return ActivityUtils.getTopActivity()?.let { it.action() }
?: run {
@@ -68,6 +74,7 @@ private fun <T> withActivity(action: Activity.() -> T?): T? {
/** The type of flashbar message. */
enum class FlashType {
ERROR,
INFO,
SUCCESS
@@ -39,7 +39,8 @@ import java.util.function.Predicate
*
* @author Akash Yadav
*/
class CommonCompletionProvider(private val server: ILanguageServer) {
class CommonCompletionProvider(private val server: ILanguageServer,
private val cancelChecker: CompletionCancelChecker) {
private val log = ILogger.newInstance(javaClass.simpleName)
@@ -66,7 +67,7 @@ class CommonCompletionProvider(private val server: ILanguageServer) {
CompletionParams(Position(position.line, position.column, position.index), file)
params.content = content
params.prefix = prefix
server.complete(params)
server.complete(params, cancelChecker)
} catch (e: Throwable) {
if (e is ProcessCancelledException) {
@@ -74,7 +75,7 @@ class CommonCompletionProvider(private val server: ILanguageServer) {
}
// Do not log if completion was interrupted or cancelled
if (!(e is ProcessCancelledException || e is CompletionCancelledException)) {
if (!(e is ProcessCancelledException || e is CompletionCancelledException)) {
if (!server.handleFailure(LSPFailure(COMPLETION, e))) {
log.error("Unable to compute completions", e)
}
@@ -17,7 +17,7 @@
package com.itsaky.androidide.editor.language
import com.itsaky.androidide.lsp.api.ICompletionCancelChecker
import com.itsaky.androidide.progress.ICancelChecker
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException
import io.github.rosemoe.sora.lang.completion.CompletionPublisher
@@ -26,11 +26,13 @@ import io.github.rosemoe.sora.lang.completion.CompletionPublisher
*
* @author Akash Yadav
*/
class CompletionCancelChecker(val publisher: CompletionPublisher) : ICompletionCancelChecker {
class CompletionCancelChecker(private val publisher: CompletionPublisher) :
ICancelChecker.Default() {
/** Check if the completion is cancelled. */
@Throws(CompletionCancelledException::class)
override fun abortIfCancelled() {
publisher.checkCancelled()
super.abortIfCancelled()
}
}
@@ -17,25 +17,21 @@
package com.itsaky.androidide.editor.language;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.itsaky.androidide.editor.api.IEditor;
import com.itsaky.androidide.lookup.Lookup;
import com.itsaky.androidide.lsp.api.ICompletionCancelChecker;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.preferences.internal.EditorPreferencesKt;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.utils.ILogger;
import java.nio.file.Paths;
import java.util.ArrayList;
import io.github.rosemoe.sora.lang.Language;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.format.Formatter;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import java.nio.file.Paths;
import java.util.ArrayList;
/**
* Base class for language implementations in the IDE.
@@ -43,7 +39,7 @@ import io.github.rosemoe.sora.text.ContentReference;
* @author Akash Yadav
*/
public abstract class IDELanguage implements Language {
private static final ILogger LOG = ILogger.newInstance("IDELanguage");
private Formatter formatter;
@@ -56,10 +52,10 @@ public abstract class IDELanguage implements Language {
throws CompletionCancelledException {
try {
final var cancelChecker = new CompletionCancelChecker(publisher);
Lookup.getDefault().register(ICompletionCancelChecker.class, cancelChecker);
doComplete(content, position, publisher, extraArguments);
Lookup.getDefault().register(ICancelChecker.class, cancelChecker);
doComplete(content, position, publisher, cancelChecker, extraArguments);
} finally {
Lookup.getDefault().unregister(ICompletionCancelChecker.class);
Lookup.getDefault().unregister(ICancelChecker.class);
}
}
@@ -67,6 +63,7 @@ public abstract class IDELanguage implements Language {
final @NonNull ContentReference content,
final @NonNull CharPosition position,
final @NonNull CompletionPublisher publisher,
final @NonNull CompletionCancelChecker cancelChecker,
final @NonNull Bundle extraArguments) {
final var server = getLanguageServer();
if (server == null) {
@@ -80,7 +77,7 @@ public abstract class IDELanguage implements Language {
return;
}
final var completionProvider = new CommonCompletionProvider(server);
final var completionProvider = new CommonCompletionProvider(server, cancelChecker);
final var file = Paths.get(path);
final var completionItems =
@@ -122,12 +119,13 @@ public abstract class IDELanguage implements Language {
public int getTabSize() {
return EditorPreferencesKt.getTabSize();
}
@Override
public int getIndentAdvance(@NonNull final ContentReference content, final int line, final int column) {
public int getIndentAdvance(@NonNull final ContentReference content, final int line,
final int column) {
return getIndentAdvance(content.getLine(line).substring(0, column));
}
public int getIndentAdvance(@NonNull String line) {
return 0;
}
@@ -22,7 +22,7 @@ import static com.itsaky.androidide.resources.R.string;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.app.Dialog;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Rect;
@@ -33,9 +33,11 @@ import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.editor.R;
import com.itsaky.androidide.editor.adapters.CompletionListAdapter;
import com.itsaky.androidide.editor.api.IEditor;
import com.itsaky.androidide.editor.api.ILspEditor;
@@ -56,9 +58,11 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent;
import com.itsaky.androidide.flashbar.Flashbar;
import com.itsaky.androidide.lsp.api.ILanguageClient;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.models.Command;
import com.itsaky.androidide.lsp.models.DefinitionParams;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.ReferenceParams;
@@ -68,10 +72,12 @@ import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.FileManager;
import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.DocumentUtils;
import com.itsaky.androidide.utils.FlashbarActivityUtilsKt;
import com.itsaky.androidide.utils.FlashbarUtilsKt;
import com.itsaky.androidide.utils.ILogger;
import io.github.rosemoe.sora.event.ContentChangeEvent;
@@ -86,6 +92,7 @@ import io.github.rosemoe.sora.widget.component.EditorTextActionWindow;
import io.github.rosemoe.sora.widget.snippet.variable.ISnippetVariableResolver;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import kotlin.io.FilesKt;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -492,84 +499,70 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor {
* file in this editor, the range specified in the response will be selected.
*/
@Override
@SuppressWarnings({"deprecation", "unused"})
public void findDefinition() {
if (getFile() == null) {
return;
}
final ProgressDialog pd = ProgressDialog.show(getContext(), null,
getContext().getString(string.msg_finding_definition));
final var cancelChecker = new ICancelChecker.Default();
try {
final CompletableFuture<DefinitionResult> future = CompletableFuture.supplyAsync(() -> {
final var params = new com.itsaky.androidide.lsp.models.DefinitionParams(getFile().toPath(),
new Position(getCursor().getLeftLine(),
getCursor().getLeftColumn()));
final var action = (Consumer<Flashbar>) flashbar -> {
final var params = new DefinitionParams(getFile().toPath(),
getCursorLSPPosition());
return languageServer.findDefinition(params);
});
final CompletableFuture<DefinitionResult> future = CompletableFuture.supplyAsync(
() -> languageServer.findDefinition(params, cancelChecker));
future.whenComplete((result, error) -> {
if (result == null || languageClient == null || future.isCancelled() ||
future.isCompletedExceptionally()) {
LOG.error("An error occurred while finding definition", error);
showDefinitionNotFound(pd);
return;
}
future.whenComplete(
(result, error) -> onFindDefinitionResult(flashbar, future, result, error));
};
final var locations = result.getLocations();
if (locations.size() == 0) {
LOG.error("No definitions found", "Size:", locations.size());
showDefinitionNotFound(pd);
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> {
if (locations.size() == 1) {
var location = locations.get(0);
if (DocumentUtils.isSameFile(location.getFile(), getFile().toPath())) {
setSelection(location.getRange());
return;
}
languageClient.showDocument(
new ShowDocumentParams(location.getFile(), location.getRange()));
} else {
languageClient.showLocations(locations);
}
});
dismissOnUiThread(pd);
});
} catch (Throwable th) {
LOG.error("An error occurred while finding definition", th);
showDefinitionNotFound(pd);
}
FlashbarActivityUtilsKt.flashProgress(((Activity) getContext()),
builder -> configureFlashbar(builder, string.msg_finding_definition, cancelChecker),
action);
}
/**
* Notify the user that no definitions can be found.
*
* @param pd The {@link ProgressDialog} that was shown when requesting the definitions.
*/
@SuppressWarnings("deprecation")
private void showDefinitionNotFound(final ProgressDialog pd) {
//noinspection ConstantConditions
private void onFindDefinitionResult(Flashbar flashbar, CompletableFuture<DefinitionResult> future,
DefinitionResult result, Throwable error) {
LOG.debug("onFindDefinitionsResult");
dismissOnUiThread(flashbar);
if (result == null || languageClient == null || future.isCancelled() ||
future.isCompletedExceptionally()) {
LOG.error("An error occurred while finding definition", error);
notify(string.msg_no_definition);
return;
}
final var locations = result.getLocations();
if (locations.size() == 0) {
LOG.error("No definitions found", "Size:", locations.size());
notify(string.msg_no_definition);
return;
}
ThreadUtils.runOnUiThread(() -> {
FlashbarUtilsKt.flashError(string.msg_no_definition);
pd.dismiss();
if (locations.size() == 1) {
var location = locations.get(0);
if (DocumentUtils.isSameFile(location.getFile(), getFile().toPath())) {
setSelection(location.getRange());
return;
}
languageClient.showDocument(
new ShowDocumentParams(location.getFile(), location.getRange()));
} else {
languageClient.showLocations(locations);
}
});
}
/**
* Dismisses the given dialog on the UI thread.
* Dismisses the given flashbar on the UI thread.
*
* @param dialog The dialog to dismiss.
* @param flashbar The flashbar to dismiss.
*/
private void dismissOnUiThread(@NonNull final Dialog dialog) {
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(dialog::dismiss);
private void dismissOnUiThread(@NonNull final Flashbar flashbar) {
post(flashbar::dismiss);
}
/**
@@ -585,66 +578,71 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor {
return;
}
@SuppressWarnings("deprecation") final ProgressDialog pd = ProgressDialog.show(getContext(),
null, getContext().getString(string.msg_finding_references));
final var cancelChecker = new ICancelChecker.Default();
try {
final var action = (Consumer<Flashbar>) flashbar -> {
final var referenceParams = new ReferenceParams(getFile().toPath(),
getCursorLSPPosition(), true);
final var future = CompletableFuture.supplyAsync(
() -> languageServer.findReferences(referenceParams));
() -> languageServer.findReferences(referenceParams, cancelChecker));
future.whenComplete((result, error) -> onFindReferencesResult(pd, future, result, error));
} catch (Throwable th) {
LOG.error("An error occurred while finding references", th);
showReferencesNotFound(pd);
}
future.whenComplete(
(result, error) -> onFindReferencesResult(flashbar, future, result, error));
};
FlashbarActivityUtilsKt.flashProgress(((Activity) getContext()),
builder -> configureFlashbar(builder, R.string.msg_finding_references, cancelChecker),
action);
}
@SuppressWarnings("deprecation")
private void onFindReferencesResult(final ProgressDialog pd,
private void onFindReferencesResult(final Flashbar flashbar,
final CompletableFuture<ReferenceResult> future,
final ReferenceResult result, final Throwable error
) {
LOG.debug("onFindRefsResult");
dismissOnUiThread(flashbar);
if (result == null || languageClient == null || future.isCancelled() ||
future.isCompletedExceptionally()) {
LOG.error("An error occurred while finding references", error);
showReferencesNotFound(pd);
notify(string.msg_no_references);
return;
}
if (result.getLocations().isEmpty()) {
showReferencesNotFound(pd);
notify(string.msg_no_references);
return;
} else {
if (result.getLocations().size() == 1) {
final var loc = result.getLocations().get(0);
if (DocumentUtils.isSameFile(loc.getFile(), getFile().toPath())) {
setSelection(loc.getRange());
return;
}
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> languageClient.showLocations(result.getLocations()));
}
dismissOnUiThread(pd);
if (result.getLocations().size() == 1) {
final var loc = result.getLocations().get(0);
if (DocumentUtils.isSameFile(loc.getFile(), getFile().toPath())) {
setSelection(loc.getRange());
return;
}
}
ThreadUtils.runOnUiThread(() -> languageClient.showLocations(result.getLocations()));
}
/**
* Notify the user that no references were found for the selected token.
* Notify the user that no results were found for a task.
*
* @param pd The {@link ProgressDialog} that was shown when requesting references.
*/
@SuppressWarnings("deprecation")
private void showReferencesNotFound(final ProgressDialog pd) {
private void notify(@StringRes final int message) {
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> {
FlashbarUtilsKt.flashError(string.msg_no_references);
pd.dismiss();
});
ThreadUtils.runOnUiThread(() -> FlashbarUtilsKt.flashError(message));
}
private void configureFlashbar(Flashbar.Builder builder, @StringRes int message,
ICancelChecker cancelChecker) {
builder.message(message)
.negativeActionText(android.R.string.cancel)
.negativeActionTapListener(bar -> {
cancelChecker.cancel();
bar.dismiss();
});
}
/**
@@ -43,6 +43,7 @@ import com.itsaky.androidide.lsp.models.CompletionResult;
import com.itsaky.androidide.lsp.models.CompletionsKt;
import com.itsaky.androidide.lsp.models.MatchLevel;
import com.itsaky.androidide.progress.ICancelChecker;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -59,7 +60,7 @@ public interface ICompletionProvider {
/** Abort the completion process if cancelled. */
default void abortCompletionIfCancelled() {
final var checker = Lookup.getDefault().lookup(ICompletionCancelChecker.class);
final var checker = Lookup.getDefault().lookup(ICancelChecker.class);
if (checker != null) {
checker.abortIfCancelled();
}
@@ -50,6 +50,7 @@ import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.Project;
@@ -113,29 +114,32 @@ public interface ILanguageServer {
/**
* Compute code completions for the given completion params.
*
* @param params The completion params.
* @param params The completion params.
* @param cancelChecker
* @return The completion provider.
*/
@NonNull
CompletionResult complete(CompletionParams params);
CompletionResult complete(CompletionParams params, ICancelChecker cancelChecker);
/**
* Find references using the given params.
*
* @param params The params to use for computing references.
* @param params The params to use for computing references.
* @param cancelChecker
* @return The result of the computation.
*/
@NonNull
ReferenceResult findReferences(@NonNull ReferenceParams params);
ReferenceResult findReferences(@NonNull ReferenceParams params, ICancelChecker cancelChecker);
/**
* Find definition using the given params.
*
* @param params The params to use for computing the definition.
* @param params The params to use for computing the definition.
* @param cancelChecker
* @return The result of the computation.
*/
@NonNull
DefinitionResult findDefinition(@NonNull DefinitionParams params);
DefinitionResult findDefinition(@NonNull DefinitionParams params, ICancelChecker cancelChecker);
/**
* Request the server to provide an expanded selection range for the current selection.
@@ -20,7 +20,6 @@ package com.itsaky.androidide.lsp.java;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent;
@@ -60,6 +59,7 @@ import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.lsp.util.LSPEditorActions;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.FileManager;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
@@ -67,15 +67,13 @@ import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.utils.DocumentUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.VMUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class JavaLanguageServer implements ILanguageServer {
@@ -87,7 +85,7 @@ public class JavaLanguageServer implements ILanguageServer {
private Path selectedFile;
private final AnalyzeTimer timer = new AnalyzeTimer(this::analyzeSelected);
private CachedCompletion cachedCompletion;
private static final ILogger LOG = ILogger.newInstance("JavaLanguageServer");
public JavaLanguageServer() {
@@ -179,14 +177,14 @@ public class JavaLanguageServer implements ILanguageServer {
@NonNull
@Override
public CompletionResult complete(final CompletionParams params) {
public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (compiler == null
|| !settings.completionsEnabled()
|| !this.completionProvider.canComplete(params.getFile())) {
return CompletionResult.EMPTY;
}
if (diagnosticProvider.isAnalyzing()) {
LOG.warn("Cancelling source code analysis due to completion request");
diagnosticProvider.cancel();
@@ -199,24 +197,26 @@ public class JavaLanguageServer implements ILanguageServer {
@NonNull
@Override
public ReferenceResult findReferences(@NonNull ReferenceParams params) {
public ReferenceResult findReferences(@NonNull ReferenceParams params,
ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.referencesEnabled() || compiler == null) {
return new ReferenceResult(Collections.emptyList());
}
return new ReferenceProvider(compiler).findReferences(params);
return new ReferenceProvider(compiler, cancelChecker).findReferences(params);
}
@NonNull
@Override
public DefinitionResult findDefinition(@NonNull DefinitionParams params) {
public DefinitionResult findDefinition(@NonNull DefinitionParams params,
ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.definitionsEnabled() || compiler == null) {
return new DefinitionResult(Collections.emptyList());
}
return new DefinitionProvider(compiler, getSettings()).findDefinition(params);
return new DefinitionProvider(compiler, getSettings(), cancelChecker).findDefinition(params);
}
@NonNull
@@ -18,9 +18,9 @@
package com.itsaky.androidide.lsp.java.providers
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.api.ICompletionCancelChecker
import com.itsaky.androidide.lsp.api.IServerSettings
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.utils.ILogger
import java.nio.file.Path
@@ -34,12 +34,12 @@ abstract class BaseJavaServiceProvider(
protected val compiler: JavaCompilerService,
protected val settings: IServerSettings
) {
private val log = ILogger.newInstance(javaClass.simpleName)
/** Abort the completion if cancelled. */
fun abortCompletionIfCancelled() {
val checker = Lookup.getDefault().lookup(ICompletionCancelChecker::class.java)
val checker = Lookup.getDefault().lookup(ICancelChecker::class.java)
checker?.abortIfCancelled()
}
}
@@ -15,15 +15,14 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.api
package com.itsaky.androidide.lsp.java.providers
import com.itsaky.androidide.progress.ICancelChecker
/**
* Checks for cancellation of a process.
* A service provider which supports cancellation.
*
* @author Akash Yadav
*/
fun interface ICompletionCancelChecker {
/** Abort the process if it is cancelled. */
fun abortIfCancelled()
}
abstract class CancelableServiceProvider(cancelChecker: ICancelChecker) :
ICancelChecker by cancelChecker
@@ -18,9 +18,7 @@
package com.itsaky.androidide.lsp.java.providers;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.api.IServerSettings;
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
@@ -33,21 +31,21 @@ import com.itsaky.androidide.lsp.models.DefinitionParams;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.models.Location;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.utils.DocumentUtils;
import com.itsaky.androidide.utils.ILogger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import jdkx.lang.model.element.Element;
import jdkx.lang.model.element.TypeElement;
import jdkx.lang.model.type.TypeKind;
import jdkx.tools.JavaFileObject;
public class DefinitionProvider {
public class DefinitionProvider extends CancelableServiceProvider {
public static final List<Location> NOT_SUPPORTED = Collections.emptyList();
private static final ILogger LOG = ILogger.newInstance("JavaDefinitionProvider");
private final JavaCompilerService compiler;
@@ -56,7 +54,9 @@ public class DefinitionProvider {
private Position position;
private int line, column;
public DefinitionProvider(JavaCompilerService compiler, IServerSettings settings) {
public DefinitionProvider(JavaCompilerService compiler, IServerSettings settings,
ICancelChecker cancelChecker) {
super(cancelChecker);
this.compiler = compiler;
this.settings = settings;
}
@@ -76,9 +76,11 @@ public class DefinitionProvider {
}
public List<Location> findDefinition() {
abortIfCancelled();
final SynchronizedTask compile = compiler.compile(file);
abortIfCancelled();
final Element element =
compile.get(task -> NavigationHelper.findElement(task, file, line, column));
compile.get(task -> NavigationHelper.findElement(task, file, line, column, this));
if (element == null) {
LOG.error("Cannot find element at line:", line, "and column:", column);
@@ -88,9 +90,9 @@ public class DefinitionProvider {
IJavaDefinitionProvider provider = null;
if (element.asType().getKind() == TypeKind.ERROR) {
provider = new ErroneousDefinitionProvider(position, file, compiler, settings);
provider = new ErroneousDefinitionProvider(position, file, compiler, settings, this);
} else if (NavigationHelper.isLocal(element)) {
provider = new LocalDefinitionProvider(position, file, compiler, settings);
provider = new LocalDefinitionProvider(position, file, compiler, settings, this);
}
if (provider == null) {
@@ -108,10 +110,10 @@ public class DefinitionProvider {
final JavaFileObject jfo = optional.get();
if (DocumentUtils.isSameFile(Paths.get(jfo.toUri()), file)) {
provider = new LocalDefinitionProvider(position, file, compiler, settings);
provider = new LocalDefinitionProvider(position, file, compiler, settings, this);
} else {
provider =
new RemoteDefinitionProvider(position, file, compiler, settings).setOtherFile(jfo);
new RemoteDefinitionProvider(position, file, compiler, settings, this).setOtherFile(jfo);
}
}
@@ -120,6 +122,7 @@ public class DefinitionProvider {
private String className(Element element) {
while (element != null) {
abortIfCancelled();
if (element instanceof TypeElement) {
TypeElement type = (TypeElement) element;
return type.getQualifiedName().toString();
@@ -18,35 +18,36 @@
package com.itsaky.androidide.lsp.java.providers;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.CancelChecker;
import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
import com.itsaky.androidide.lsp.java.visitors.FindReferences;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.models.Location;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.util.TreePath;
import com.itsaky.androidide.progress.ICancelChecker;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import jdkx.lang.model.element.Element;
import jdkx.lang.model.element.TypeElement;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.util.TreePath;
public class ReferenceProvider {
public class ReferenceProvider extends CancelableServiceProvider {
public static final List<Location> NOT_SUPPORTED = Collections.emptyList();
private final CompilerProvider compiler;
private Path file;
private int line, column;
public ReferenceProvider(CompilerProvider compiler) {
public ReferenceProvider(CompilerProvider compiler, ICancelChecker checker) {
super(checker);
this.compiler = compiler;
}
@@ -57,60 +58,102 @@ public class ReferenceProvider {
// 1-based line and column indexes
this.line = params.getPosition().getLine() + 1;
this.column = params.getPosition().getColumn() + 1;
return new ReferenceResult(find());
List<Location> locations;
try {
locations = find();
} catch (Exception err) {
if (!CancelChecker.isCancelled(err)) {
throw err;
}
locations = new ArrayList<>();
}
return new ReferenceResult(locations);
}
public List<Location> find() {
abortIfCancelled();
final SynchronizedTask synchronizedTask = compiler.compile(file);
return synchronizedTask.get(
// findTypeReferences and findMemberReferences initiate another compilation task
// However, initiating a compilation task while another compilation is in progress will result in a deadlock
// Therefore, we return a supplier from the current synchronized task and
final Supplier<List<Location>> result = synchronizedTask.get(
task -> {
Element element = NavigationHelper.findElement(task, file, line, column);
if (element == null) return NOT_SUPPORTED;
if (NavigationHelper.isLocal(element)) {
return findReferences(task);
abortIfCancelled();
Element element = NavigationHelper.findElement(task, file, line, column, this);
if (element == null) {
return () -> NOT_SUPPORTED;
}
if (NavigationHelper.isLocal(element)) {
// findReferences method here uses the compilation task object
// however, finding the references lazily using supplier will leak this task
final var references = findReferences(task);
return () -> references;
}
if (NavigationHelper.isType(element)) {
TypeElement type = (TypeElement) element;
String className = type.getQualifiedName().toString();
task.close();
return findTypeReferences(className);
return () -> findTypeReferences(className);
}
if (NavigationHelper.isMember(element)) {
TypeElement parentClass = (TypeElement) element.getEnclosingElement();
String className = parentClass.getQualifiedName().toString();
String memberName = element.getSimpleName().toString();
final var parentClass = (TypeElement) element.getEnclosingElement();
final var className = parentClass.getQualifiedName().toString();
var memberName = element.getSimpleName().toString();
if (memberName.equals("<init>")) {
memberName = parentClass.getSimpleName().toString();
}
task.close();
return findMemberReferences(className, memberName);
String finalMemberName = memberName;
return () -> findMemberReferences(className, finalMemberName);
}
return NOT_SUPPORTED;
return () -> NOT_SUPPORTED;
});
return result.get();
}
private List<Location> findTypeReferences(String className) {
abortIfCancelled();
Path[] files = compiler.findTypeReferences(className);
if (files.length == 0) return Collections.emptyList();
if (files.length == 0) {
return Collections.emptyList();
}
abortIfCancelled();
return compiler.compile(files).get(this::findReferences);
}
private List<Location> findMemberReferences(String className, String memberName) {
Path[] files = compiler.findMemberReferences(className, memberName);
if (files.length == 0) return Collections.emptyList();
abortIfCancelled();
final var files = compiler.findMemberReferences(className, memberName);
if (files.length == 0) {
return Collections.emptyList();
}
abortIfCancelled();
return compiler.compile(files).get(this::findReferences);
}
private List<Location> findReferences(CompileTask task) {
Element element = NavigationHelper.findElement(task, file, line, column);
abortIfCancelled();
Element element = NavigationHelper.findElement(task, file, line, column, this);
List<TreePath> paths = new ArrayList<>();
for (CompilationUnitTree root : task.roots) {
abortIfCancelled();
new FindReferences(task.task, element).scan(root, paths);
}
List<Location> locations = new ArrayList<>();
for (TreePath p : paths) {
abortIfCancelled();
locations.add(FindHelper.location(task, p));
}
return locations;
}
}
}
@@ -24,6 +24,7 @@ import com.itsaky.androidide.lsp.java.providers.DefinitionProvider
import com.itsaky.androidide.lsp.java.utils.FindHelper
import com.itsaky.androidide.models.Location
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.utils.DocumentUtils.isSameFile
import openjdk.source.util.Trees
import java.nio.file.Path
@@ -41,8 +42,8 @@ class ErroneousDefinitionProvider(
position: Position,
completingFile: Path,
compiler: JavaCompilerService,
settings: IServerSettings,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings) {
settings: IServerSettings, cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
override fun doFindDefinition(element: Element): List<Location> {
val name = element.simpleName ?: return DefinitionProvider.NOT_SUPPORTED
@@ -54,6 +55,7 @@ class ErroneousDefinitionProvider(
private fun findAllMembers(className: String, memberName: String): List<Location> {
val otherFile = compiler.findAnywhere(className)
abortIfCancelled()
if (!otherFile.isPresent) {
log.error("Cannot find source file for class:", className)
return emptyList()
@@ -65,16 +67,20 @@ class ErroneousDefinitionProvider(
sources = listOf<JavaFileObject>(fileAsSource)
}
abortIfCancelled()
return compiler.compile(sources).get { task ->
val locations = mutableListOf<Location>()
val trees = Trees.instance(task.task)
val elements = task.task.elements
val parentClass = elements.getTypeElement(className)
abortIfCancelled()
for (member in elements.getAllMembers(parentClass)) {
if (!member.simpleName.contentEquals(memberName)) continue
val path = trees.getPath(member) ?: continue
val location = FindHelper.location(task, path, memberName)
abortIfCancelled()
locations.add(location)
}
@@ -23,9 +23,10 @@ import com.itsaky.androidide.lsp.java.providers.BaseJavaServiceProvider
import com.itsaky.androidide.lsp.java.providers.DefinitionProvider
import com.itsaky.androidide.models.Location
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.utils.ILogger
import java.nio.file.Path
import jdkx.lang.model.element.Element
import java.nio.file.Path
/**
* Provides definition for a specific symbol in Java source code.
@@ -36,8 +37,9 @@ abstract class IJavaDefinitionProvider(
protected val position: Position,
completingFile: Path,
compiler: JavaCompilerService,
settings: IServerSettings
) : BaseJavaServiceProvider(completingFile, compiler, settings) {
settings: IServerSettings,
cancelChecker: ICancelChecker
) : BaseJavaServiceProvider(completingFile, compiler, settings), ICancelChecker by cancelChecker {
protected val log = ILogger.newInstance(javaClass.simpleName)
@@ -22,6 +22,7 @@ import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.utils.FindHelper
import com.itsaky.androidide.models.Location
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.progress.ICancelChecker
import openjdk.source.util.Trees
import java.nio.file.Path
import jdkx.lang.model.element.Element
@@ -35,8 +36,8 @@ class LocalDefinitionProvider(
position: Position,
completingFile: Path,
compiler: JavaCompilerService,
settings: IServerSettings,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings) {
settings: IServerSettings, cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
override fun doFindDefinition(element: Element): List<Location> {
return compiler.compile(file).get {
@@ -52,6 +53,7 @@ class LocalDefinitionProvider(
name = element.enclosingElement.simpleName
}
abortIfCancelled()
return@get listOf(FindHelper.location(it, path, name))
}
}
@@ -23,6 +23,7 @@ import com.itsaky.androidide.lsp.java.compiler.SourceFileObject
import com.itsaky.androidide.lsp.java.utils.NavigationHelper
import com.itsaky.androidide.models.Location
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.progress.ICancelChecker
import java.nio.file.Path
import jdkx.lang.model.element.Element
import jdkx.tools.JavaFileObject
@@ -36,8 +37,8 @@ class RemoteDefinitionProvider(
position: Position,
completingFile: Path,
compiler: JavaCompilerService,
settings: IServerSettings,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings) {
settings: IServerSettings, cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
private lateinit var otherFile: JavaFileObject
@@ -48,7 +49,7 @@ class RemoteDefinitionProvider(
override fun doFindDefinition(element: Element): List<Location> {
// val task = compiler.compile(listOf(SourceFileObject(file), otherFile))
val provider = LocalDefinitionProvider(position, file, compiler, settings)
val provider = LocalDefinitionProvider(position, file, compiler, settings, this)
return provider.findDefinition(element)
// return provider
// .findDefinition(task.get { NavigationHelper.findElement(it, file, line, column) })
@@ -34,8 +34,10 @@
package com.itsaky.androidide.lsp.java.utils;
import androidx.annotation.Nullable;
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
import com.itsaky.androidide.lsp.java.visitors.FindNameAt;
import com.itsaky.androidide.progress.ICancelChecker;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.util.TreePath;
import openjdk.source.util.Trees;
@@ -47,12 +49,20 @@ import jdkx.lang.model.element.Modifier;
public class NavigationHelper {
public static Element findElement(CompileTask task, Path file, int line, int column) {
@Nullable
public static Element findElement(CompileTask task, Path file, int line, int column, ICancelChecker cancelChecker) {
Trees trees = Trees.instance(task.task);
for (CompilationUnitTree root : task.roots) {
if (cancelChecker != null) {
cancelChecker.abortIfCancelled();
}
if (root.getSourceFile().toUri().equals(file.toUri())) {
long cursor = root.getLineMap().getPosition(line, column);
TreePath path = new FindNameAt(task).scan(root, cursor);
if (cancelChecker != null) {
cancelChecker.abortIfCancelled();
}
if (path == null) {
return null;
}
@@ -81,7 +81,7 @@ class JavaCompletionProviderTest {
private fun completionTitles(pos: com.itsaky.androidide.models.Position): List<CharSequence> {
return JavaLSPTest.server
.complete(CompletionParams(pos, JavaLSPTest.file!!).apply { prefix = "" })
.complete(CompletionParams(pos, JavaLSPTest.file!!).apply { prefix = "" }, )
.items
.map { it.label }
}
@@ -43,6 +43,7 @@ import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider;
import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider;
import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.utils.DocumentUtils;
@@ -105,7 +106,7 @@ public class XMLLanguageServer implements ILanguageServer {
@NonNull
@Override
public CompletionResult complete(final CompletionParams params) {
public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) {
final ICompletionProvider completionProvider;
if (!getSettings().completionsEnabled()) {
completionProvider = new NoCompletionsProvider();
@@ -126,13 +127,15 @@ public class XMLLanguageServer implements ILanguageServer {
@NonNull
@Override
public ReferenceResult findReferences(@NonNull ReferenceParams params) {
public ReferenceResult findReferences(@NonNull ReferenceParams params,
ICancelChecker cancelChecker) {
return new ReferenceResult(Collections.emptyList());
}
@NonNull
@Override
public DefinitionResult findDefinition(@NonNull DefinitionParams params) {
public DefinitionResult findDefinition(@NonNull DefinitionParams params,
ICancelChecker cancelChecker) {
return new DefinitionResult(Collections.emptyList());
}
@@ -25,7 +25,7 @@ class CompletionHelperImpl : CompletionHelper {
override fun complete(transform: (CompletionItem) -> CharSequence): Pair<Boolean, List<CharSequence>> {
return XMLLSPTest.run {
val createCompletionParams = createCompletionParams()
val result = server.complete(createCompletionParams)
val result = server.complete(createCompletionParams, )
result.isIncomplete to
result.items
.filter { it.label != null }
@@ -17,25 +17,48 @@
package com.itsaky.androidide.progress
import java.util.concurrent.atomic.AtomicBoolean
/**
* Check whether a process is cancelled.
*
* @author Akash Yadav
*/
interface ICancelChecker {
/**
* Cancel this process.
*/
fun cancel()
/**
* Check whether this process has been cancelled or not.
*
* @return Whether the process has been cancelled.
*/
fun isCancelled(): Boolean
companion object {
/**
* Throw [ProcessCancelledException] if this process has been cancelled.
*/
@Throws(ProcessCancelledException::class)
fun abortIfCancelled()
internal class Default(private var cancelled: Boolean = false) : ICancelChecker {
open class Default(cancelled: Boolean = false) : ICancelChecker {
override fun cancel() {
cancelled = true
}
private val cancelled = AtomicBoolean(cancelled)
override fun isCancelled(): Boolean {
return cancelled
override fun cancel() {
cancelled.set(true)
}
override fun isCancelled(): Boolean {
return cancelled.get()
}
override fun abortIfCancelled() {
if (isCancelled()) {
throw ProcessCancelledException()
}
}
}
@@ -17,7 +17,7 @@
package com.itsaky.androidide.progress
import com.itsaky.androidide.progress.ICancelChecker.Companion.Default
import com.itsaky.androidide.progress.ICancelChecker.Default
import java.util.WeakHashMap
/**