mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Fixed: Lag caused due to a large number of diagnostic items in RecyclerView
This commit is contained in:
@@ -26,6 +26,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute,MediaCapabilities">
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<activity
|
||||
android:name=".CrashHandlerActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -241,6 +241,7 @@ public class EditorActivity extends StudioActivity
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
LOG.debug("Prepare options menu");
|
||||
ensureToolbarMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -33,16 +33,17 @@ import com.itsaky.lsp.models.DiagnosticSeverity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAdapter.VH> {
|
||||
|
||||
private final List<DiagnosticItem> diags;
|
||||
private final List<DiagnosticItem> diagnostics;
|
||||
private final File file;
|
||||
private final DiagnosticClickListener listener;
|
||||
|
||||
public DiagnosticItemAdapter(
|
||||
List<DiagnosticItem> diags, File file, DiagnosticClickListener listener) {
|
||||
this.diags = diags;
|
||||
List<DiagnosticItem> diagnostics, File file, DiagnosticClickListener listener) {
|
||||
this.diagnostics = Objects.requireNonNull(diagnostics);
|
||||
this.file = file;
|
||||
this.listener = listener;
|
||||
}
|
||||
@@ -56,7 +57,7 @@ public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAd
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(DiagnosticItemAdapter.VH p1, int p2) {
|
||||
final DiagnosticItem diagnostic = diags.get(p2);
|
||||
final DiagnosticItem diagnostic = diagnostics.get(p2);
|
||||
final LayoutDiagnosticItemBinding binding = p1.binding;
|
||||
|
||||
binding.icon.setImageResource(getDiagnosticIconId(diagnostic));
|
||||
@@ -81,7 +82,7 @@ public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAd
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return diags.size();
|
||||
return diagnostics.size();
|
||||
}
|
||||
|
||||
private int getDiagnosticIconId(DiagnosticItem diagnostic) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.graphics.PorterDuff;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -32,17 +33,19 @@ import com.itsaky.androidide.interfaces.DiagnosticClickListener;
|
||||
import com.itsaky.androidide.models.DiagnosticGroup;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.VH> {
|
||||
|
||||
private final List<DiagnosticGroup> diags;
|
||||
private final List<DiagnosticGroup> diagnostics;
|
||||
private final DiagnosticClickListener listener;
|
||||
|
||||
public DiagnosticsAdapter(List<DiagnosticGroup> diags, DiagnosticClickListener listener) {
|
||||
this.diags = diags;
|
||||
public DiagnosticsAdapter(List<DiagnosticGroup> diagnostics, DiagnosticClickListener listener) {
|
||||
this.diagnostics = Objects.requireNonNull(diagnostics);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DiagnosticsAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
|
||||
return new VH(
|
||||
@@ -51,7 +54,7 @@ public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(DiagnosticsAdapter.VH p1, int p2) {
|
||||
final DiagnosticGroup group = diags.get(p2);
|
||||
final DiagnosticGroup group = diagnostics.get(p2);
|
||||
final LayoutDiagnosticGroupBinding binding = p1.binding;
|
||||
|
||||
final int color =
|
||||
@@ -77,7 +80,7 @@ public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return diags.size();
|
||||
return diagnostics.size();
|
||||
}
|
||||
|
||||
public class VH extends RecyclerView.ViewHolder {
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
*/
|
||||
package com.itsaky.androidide.lsp;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.os.Trace;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.blankj.utilcode.util.FileIOUtils;
|
||||
@@ -57,10 +58,13 @@ import com.itsaky.toaster.Toaster;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.rosemoe.sora.lang.diagnostic.DiagnosticsContainer;
|
||||
@@ -75,6 +79,9 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
private final Map<File, List<DiagnosticItem>> diagnostics = new HashMap<>();
|
||||
protected EditorActivityProvider activityProvider;
|
||||
|
||||
public static final int MAX_DIAGNOSTIC_FILES = 10;
|
||||
public static final int MAX_DIAGNOSTIC_ITEMS_PER_FILE = 20;
|
||||
|
||||
private IDELanguageClientImpl(EditorActivityProvider provider) {
|
||||
setActivityProvider(provider);
|
||||
}
|
||||
@@ -153,8 +160,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
File file = result.getFile().toFile();
|
||||
if (!file.exists() || !file.isFile()) return;
|
||||
|
||||
LOG.debug("Updating diagnostics. Is main thread?", Looper.getMainLooper() == Looper.myLooper());
|
||||
|
||||
Trace.beginSection("publishDiagnostics");
|
||||
final var editorView = activity().getEditorForFile(file);
|
||||
if (editorView != null) {
|
||||
final var editor = editorView.getEditor();
|
||||
@@ -174,6 +180,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
|
||||
diagnostics.put(file, result.getDiagnostics());
|
||||
activity().setDiagnosticsAdapter(newDiagnosticsAdapter());
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -284,18 +291,81 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
});
|
||||
}
|
||||
|
||||
private List<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> diags) {
|
||||
List<DiagnosticGroup> groups = new ArrayList<>();
|
||||
if (diags == null || diags.size() <= 0) return groups;
|
||||
for (File file : diags.keySet()) {
|
||||
var fileDiags = diags.get(file);
|
||||
if (fileDiags == null || fileDiags.size() <= 0) continue;
|
||||
DiagnosticGroup group = new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiags);
|
||||
private List<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> map) {
|
||||
final var groups = new ArrayList<DiagnosticGroup>();
|
||||
var diagnosticMap = map;
|
||||
if (diagnosticMap == null || diagnosticMap.size() <= 0) return groups;
|
||||
|
||||
if (diagnosticMap.size() > 10) {
|
||||
LOG.warn("Limiting the diagnostics to 10 files");
|
||||
diagnosticMap = filterRelevantDiagnostics(map);
|
||||
}
|
||||
|
||||
for (File file : diagnosticMap.keySet()) {
|
||||
var fileDiagnostics = diagnosticMap.get(file);
|
||||
if (fileDiagnostics == null || fileDiagnostics.size() <= 0) continue;
|
||||
|
||||
// Trim the diagnostics list if we have too many diagnostic items.
|
||||
// Including a lot of diagnostic items will result in UI lag when they are shown
|
||||
if (fileDiagnostics.size() > MAX_DIAGNOSTIC_ITEMS_PER_FILE) {
|
||||
LOG.warn(
|
||||
"Limiting diagnostics to",
|
||||
MAX_DIAGNOSTIC_ITEMS_PER_FILE,
|
||||
"items for file",
|
||||
file.getName());
|
||||
|
||||
fileDiagnostics = fileDiagnostics.subList(0, MAX_DIAGNOSTIC_ITEMS_PER_FILE);
|
||||
}
|
||||
DiagnosticGroup group =
|
||||
new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiagnostics);
|
||||
groups.add(group);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Map<File, List<DiagnosticItem>> filterRelevantDiagnostics(
|
||||
@NonNull final Map<File, List<DiagnosticItem>> map) {
|
||||
final var result = new HashMap<File, List<DiagnosticItem>>();
|
||||
final var files = map.keySet();
|
||||
|
||||
// Diagnostics of files that are open must always be included
|
||||
final var relevantFiles = findOpenFiles(files, MAX_DIAGNOSTIC_FILES);
|
||||
|
||||
// If we can show a few more file diagnostics...
|
||||
if (relevantFiles.size() < MAX_DIAGNOSTIC_FILES) {
|
||||
final var alphabetical = new TreeSet<>(Comparator.comparing(File::getName));
|
||||
alphabetical.addAll(files);
|
||||
for (var file : alphabetical) {
|
||||
relevantFiles.add(file);
|
||||
if (relevantFiles.size() == MAX_DIAGNOSTIC_FILES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var file : relevantFiles) {
|
||||
result.put(file, map.get(file));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Set<File> findOpenFiles(final Set<File> files, final int max) {
|
||||
final var openedFiles = activity().getViewModel().getOpenedFiles();
|
||||
final var result = new TreeSet<File>();
|
||||
for (int i = 0; i < openedFiles.size(); i++) {
|
||||
final var opened = openedFiles.get(i);
|
||||
if (files.contains(opened)) {
|
||||
result.add(opened);
|
||||
}
|
||||
if (result.size() == max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void showAvailableQuickfixes(IDEEditor editor, List<CodeActionItem> actions) {
|
||||
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(activity());
|
||||
builder.setTitle(R.string.msg_code_actions);
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package com.itsaky.androidide.views.editor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
@@ -128,21 +127,21 @@ public class IDEEditor extends CodeEditor {
|
||||
* @param event The content change event.
|
||||
*/
|
||||
private void handleContentChange(ContentChangeEvent event, Unsubscribe unsubscribe) {
|
||||
if (getContext() instanceof Activity) {
|
||||
((Activity) getContext()).invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
if (getFile() == null || mLanguageServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var documentHandler = mLanguageServer.getDocumentHandler();
|
||||
final var file = getFile().toPath();
|
||||
if (documentHandler.accepts(file)) {
|
||||
documentHandler.onContentChange(new DocumentChangeEvent(file, getText(), mFileVersion + 1));
|
||||
}
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
final var documentHandler = mLanguageServer.getDocumentHandler();
|
||||
final var file = getFile().toPath();
|
||||
if (documentHandler.accepts(file)) {
|
||||
documentHandler.onContentChange(
|
||||
new DocumentChangeEvent(file, getText(), mFileVersion + 1));
|
||||
}
|
||||
|
||||
checkForSignatureHelp(event);
|
||||
checkForSignatureHelp(event);
|
||||
});
|
||||
}
|
||||
|
||||
public static int createInputFlags() {
|
||||
@@ -251,7 +250,6 @@ public class IDEEditor extends CodeEditor {
|
||||
* If any language server is set, requests signature help at the cursor's position. On a valid
|
||||
* response, shows the signature help in a popup window.
|
||||
*/
|
||||
@SuppressWarnings("unused") // accessed using reflection in CompletionItem class
|
||||
public void signatureHelp() {
|
||||
if (mLanguageServer != null && getFile() != null) {
|
||||
final CompletableFuture<SignatureHelp> future =
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,8 @@
|
||||
|
||||
package com.itsaky.lsp.java;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
@@ -90,7 +92,12 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
applySettings(getSettings());
|
||||
}
|
||||
|
||||
private void logIfMainThread(String methodName) {
|
||||
LOG.debug(methodName, ": Is main thread?", Looper.myLooper() == Looper.getMainLooper());
|
||||
}
|
||||
|
||||
private void analyzeSelected() {
|
||||
logIfMainThread("analyzeSelected");
|
||||
if (this.selectedFile == null) {
|
||||
return;
|
||||
}
|
||||
@@ -129,6 +136,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
|
||||
@NonNull
|
||||
private JavaCompilerService createCompiler() {
|
||||
logIfMainThread("createCompiler");
|
||||
return new JavaCompilerService(configuration.getClassPaths(), Collections.emptySet());
|
||||
}
|
||||
|
||||
@@ -197,6 +205,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
|
||||
@Override
|
||||
public void configurationChanged(Object newConfiguration) {
|
||||
logIfMainThread("configurationChanged");
|
||||
if (!(newConfiguration instanceof JavaServerConfiguration)) {
|
||||
LOG.error("Invalid configuration passed to server.", newConfiguration);
|
||||
LOG.error("Configuration change event will be ignored.");
|
||||
@@ -219,6 +228,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
@NonNull
|
||||
@Override
|
||||
public ICompletionProvider getCompletionProvider() {
|
||||
logIfMainThread("completionProvider");
|
||||
if (!settings.completionsEnabled()) {
|
||||
return new NoCompletionsProvider();
|
||||
}
|
||||
@@ -260,6 +270,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
@NonNull
|
||||
@Override
|
||||
public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
|
||||
logIfMainThread("signatureHelp");
|
||||
if (!settings.signatureHelpEnabled()) {
|
||||
return new SignatureHelp(Collections.emptyList(), -1, -1);
|
||||
}
|
||||
@@ -270,6 +281,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<DiagnosticItem> analyze(@NonNull Path file) {
|
||||
logIfMainThread("analyze file:" + file);
|
||||
if (!settings.codeAnalysisEnabled()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -301,6 +313,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
|
||||
@Override
|
||||
public void onFileOpened(DocumentOpenEvent event) {
|
||||
logIfMainThread("onFileOpened");
|
||||
onFileSelected(event.getOpenedFile());
|
||||
ensureAnalyzeTimerStarted();
|
||||
FileStore.open(event);
|
||||
@@ -308,6 +321,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
|
||||
@Override
|
||||
public void onContentChange(@NonNull DocumentChangeEvent event) {
|
||||
logIfMainThread("onContentChanged");
|
||||
// If a file's content is changed, it is definitely visible to user.
|
||||
onFileSelected(event.getChangedFile());
|
||||
ensureAnalyzeTimerStarted();
|
||||
@@ -321,6 +335,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
|
||||
@Override
|
||||
public void onFileClosed(DocumentCloseEvent event) {
|
||||
logIfMainThread("onFileClosed");
|
||||
FileStore.close(event);
|
||||
}
|
||||
|
||||
@@ -330,6 +345,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
}
|
||||
|
||||
private void ensureAnalyzeTimerStarted() {
|
||||
logIfMainThread("ensureAnalyzeTimerStarted");
|
||||
if (!this.analyzeTimer.isStarted()) {
|
||||
this.analyzeTimer.start();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user