diff --git a/app/build.gradle b/app/build.gradle index f8abfea27..46e12a203 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.application' -apply plugin: 'com.itsaky.androidide' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'stringfog' @@ -136,6 +135,8 @@ dependencies { implementation project(':emulator') implementation project(':emulatorview') implementation project(':javapoet') + + implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0' implementation fileTree(dir: 'libs', include: ['*.jar']) diff --git a/app/src/main/assets/data/jls.jar b/app/src/main/assets/data/jls.jar new file mode 100644 index 000000000..b631faa37 Binary files /dev/null and b/app/src/main/assets/data/jls.jar differ diff --git a/app/src/main/assets/data/jls.zip b/app/src/main/assets/data/jls.zip deleted file mode 100644 index a935d26ac..000000000 Binary files a/app/src/main/assets/data/jls.zip and /dev/null differ diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java index b2784c23b..db358929c 100644 --- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java +++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java @@ -31,6 +31,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.navigation.NavigationView; import com.google.android.material.tabs.TabLayout; import com.google.android.material.transition.MaterialContainerTransform; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.itsaky.androidide.adapters.EditorPagerAdapter; import com.itsaky.androidide.app.StudioActivity; @@ -48,8 +50,11 @@ import com.itsaky.androidide.handlers.IDEHandler; import com.itsaky.androidide.handlers.jls.JLSHandler; import com.itsaky.androidide.handlers.jls.LanguageClientHandler; import com.itsaky.androidide.interfaces.DiagnosticClickListener; +import com.itsaky.androidide.interfaces.EditorActivityProvider; import com.itsaky.androidide.language.buildout.BuildOutputLanguage; import com.itsaky.androidide.language.logs.LogLanguageImpl; +import com.itsaky.androidide.lsp.LSP; +import com.itsaky.androidide.lsp.LSPProvider; import com.itsaky.androidide.managers.PreferenceManager; import com.itsaky.androidide.managers.VersionedFileManager; import com.itsaky.androidide.models.AndroidProject; @@ -85,10 +90,16 @@ import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Stack; import java.util.regex.Pattern; import me.piruin.quickaction.ActionItem; import me.piruin.quickaction.QuickAction; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.Position; +import com.itsaky.androidide.utils.LSPUtils; public class EditorActivity extends StudioActivity implements FileTreeFragment.FileActionListener, TabLayout.OnTabSelectedListener, @@ -96,7 +107,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F DiagnosticClickListener, EditorFragment.FileOpenListener, EditorPagerAdapter.EditorStateListener, - IDEHandler.Provider { + IDEHandler.Provider, + EditorActivityProvider { private ActivityEditorBinding mBinding; private EditorPagerAdapter mPagerAdapter; @@ -131,6 +143,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F private static final String TAG_FILE_OPTIONS_FRAGMENT = "file_options_fragment"; public static final String TAG = "EditorActivity"; + + private static final org.eclipse.lsp4j.Range Range_ofZero = new org.eclipse.lsp4j.Range(new Position(0, 0), new Position(0, 0)); private static final int ACTIONID_CLOSE = 100; private static final int ACTIONID_OTHERS = 101; @@ -204,6 +218,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F registerLogReceiver(); setupContainers(); setupSignatureText(); + + startLanguageServers(); } @Override @@ -380,6 +396,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F invalidateOptionsMenu(); return true; } + + @Override + public EditorActivity provide() { + return this; + } @Override public EditorActivity provideEditorActivity() { @@ -592,7 +613,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F @Override public void onDiagnosticClick(File file, Diagnostic diagnostic) { - openFileAndSelect(file, diagnostic.range); +// openFileAndSelect(file, diagnostic.getRange()); hideDiagnostics(); } @@ -639,16 +660,16 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F } } - public void openFileAndSelect(File file, Range range) { + public void openFileAndSelect(File file, org.eclipse.lsp4j.Range range) { openFile(file, range); EditorFragment opened = mPagerAdapter.findEditorByFile(file); if (opened != null && opened.getEditor() != null) { CodeEditor editor = opened.getEditor(); editor.post(() -> { - if (range.start.equals(range.end)) { - editor.setSelection(range.start.line, range.start.column); + if (LSPUtils.isEqual(range.getStart(), range.getEnd())) { + editor.setSelection(range.getStart().getLine(), range.getEnd().getCharacter()); } else { - editor.setSelectionRegion(range.start.line, range.start.column, range.end.line, range.end.column); + editor.setSelectionRegion(range.getStart().getLine(), range.getStart().getCharacter(), range.getEnd().getLine(), range.getEnd().getCharacter()); } }); } @@ -867,8 +888,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F openFile(file, null); } - public EditorFragment openFile(File file, Range selection) { - if(selection == null) selection = Range.ofZero(); + public EditorFragment openFile(File file, org.eclipse.lsp4j.Range selection) { + if(selection == null) selection = Range_ofZero; int i = mPagerAdapter.openFile(file, selection, this, mJLSHandler); if(i >= 0 && !mBinding.tabs.getTabAt(i).isSelected()) mBinding.tabs.getTabAt(i).select(); @@ -935,7 +956,27 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F public void createServices() { new TaskExecutor().executeAsync(() -> { getApp().createCompletionService(); - mJLSHandler.start(); + + LanguageServer javaServer = LSPProvider.getServerForLanguage(LSPProvider.LANGUAGE_JAVA); + if(javaServer == null) return null; + + List cps = mProject.getClassPaths(); + JsonObject settings = new JsonObject(); + JsonObject java = new JsonObject(); + JsonArray classPath = new JsonArray(); + + for(int i=0;i { setStatus(getString(getApp().areCompletorsStarted() ? R.string.msg_service_started : R.string.msg_starting_completion_failed)); @@ -997,6 +1038,19 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F ////////////// PRIVATE APIS ///////////////////// ///////////////////////////////////////////////// + private void startLanguageServers() { + LSP.Java.start(() -> { + Optional result = LSP.Java.init(mProject.getProjectPath()); + + if(result.isPresent()) { + // TODO Handle initialize result + // TODO Register server capablities + } else if(true) { + // TODO Show user that we cant initialize Java language server + } + }); + } + private void getProjectFromIntent() { this.mProject = getIntent().getParcelableExtra(EXTRA_PROJECT); getApp().getPrefManager().setOpenedProject(this.mProject.getProjectPath()); diff --git a/app/src/main/java/com/itsaky/androidide/adapters/CompletionListAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/CompletionListAdapter.java index 314a60c47..f587f2185 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/CompletionListAdapter.java +++ b/app/src/main/java/com/itsaky/androidide/adapters/CompletionListAdapter.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import android.widget.TextView; import androidx.core.content.ContextCompat; import com.blankj.utilcode.util.ThreadUtils; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -18,7 +19,6 @@ import com.itsaky.androidide.databinding.LayoutCompletionItemBinding; import com.itsaky.androidide.models.CompletionItemWrapper; import com.itsaky.androidide.models.CompletionListItem; import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; import com.itsaky.androidide.utils.Logger; import com.itsaky.androidide.utils.TypefaceUtils; import com.itsaky.apiinfo.ApiInfo; @@ -26,10 +26,10 @@ import com.itsaky.apiinfo.models.ClassInfo; import com.itsaky.apiinfo.models.FieldInfo; import com.itsaky.apiinfo.models.Info; import com.itsaky.apiinfo.models.MethodInfo; -import com.itsaky.lsp.CompletionItemKind; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.widget.EditorCompletionAdapter; import java.util.Locale; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; public class CompletionListAdapter extends EditorCompletionAdapter { @@ -44,23 +44,10 @@ public class CompletionListAdapter extends EditorCompletionAdapter { protected View getView(int position, View convertView, ViewGroup parent, boolean isCurrentCursorPosition) { LayoutCompletionItemBinding binding = LayoutCompletionItemBinding.inflate(LayoutInflater.from(getContext()), parent, false); - Either e = getItem(position); - String label = "", desc = "", type = ""; - String header = ""; - if (e != null && e.isLeft()) { - SuggestItem s = e.getLeft(); - label = s.getName(); - desc = s.getDescription(); - type = s.getReturnType(); - header = String.valueOf(s.getTypeHeader()).toUpperCase(Locale.US); - } else if (e != null && e.isRight()) { - CompletionListItem item = (CompletionListItem) e.getRight(); - header = item.getType() == null ? CompletionListItem.Type.OBJECT.getAsString() : item.getType().getAsString(); - label = item.getLabel(); - type = item.getItemType().contains(".") ? item.getItemType().substring(item.getItemType().lastIndexOf(".") + 1) : item.getItemType(); - desc = item.getType() != CompletionListItem.Type.NOT_IMPORTED_CLASS ? item.getDetail() : getContext().getString(R.string.label_not_imported, item.getDetail()); - } - + CompletionItem item = getItem(position); + String label = item.getLabel(), desc = item.getDetail(), type = item.getKind().toString(); + String header = type == null || type.length() <= 0 ? "O" : String.valueOf(type.toString().charAt(0)); + binding.completionIconText.setText(header); binding.completionLabel.setText(label); binding.completionType.setText(type); @@ -75,8 +62,8 @@ public class CompletionListAdapter extends EditorCompletionAdapter { binding.completionApiInfo.setVisibility(View.GONE); - if (e != null && e.isLeft() && e.getLeft() instanceof CompletionItemWrapper) { - showApiInfoIfNeeded((CompletionItemWrapper) e.getLeft(), binding.completionApiInfo); + if (item != null) { + showApiInfoIfNeeded(item, binding.completionApiInfo); } binding.getRoot().getViewTreeObserver().addOnGlobalLayoutListener(() -> { @@ -89,22 +76,22 @@ public class CompletionListAdapter extends EditorCompletionAdapter { return binding.getRoot(); } - private void showApiInfoIfNeeded(final CompletionItemWrapper item, final TextView completionApiInfo) { + private void showApiInfoIfNeeded(final CompletionItem item, final TextView completionApiInfo) { new Thread(() -> { final ApiInfo info = StudioApp.getInstance().getApiInfo(); boolean hasRead = info != null && info.hasRead(); - boolean isValid = isValidForApiVersion(item.getItem()); + boolean isValid = isValidForApiVersion(item); if (hasRead && isValid) { - JsonElement element = item.getItem().data; + JsonElement element = new Gson().toJsonTree(item.getData()); if (element == null || !element.isJsonObject()) return; JsonObject data = element.getAsJsonObject(); if (!data.has("className")) return; final String className = data.get("className").getAsString(); - int kind = item.getItem().kind; + CompletionItemKind kind = item.getKind(); ClassInfo clazz = info.getClassByName(className); if(clazz == null) return; @@ -168,10 +155,10 @@ public class CompletionListAdapter extends EditorCompletionAdapter { }).start(); } - private boolean isValidForApiVersion(com.itsaky.lsp.CompletionItem item) { + private boolean isValidForApiVersion(CompletionItem item) { if (item == null) return false; - final int type = item.kind; - JsonElement element = item.data; + final CompletionItemKind type = item.getKind(); + JsonElement element = new Gson().toJsonTree(item.getData()); if ( /** diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java index d12ac0729..9b049f8fb 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java +++ b/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java @@ -66,7 +66,7 @@ public class EditorPagerAdapter extends FragmentStatePagerAdapter implements Edi return null; } - public int openFile(File file, Range selection, EditorFragment.FileOpenListener listener, JLSRequestor jlsRequestor) { + public int openFile(File file, org.eclipse.lsp4j.Range selection, EditorFragment.FileOpenListener listener, JLSRequestor jlsRequestor) { int openedFileIndex = -1; for (int i=0;i < mOpenedFiles.size();i++) { File f = mOpenedFiles.get(i); diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java index 638973dde..cbfc40b5a 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java @@ -44,6 +44,7 @@ import java.util.Map; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.Token; import io.github.rosemoe.editor.text.Content; +import com.itsaky.androidide.utils.LSPUtils; public class EditorFragment extends BaseFragment implements EditorEventListener { @@ -104,13 +105,13 @@ public class EditorFragment extends BaseFragment implements EditorEventListener } } - public static EditorFragment newInstance(File file, AndroidProject project, Range selection) { + public static EditorFragment newInstance(File file, AndroidProject project, org.eclipse.lsp4j.Range selection) { Bundle bundle = new Bundle(); bundle.putString(KEY_FILE_PATH, file.getAbsolutePath()); - bundle.putInt(KEY_LINE_START, selection.start.line); - bundle.putInt(KEY_LINE_END, selection.end.line); - bundle.putInt(KEY_COLUMN_START, selection.start.column); - bundle.putInt(KEY_COLUMN_END, selection.end.column); + bundle.putInt(KEY_LINE_START, selection.getStart().getLine()); + bundle.putInt(KEY_LINE_END, selection.getEnd().getLine()); + bundle.putInt(KEY_COLUMN_START, selection.getStart().getCharacter()); + bundle.putInt(KEY_COLUMN_END, selection.getEnd().getCharacter()); bundle.putParcelable(KEY_PROJECT, project); EditorFragment frag = new EditorFragment(); frag.setArguments(bundle); @@ -164,32 +165,32 @@ public class EditorFragment extends BaseFragment implements EditorEventListener configureEditorIfNeeded(); - final Range range = fromArgs(getArguments()); + final org.eclipse.lsp4j.Range range = fromArgs(getArguments()); new TaskExecutor().executeAsync(new ReadFileTask(mFile), result -> { mBinding.editor.setText(result); postRead(); mBinding.editor.post(() -> { - if(range.start.equals(range.end)) { - getEditor().setSelection(range.start.line, range.start.column); + if(LSPUtils.isEqual(range.getStart(), range.getEnd())) { + getEditor().setSelection(range.getStart().getLine(), range.getStart().getCharacter()); } else { - getEditor().setSelectionRegion(range.start.line, range.start.column, range.end.line, range.end.column); + getEditor().setSelectionRegion(range.getStart().getLine(), range.getStart().getCharacter(), range.getEnd().getLine(), range.getEnd().getCharacter()); } }); }); } - private Range fromArgs(Bundle args) { + private org.eclipse.lsp4j.Range fromArgs(Bundle args) { if(!(args.containsKey(KEY_LINE_START) && args.containsKey(KEY_COLUMN_START) && args.containsKey(KEY_LINE_END) && args.containsKey(KEY_COLUMN_END))) - return Range.ofZero(); + return LSPUtils.Range_ofZero; - return new Range( - new Position( + return new org.eclipse.lsp4j.Range( + new org.eclipse.lsp4j.Position( args.getInt(KEY_LINE_START), args.getInt(KEY_COLUMN_START)), - new Position( + new org.eclipse.lsp4j.Position( args.getInt(KEY_LINE_END), args.getInt(KEY_COLUMN_END) )); @@ -302,7 +303,6 @@ public class EditorFragment extends BaseFragment implements EditorEventListener } private void postRead() { - mBinding.editor.setFile(getFile()); if (mFile.isFile() && mFile.getName().endsWith(EXT_JAVA)) { mBinding.editor.setEditorLanguage(mJavaLanguage = new JavaLanguage(mProject)); } else if (mFile.isFile() && mFile.getName().endsWith(EXT_XML)) { @@ -312,6 +312,11 @@ public class EditorFragment extends BaseFragment implements EditorEventListener } else { mBinding.editor.setEditorLanguage(new EmptyLanguage()); } + + // File must be set only after setting the language server + // This will make sure that textDocument/didOpen is sent + mBinding.editor.setFile(getFile()); + mBinding.editor.setColorScheme(new SchemeAndroidIDE()); isRead = true; if(mOpenListener != null) @@ -427,16 +432,7 @@ public class EditorFragment extends BaseFragment implements EditorEventListener } private void notifySaved(boolean wrote, String text) { - if (wrote && mJLSRequestor != null) { - try { - TextDocumentIdentifier id = new TextDocumentIdentifier(); - id.uri = mFile.toURI(); - DidSaveTextDocumentParams p = new DidSaveTextDocumentParams(); - p.text = text; - p.textDocument = id; - mJLSRequestor.didSave(p); - } catch (Throwable th) {} - } + mBinding.editor.didSave(); if(mModificationStateListener != null) mModificationStateListener.onSaved(this); diff --git a/app/src/main/java/com/itsaky/androidide/handlers/jls/JLSHandler.java b/app/src/main/java/com/itsaky/androidide/handlers/jls/JLSHandler.java index 9a501f08f..53df45e50 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/jls/JLSHandler.java +++ b/app/src/main/java/com/itsaky/androidide/handlers/jls/JLSHandler.java @@ -65,7 +65,7 @@ public class JLSHandler extends IDEHandler implements JLSRequestor { this.client.start(); this.languageServer = new JavaLanguageServer(project, this.client); - this.languageServer.startServer(); +// this.languageServer.startServer(); StudioApp.getInstance().setJavaLanguageServer(languageServer); } diff --git a/app/src/main/java/com/itsaky/androidide/handlers/jls/LanguageClientHandler.java b/app/src/main/java/com/itsaky/androidide/handlers/jls/LanguageClientHandler.java index 61ddc8687..e33990288 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/jls/LanguageClientHandler.java +++ b/app/src/main/java/com/itsaky/androidide/handlers/jls/LanguageClientHandler.java @@ -6,7 +6,6 @@ import android.view.View; import com.blankj.utilcode.util.FileIOUtils; import com.blankj.utilcode.util.ThrowableUtils; import com.google.gson.JsonElement; -import com.itsaky.androidide.EditorActivity; import com.itsaky.androidide.R; import com.itsaky.androidide.adapters.DiagnosticsAdapter; import com.itsaky.androidide.adapters.SearchListAdapter; @@ -24,7 +23,6 @@ import com.itsaky.lsp.Location; import com.itsaky.lsp.Message; import com.itsaky.lsp.ParameterInformation; import com.itsaky.lsp.PublishDiagnosticsParams; -import com.itsaky.lsp.Range; import com.itsaky.lsp.ShowMessageParams; import com.itsaky.lsp.SignatureHelp; import com.itsaky.lsp.SignatureInformation; @@ -82,31 +80,7 @@ public class LanguageClientHandler extends IDEHandler implements LanguageClient activity().getApp().toast(R.string.msg_no_definition, Toaster.Type.ERROR); return; } - Location loc = locations.get(0); - final File file = new File(loc.uri); - final Range range = loc.range; - try { - if(activity().getPagerAdapter() == null) return; - if(activity().getPagerAdapter().getCount() <= 0) { - activity().openFile(file, range); - return; - } - EditorFragment frag = activity().getPagerAdapter().getFrag(activity().getBinding().tabs.getSelectedTabPosition()); - if(frag != null - && frag.getFile() != null - && frag.getEditor() != null - && frag.getFile().getAbsolutePath().equals(file.getAbsolutePath())) { - if(range.start.equals(range.end)) { - frag.getEditor().setSelection(range.start.line, range.start.column); - } else { - frag.getEditor().setSelectionRegion(range.start.line, range.start.column, range.end.line, range.end.column); - } - } else { - activity().openFileAndSelect(file, range); - } - } catch (Throwable th) { - LOG.error(ThrowableUtils.getFullStackTrace(th)); - } + } @Override @@ -125,36 +99,36 @@ public class LanguageClientHandler extends IDEHandler implements LanguageClient } final Map> results = new HashMap<>(); - for(int i=0;i matches = results.containsKey(file) ? results.get(file) : new ArrayList<>(); - matches.add( - new SearchResult( - loc.range, - file, - content.getLineString(loc.range.start.line), - content.subContent( - loc.range.start.line, - loc.range.start.column, - loc.range.end.line, - loc.range.end.column - ).toString() - ) - ); - results.put(file, matches); - } catch (Throwable th) { - LOG.error(ThrowableUtils.getFullStackTrace(th)); - } - } +// for(int i=0;i matches = results.containsKey(file) ? results.get(file) : new ArrayList<>(); +// matches.add( +// new SearchResult( +// loc.range, +// file, +// content.getLineString(loc.range.start.line), +// content.subContent( +// loc.range.start.line, +// loc.range.start.column, +// loc.range.end.line, +// loc.range.end.column +// ).toString() +// ) +// ); +// results.put(file, matches); +// } catch (Throwable th) { +// LOG.error(ThrowableUtils.getFullStackTrace(th)); +// } +// } activity().handleSearchResults(results); } diff --git a/app/src/main/java/com/itsaky/androidide/interfaces/EditorActivityProvider.java b/app/src/main/java/com/itsaky/androidide/interfaces/EditorActivityProvider.java new file mode 100644 index 000000000..26ff78ff1 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/interfaces/EditorActivityProvider.java @@ -0,0 +1,12 @@ +package com.itsaky.androidide.interfaces; + +import com.itsaky.androidide.EditorActivity; + +public interface EditorActivityProvider { + + /** + * Provides a reference to {@link com.itsaky.androidide.EditorActivity EditorActivity}. + * This is used by LanguageClients to get reference to EditorActivity without keeping a strong reference. + */ + EditorActivity provide(); +} diff --git a/app/src/main/java/com/itsaky/androidide/language/buildout/BuildOutputLanguage.java b/app/src/main/java/com/itsaky/androidide/language/buildout/BuildOutputLanguage.java index 3eaa8feb6..3aa696c15 100644 --- a/app/src/main/java/com/itsaky/androidide/language/buildout/BuildOutputLanguage.java +++ b/app/src/main/java/com/itsaky/androidide/language/buildout/BuildOutputLanguage.java @@ -11,6 +11,7 @@ import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; import io.github.rosemoe.editor.interfaces.NewlineHandler; import io.github.rosemoe.editor.widget.SymbolPairMatch; import io.github.rosemoe.editor.langs.EmptyLanguage; +import org.eclipse.lsp4j.services.LanguageServer; public class BuildOutputLanguage extends BaseLanguage { @@ -19,6 +20,16 @@ public class BuildOutputLanguage extends BaseLanguage { return new EmptyLanguage.EmptyAutoCompleteProvider(); } + @Override + public LanguageServer getLanguageServer() { + return null; + } + + @Override + public String getLanguageCode() { + return null; + } + @Override public boolean isAutoCompleteChar(char ch) { return false; diff --git a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAutoComplete.java b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAutoComplete.java index 8c3fba816..099471a9b 100644 --- a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAutoComplete.java +++ b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAutoComplete.java @@ -2,51 +2,54 @@ package com.itsaky.androidide.language.groovy; import com.itsaky.androidide.models.GradleSuggestItem; import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.TextAnalyzeResult; import io.github.rosemoe.editor.widget.CodeEditor; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.CompletionItemKind; public class GroovyAutoComplete implements AutoCompleteProvider { private static final List ANDROIDX_ARTIFACTS = createArtifacts(); private static final List CONFIGURATIONS = createConfigs(); private static final List OTHERS = createOtherCompletions(); - - @Override - public List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) { - List> result = new ArrayList<>(); - - for(String artifact : ANDROIDX_ARTIFACTS) { - if(!artifact.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; - SuggestItem item = createCompletionItem(artifact, prefix, true); - Either either = Either.forLeft(item); - result.add(either); - } - - for(String config : CONFIGURATIONS) { - if(!config.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; - SuggestItem item = createCompletionItem(config, prefix, false); - Either either = Either.forLeft(item); - result.add(either); - } - - for(String other : OTHERS) { - if(!other.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; - SuggestItem item = createCompletionItem(other, prefix, false); - Either either = Either.forLeft(item); - result.add(either); - } - - return result; - } - private SuggestItem createCompletionItem(String itemLabel, String prefix, boolean artifact) { - return new GradleSuggestItem(itemLabel, "", prefix, artifact); + @Override + public List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception { + List result = new ArrayList<>(); + + for(String artifact : ANDROIDX_ARTIFACTS) { + if(!artifact.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; + result.add(createCompletionItem(artifact, prefix, true)); + } + + for(String config : CONFIGURATIONS) { + if(!config.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; + result.add(createCompletionItem(config, prefix, false)); + } + + for(String other : OTHERS) { + if(!other.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue; + result.add(createCompletionItem(other, prefix, false)); + } + + return result; + } + + private CompletionItem createCompletionItem(String itemLabel, String prefix, boolean artifact) { + CompletionItem item = new CompletionItem(); + item.setLabel(itemLabel); + item.setDetail(""); + item.setInsertText(itemLabel); + item.setSortText("0" + itemLabel); + item.setInsertTextFormat(InsertTextFormat.PlainText); + item.setKind(CompletionItemKind.Text); + return item; } private static List createArtifacts() { diff --git a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java index aa7b7a83a..17773c2a7 100644 --- a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java +++ b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java @@ -3,6 +3,7 @@ package com.itsaky.androidide.language.groovy; import com.itsaky.androidide.language.BaseLanguage; import com.itsaky.androidide.language.groovy.lexer.GroovyLexer; import com.itsaky.androidide.language.java.manager.JavaCharacter; +import com.itsaky.androidide.lsp.LSPProvider; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; import io.github.rosemoe.editor.interfaces.CodeAnalyzer; import io.github.rosemoe.editor.interfaces.NewlineHandler; @@ -11,6 +12,7 @@ import io.github.rosemoe.editor.widget.SymbolPairMatch; import java.io.StringReader; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.services.LanguageServer; public class GroovyLanguage extends BaseLanguage { @@ -21,6 +23,16 @@ public class GroovyLanguage extends BaseLanguage { analyzer = new GroovyAnalyzer(); completor = new GroovyAutoComplete(); } + + @Override + public LanguageServer getLanguageServer() { + return null; + } + + @Override + public String getLanguageCode() { + return LSPProvider.LANGUAGE_GROOVY; + } @Override public CodeAnalyzer getAnalyzer() { diff --git a/app/src/main/java/com/itsaky/androidide/language/java/JavaAutoComplete.java b/app/src/main/java/com/itsaky/androidide/language/java/JavaAutoComplete.java index cad448cb7..540016cc7 100644 --- a/app/src/main/java/com/itsaky/androidide/language/java/JavaAutoComplete.java +++ b/app/src/main/java/com/itsaky/androidide/language/java/JavaAutoComplete.java @@ -1,67 +1,77 @@ package com.itsaky.androidide.language.java; -import androidx.core.util.Pair; -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.language.java.JavaLanguageAnalyzer; -import com.itsaky.androidide.language.java.server.JavaLanguageServer; -import com.itsaky.androidide.models.CompletionItemWrapper; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; -import com.itsaky.lsp.CompletionList; -import com.itsaky.lsp.Position; -import com.itsaky.lsp.TextDocumentIdentifier; -import com.itsaky.lsp.TextDocumentPositionParams; +import com.itsaky.androidide.utils.Logger; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.TextAnalyzeResult; import io.github.rosemoe.editor.widget.CodeEditor; import java.util.ArrayList; -import java.util.List; import java.util.Collections; import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageServer; +import com.itsaky.androidide.utils.JSONUtility; public class JavaAutoComplete implements AutoCompleteProvider { - private JavaLanguageAnalyzer analyzer; - private int lastId = -1; + private JavaLanguage language; + private CompletableFuture, CompletionList>> future; - public JavaAutoComplete(JavaLanguageAnalyzer analyzer) { - this.analyzer = analyzer; + public JavaAutoComplete(JavaLanguage lang) { + this.language = lang; } @Override - public List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) { - final JavaLanguageServer server = StudioApp.getInstance().getJavaLanguageServer(); - if(server != null) { - if(lastId != -1) server.cancelRequest(lastId); - TextDocumentIdentifier id = new TextDocumentIdentifier(); - id.uri = editor.getFile().toURI(); - TextDocumentPositionParams p = new TextDocumentPositionParams(); - p.position = new Position(line, editor.getCursor().getLeftColumn()); - p.textDocument = id; - Pair response = server.completion(p); - lastId = response.first; - CompletionList list = response.second; - if(list != null && list.items != null) { - List> result = new ArrayList<>(); - for(int i=0;i either = Either.forLeft(new CompletionItemWrapper(item, prefix)); - result.add(either); - } - Collections.sort(result, RESULT_SORTER); - return result; + public List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception { + LanguageServer languageServer = language.getLanguageServer(); + if(languageServer != null && fileUri != null) { + + if(future != null && !future.isDone()) future.cancel(true); + + CompletionParams params = new CompletionParams(); + params.setPosition(new Position(line, column)); + params.setTextDocument(new TextDocumentIdentifier(fileUri)); + future = languageServer.getTextDocumentService().completion(params); + + if(future.isCancelled()) return new ArrayList(); + + try { + Either, CompletionList> either = future.get(); + if(either.isLeft()) + return sort(either.getLeft()); + + if(either.isRight()) + return sort(either.getRight().getItems()); + + } catch (Throwable th) { + LOG.error("Java completion error: ", th); + // Ignore this exception. An empty list will be sent after this + // This exeption is thrown probably because the request was cancelled or interrupted } } - return new ArrayList>(); + return new ArrayList(); } - private static final Comparator> RESULT_SORTER = new Comparator>(){ - + private List sort(List items) { + Collections.sort(items, RESULT_SORTER); + return items; + } + + private static final Comparator RESULT_SORTER = new Comparator(){ + @Override - public int compare(Either p1, Either p2) { - return p1.getLeft().getSortText().compareTo(p2.getLeft().getSortText()); + public int compare(CompletionItem p1, CompletionItem p2) { + String s1 = p1.getSortText() == null ? p1.getLabel() : p1.getSortText(); + String s2 = p2.getSortText() == null ? p2.getLabel() : p2.getSortText(); + return s1.compareTo(s2); } }; + + private static final Logger LOG = Logger.instance("JavaAutoComplete"); } diff --git a/app/src/main/java/com/itsaky/androidide/language/java/JavaLanguage.java b/app/src/main/java/com/itsaky/androidide/language/java/JavaLanguage.java index a4739c566..a78834d06 100644 --- a/app/src/main/java/com/itsaky/androidide/language/java/JavaLanguage.java +++ b/app/src/main/java/com/itsaky/androidide/language/java/JavaLanguage.java @@ -4,6 +4,7 @@ import com.itsaky.androidide.language.BaseLanguage; import com.itsaky.androidide.language.java.manager.JavaCharacter; import com.itsaky.androidide.language.java.parser.JavaLexer; import com.itsaky.androidide.language.java.parser.JavaParser; +import com.itsaky.androidide.lsp.LSPProvider; import com.itsaky.androidide.models.AndroidProject; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; import io.github.rosemoe.editor.interfaces.CodeAnalyzer; @@ -13,6 +14,7 @@ import io.github.rosemoe.editor.widget.SymbolPairMatch; import java.io.StringReader; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.services.LanguageServer; public class JavaLanguage extends BaseLanguage { @@ -22,10 +24,20 @@ public class JavaLanguage extends BaseLanguage { public JavaLanguage(AndroidProject project) { this.analyzer = new JavaLanguageAnalyzer(); - this.complete = new JavaAutoComplete(analyzer); + this.complete = new JavaAutoComplete(this); this.project = project; } + @Override + public LanguageServer getLanguageServer() { + return LSPProvider.getServerForLanguage(LSPProvider.LANGUAGE_JAVA); + } + + @Override + public String getLanguageCode() { + return LSPProvider.LANGUAGE_JAVA; + } + @Override public CodeAnalyzer getAnalyzer() { return analyzer; @@ -105,19 +117,6 @@ public class JavaLanguage extends BaseLanguage { return new HandleResult(sb, shiftLeft); } } - - public static String[] getKeywords() { - return new String[] { - "abstract", "assert", "boolean", "break", "byte", "case", - "catch", "char", "class", "const", "continue", "default", - "do", "double", "else", "enum", "extends", "final", "finally", - "float", "for", "if", "goto", "implements", "import", "instanceof", - "int", "interface", "long", "native", "new", "package", "private", - "protected", "public", "return", "short", "static", "strictfp", - "super", "switch", "synchronized", "this", "throw", "throws", - "transient", "try", "void", "volatile", "while", "null" - }; - } private class JavaSymbolPairs extends SymbolPairMatch { public JavaSymbolPairs() { diff --git a/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java b/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java index c3deda897..fcef3208a 100644 --- a/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java +++ b/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java @@ -2,18 +2,17 @@ package com.itsaky.androidide.language.logs; import com.itsaky.androidide.language.BaseLanguage; import com.itsaky.androidide.models.LogLine; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; import io.github.rosemoe.editor.interfaces.CodeAnalyzer; import io.github.rosemoe.editor.interfaces.NewlineHandler; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.TextAnalyzeResult; import io.github.rosemoe.editor.text.TextAnalyzer; import io.github.rosemoe.editor.widget.CodeEditor; import io.github.rosemoe.editor.widget.SymbolPairMatch; import java.util.ArrayList; import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.services.LanguageServer; public class LogLanguageImpl extends BaseLanguage { @@ -23,6 +22,16 @@ public class LogLanguageImpl extends BaseLanguage { public LogAnalyzer addLine(LogLine line) { return analyzer.addLine(line); } + + @Override + public LanguageServer getLanguageServer() { + return null; + } + + @Override + public String getLanguageCode() { + return null; + } @Override public CodeAnalyzer getAnalyzer() { @@ -91,8 +100,8 @@ public class LogLanguageImpl extends BaseLanguage { private static class LogCompletor implements AutoCompleteProvider { @Override - public List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) { - return new ArrayList>(); + public List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) { + return new ArrayList(); } } } diff --git a/app/src/main/java/com/itsaky/androidide/language/xml/XMLAutoComplete.java b/app/src/main/java/com/itsaky/androidide/language/xml/XMLAutoComplete.java index 4e3a77da0..8fc43ebaf 100644 --- a/app/src/main/java/com/itsaky/androidide/language/xml/XMLAutoComplete.java +++ b/app/src/main/java/com/itsaky/androidide/language/xml/XMLAutoComplete.java @@ -4,16 +4,16 @@ import com.itsaky.androidide.app.StudioApp; import com.itsaky.androidide.language.xml.completion.XMLCompletionService; import com.itsaky.androidide.models.CompletionListItem; import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; import com.itsaky.androidide.utils.EitherOfResult; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.TextAnalyzeResult; import io.github.rosemoe.editor.widget.CodeEditor; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.jsonrpc.messages.Either; public class XMLAutoComplete implements AutoCompleteProvider { @@ -21,26 +21,20 @@ public class XMLAutoComplete implements AutoCompleteProvider { @Override public int compare(CompletionItem p1, CompletionItem p2) { - return ((CompletionListItem) p1).getSortText().compareTo(((CompletionListItem) p2).getSortText()); + return p1.getSortText().compareTo(p2.getSortText()); } }; + /** + * TODO: Change this to a language server implementation and remove the use of passing editor content + */ @Override - public List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) { - // We do not store the completion service here in this class - // This is because, if we modify the instance of the service in app, - // it'll be updated here - // automatically + public List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) { final XMLCompletionService service = StudioApp.getInstance().getXmlCompletionService(); - List list = new ArrayList(sort(service.complete(editor, prefix.trim()))); - List> result = new ArrayList<>(); - for(CompletionItem item : list) { - result.add(new EitherOfResult(null, item)); - } - return result; + return sort(service.complete(index, line, column, prefix.toLowerCase().trim())); } - private List sort(List result) { + private List sort(List result) { Collections.sort(result, RESULTS_SORTER); return result; } diff --git a/app/src/main/java/com/itsaky/androidide/language/xml/XMLLanguage.java b/app/src/main/java/com/itsaky/androidide/language/xml/XMLLanguage.java index 04494eeca..e2cc9137a 100644 --- a/app/src/main/java/com/itsaky/androidide/language/xml/XMLLanguage.java +++ b/app/src/main/java/com/itsaky/androidide/language/xml/XMLLanguage.java @@ -3,6 +3,7 @@ package com.itsaky.androidide.language.xml; import com.itsaky.androidide.language.BaseLanguage; import com.itsaky.androidide.language.java.manager.JavaCharacter; import com.itsaky.androidide.language.xml.lexer.XMLLexer; +import com.itsaky.androidide.lsp.LSPProvider; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; import io.github.rosemoe.editor.interfaces.CodeAnalyzer; import io.github.rosemoe.editor.interfaces.NewlineHandler; @@ -11,6 +12,7 @@ import io.github.rosemoe.editor.widget.SymbolPairMatch; import java.io.StringReader; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.services.LanguageServer; public class XMLLanguage extends BaseLanguage { @@ -24,6 +26,16 @@ public class XMLLanguage extends BaseLanguage { this.newlineHandlers = new NewlineHandler[]{new IndentHandler()}; } + @Override + public LanguageServer getLanguageServer() { + return null; + } + + @Override + public String getLanguageCode() { + return LSPProvider.LANGUAGE_XML; + } + @Override public CodeAnalyzer getAnalyzer() { return analyzer; diff --git a/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java b/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java index 08284c195..eae0f2b53 100644 --- a/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java +++ b/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java @@ -1,7 +1,6 @@ package com.itsaky.androidide.language.xml.completion; import com.itsaky.androidide.language.xml.lexer.XMLLexer; -import com.itsaky.androidide.models.CompletionListItem; import com.itsaky.androidide.utils.Environment; import com.itsaky.androidide.utils.FileUtil; import io.github.rosemoe.editor.widget.CodeEditor; @@ -17,10 +16,13 @@ import java.util.Map; import java.util.Set; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.CompletionItem; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.CompletionItemKind; public class XMLCompletionService { private HashMap attrs; @@ -50,11 +52,8 @@ public class XMLCompletionService { } - public List complete(CodeEditor editor, String prefix) { - prefix = prefix.toLowerCase(Locale.US).trim(); - final int line = editor.getCursor().getLeftLine(); - final int column = editor.getCursor().getLeftColumn(); - final List result = new ArrayList<>(); + public List complete(int index, int line, int column, String prefix) { + final List result = new ArrayList<>(); if(prefix.startsWith("<") || prefix.startsWith(" values = attr.possibleValues; for(String value : values) - if(value.toLowerCase(Locale.US).contains(prefix)) + if(value.toLowerCase(Locale.US).startsWith(prefix)) result.add(valueAsCompletion(value)); } } } else { for(Map.Entry entry : attrs.entrySet()) { Attr attr = entry.getValue(); - if(attr.name.toLowerCase(Locale.US).contains(prefix)) + if(attr.name.toLowerCase(Locale.US).startsWith(prefix)) result.add(attrAsCompletion(attr)); } } @@ -92,39 +95,37 @@ public class XMLCompletionService { return handleResults(result); } - private CompletionListItem valueAsCompletion(String value) { - CompletionListItem item = new CompletionListItem(); - item.setLabel(value) - .setCommit(value) - .setDesc("Attribute Value") - .setDetail(item.getDesc()) - .setItemType("int") - .setType(CompletionListItem.Type.ATTRIBUTE) - .cursorOffset(item.getCommit().length()); + private CompletionItem valueAsCompletion(String value) { + CompletionItem item = new CompletionItem(); + item.setLabel(value); + item.setDetail("Attribute value"); + item.setInsertText(value); + item.setInsertTextFormat(InsertTextFormat.PlainText); + item.setSortText("0" + value); + item.setKind(CompletionItemKind.Value); return item; } - private CompletionListItem attrAsCompletion(Attr attr) { - CompletionListItem item = new CompletionListItem(); - item.setLabel(attr.name) - .setCommit(attr.prefix + ":" + attr.name + "=\"" + (attr.prefix.equals("android") && attr.name.equals("id") ? "@+id/\"" : "\"")) - .setDesc("Attribute") - .setDetail(item.getDesc()) - .setItemType("int") - .setType(CompletionListItem.Type.ATTRIBUTE) - .cursorOffset(item.getCommit().length() - 1); + private CompletionItem attrAsCompletion(Attr attr) { + CompletionItem item = new CompletionItem(); + item.setLabel(attr.name); + item.setDetail("Attribute"); + item.setInsertText(attr.prefix + ":" + attr.name + "=\"" + (attr.prefix.equals("android") && attr.name.equals("id") ? "@+id/\"" : "\"")); + item.setInsertTextFormat(InsertTextFormat.PlainText); + item.setSortText("1" + attr.name); + item.setKind(CompletionItemKind.Snippet); return item; } - private CompletionListItem widgetNameAsCompletion(String name, boolean slash) { - CompletionListItem item = new CompletionListItem(); - final String n = simpleName(name); - item.setLabel(n) - .setCommit("<" + (slash ? "/" : "") + n) - .setDesc(name) - .setDetail(item.getDesc()) - .setItemType(" ") - .setType(CompletionListItem.Type.WIDGET); + private CompletionItem widgetNameAsCompletion(String name, boolean slash) { + final String n = simpleName(name); + CompletionItem item = new CompletionItem(); + item.setLabel(n); + item.setDetail(name); + item.setInsertText("<" + (slash ? "/" : "") + n); + item.setInsertTextFormat(InsertTextFormat.PlainText); + item.setSortText("2" + n); + item.setKind(CompletionItemKind.Value); return item; } @@ -132,7 +133,7 @@ public class XMLCompletionService { return name.substring(name.lastIndexOf(".") + 1); } - private List handleResults(List result) { + private List handleResults(List result) { return result; } diff --git a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClient.java b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClient.java new file mode 100644 index 000000000..41ec4b461 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClient.java @@ -0,0 +1,153 @@ +package com.itsaky.androidide.lsp; + +import com.blankj.utilcode.util.FileUtils; +import com.google.gson.Gson; +import com.itsaky.androidide.EditorActivity; +import com.itsaky.androidide.fragments.EditorFragment; +import com.itsaky.androidide.interfaces.EditorActivityProvider; +import com.itsaky.androidide.utils.LSPUtils; +import com.itsaky.androidide.utils.Logger; +import java.io.File; +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageServer; + +/** + * AndroidIDE specific implementation of the LanguageClient + */ +public abstract class IDELanguageClient implements LanguageClient { + + protected static final Gson gson = new Gson(); + protected static final Logger LOG = Logger.instance("IDELanguageClient"); + + protected EditorActivityProvider activityProvider; + + private final StarterListener starterListener; + private final OnConnectedListener onConnectedListener; + + private boolean isConnected; + + public IDELanguageClient(StarterListener starterListener, OnConnectedListener onConnectedListener) { + this.starterListener = starterListener; + this.onConnectedListener = onConnectedListener; + } + + public void setActivityProvider(EditorActivityProvider provider) { + this.activityProvider = provider; + } + + /** + * Are we connected to the server? + */ + public boolean isConnected() { + return isConnected; + } + + /** + * Called when the LanguageServer is connected successfully + */ + protected void onServerConnected(LanguageServer server) { + this.isConnected = true; + if (onConnectedListener != null) + onConnectedListener.onConnected(server); + } + + /** + * Called when the connection to the LanguageServer was cancelled. + */ + protected void onServerDisconnected() { + this.isConnected = false; + } + + /** + * Called when the ServerSocket is started. At this point, the {@link LSPClient} waits for the server to connect.
+ * This is (probably) the proper time to start your language server as ServerSocket is waiting for a client to connect. + */ + protected void startServer() { + if (this.starterListener != null) { + starterListener.startServer(); + } + } + + protected EditorActivity activity() { + if (activityProvider == null) return null; + return activityProvider.provide(); + } + + /** + * Called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show locations in EditorActivity + */ + public void showLocations(List locations) { + + } + + /** + * Called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show locations in EditorActivity + */ + public void showLocationLinks(List locations) { + + } + + /** + * Usually called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show a specific document in EditorActivity and select the specified range + */ + @Override + public CompletableFuture showDocument(ShowDocumentParams params) { + ShowDocumentResult result = new ShowDocumentResult(); + boolean success = false; + + if(params != null && params.getUri() != null && params.getSelection() != null) { + File file = new File(URI.create(params.getUri())); + if(file.exists() && file.isFile() && FileUtils.isUtf8(file)) { + final Range range = params.getSelection(); + EditorFragment frag = activity().getPagerAdapter().getFrag(activity().getBinding().tabs.getSelectedTabPosition()); + if(frag != null + && frag.getFile() != null + && frag.getEditor() != null + && frag.getFile().getAbsolutePath().equals(file.getAbsolutePath())) { + if(LSPUtils.isEqual(range.getStart(), range.getEnd())) { + frag.getEditor().setSelection(range.getStart().getLine(), range.getStart().getCharacter()); + } else { + frag.getEditor().setSelectionRegion(range.getStart().getLine(), range.getStart().getCharacter(), range.getEnd().getLine(), range.getEnd().getCharacter()); + } + } else { + activity().openFileAndSelect(file, range); + } + success = true; + } + } + + result.setSuccess(success); + return CompletableFuture.completedFuture(result); + } + + @Override + public void telemetryEvent(Object p1) { + LOG.info("telemetryEvent: ", gson.toJson(p1)); + } + + /** + * Reports connection progress + */ + protected abstract void connectionReport(String message); + + /** + * Called when there was an error connecting to server. + */ + protected abstract void connectionError(Throwable th); + + public static interface StarterListener { + void startServer(); + } + + public static interface OnConnectedListener { + void onConnected(LanguageServer server); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/LSP.java b/app/src/main/java/com/itsaky/androidide/lsp/LSP.java new file mode 100644 index 000000000..e5eeb479e --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/lsp/LSP.java @@ -0,0 +1,221 @@ +package com.itsaky.androidide.lsp; + +import com.google.gson.Gson; +import com.itsaky.androidide.app.StudioApp; +import com.itsaky.androidide.lsp.client.java.JavaLanguageClient; +import com.itsaky.androidide.shell.ShellServer; +import com.itsaky.androidide.utils.Environment; +import com.itsaky.androidide.utils.Logger; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.ClientInfo; +import org.eclipse.lsp4j.GeneralClientCapabilities; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.InitializedParams; +import org.eclipse.lsp4j.TextDocumentClientCapabilities; +import org.eclipse.lsp4j.WindowClientCapabilities; +import org.eclipse.lsp4j.WorkspaceClientCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.services.LanguageServer; +import java.util.concurrent.ExecutionException; +import java.util.Optional; +import org.eclipse.lsp4j.CallHierarchyCapabilities; +import org.eclipse.lsp4j.CodeActionCapabilities; +import org.eclipse.lsp4j.DidChangeConfigurationCapabilities; +import org.eclipse.lsp4j.DidChangeWatchedFilesCapabilities; +import org.eclipse.lsp4j.ExecuteCommandCapabilities; +import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities; +import org.eclipse.lsp4j.SemanticTokensCapabilities; +import org.eclipse.lsp4j.SynchronizationCapabilities; +import org.eclipse.lsp4j.ColorProviderCapabilities; +import org.eclipse.lsp4j.CompletionCapabilities; +import org.eclipse.lsp4j.CompletionItemCapabilities; +import com.itsaky.androidide.interfaces.EditorActivityProvider; + +/** + * A manager class to manage Language Server Protocol implementations + * An initiaize request must be sent to the server only through specific nested classes. + * This will make sure that we map the capabilites of each language server we support. + */ +public class LSP { + + private static EditorActivityProvider PROVIDER; + private static final Logger LOG = Logger.instance("LSP"); + + /** + * Stores information about ports at which the specified language server must be started + */ + public static class Ports { + public static final int JAVA = 4116; + } + + /** + * Manages Java Language Server + */ + public static class Java { + private static ShellServer mShell; + private static JavaLanguageClient javaClient; + private static LSPClientLauncher mLauncher; + + /** + * Starts the Java Language server and the client + */ + public static void start(Runnable onStarted) { + if (mShell != null && mLauncher != null && mLauncher.isConnected()) { + return; + } + javaClient = new JavaLanguageClient(() -> LSP.Java.startJLS(false), server -> storeServerInfoAndNotify(server, onStarted)); + javaClient.setActivityProvider(LSP.PROVIDER); + mLauncher = new LSPClientLauncher(javaClient, Ports.JAVA); + mLauncher.start(); + } + + /** + * Sends initialize request to the Java Language Server + */ + public static Optional init(String rootPath) { + + final LanguageServer server = LSPProvider.getServerForLanguage(LSPProvider.LANGUAGE_JAVA); + if (server == null) + return Optional.empty(); + + final List folders = new ArrayList<>(); + final File file = new File(rootPath); + final WorkspaceFolder folder = new WorkspaceFolder(); + + folder.setUri(file.toURI().toString()); + folder.setName(file.getName().toString()); + folders.add(folder); + + InitializeParams params = new InitializeParams(); + params.setClientInfo(LSP.getClientInfo()); + params.setWorkspaceFolders(folders); + params.setCapabilities(LSP.getCapabilities()); + + CompletableFuture initResult = server.initialize(params); + try { + InitializeResult result = initResult.get(); + if(result.getCapabilities() != null) { + LSPProvider.setServerCapabilitesForLanguage(LSPProvider.LANGUAGE_JAVA, result.getCapabilities()); + } + return Optional.of(result); + } catch (Throwable e) { + return Optional.empty(); + } + } + + /** + * Sends a shutdown request to the java language server + */ + + public static void shutdown() { + if(mLauncher != null) { + mLauncher.shutdown(); + } + } + + /** + * Start JDT LS from the command line + */ + private static void startJLS(boolean quiet) { + mShell = StudioApp.getInstance().newShell(t -> LOG.verbose(t)); + mShell.append(String.format("java -Djls.client.port=%d -jar %s%s", Ports.JAVA, Environment.JLS_JAR.getAbsolutePath(), quiet ? /* IMPORTANT: Do not forget the leading space */ " --quiet" : "")); + } + + /** + * Store the instance of Java Language Server in LSPProvider + */ + private static void storeServerInfoAndNotify(LanguageServer server, Runnable startedListener) { + LSPProvider.setLanguageServer(LSPProvider.LANGUAGE_JAVA, server); + LSPProvider.setClientForLanguage(LSPProvider.LANGUAGE_JAVA, javaClient); + + if(startedListener != null) + startedListener.run(); + } + } + + public static void setActivityProvider(EditorActivityProvider provider) { + PROVIDER = provider; + } + + /** + * Shuts down all known language server + */ + public static void shutdownAll() { + // Shutdown language servers one by one + LSP.Java.shutdown(); + } + + /** + * Global LSP client information that should be passed to LanguageServer with InitializeParams + */ + public static ClientInfo getClientInfo() { + ClientInfo info = new ClientInfo(); + info.setName("AndroidIDE"); + info.setVersion(com.itsaky.androidide.BuildConfig.VERSION_NAME); + return info; + } + + public static ClientCapabilities getCapabilities() { + ClientCapabilities capabilities = new ClientCapabilities(); + capabilities.setGeneral(LSP.getGeneralCapabilities()); + capabilities.setTextDocument(LSP.getTextDocumentCapabilities()); + capabilities.setWindow(LSP.getWindowCapabilities()); + capabilities.setWorkspace(LSP.getWorkspaceCapabilities()); + return capabilities; + } + + private static GeneralClientCapabilities getGeneralCapabilities() { + GeneralClientCapabilities c = new GeneralClientCapabilities(); + return c; + } + + private static TextDocumentClientCapabilities getTextDocumentCapabilities() { + TextDocumentClientCapabilities c = new TextDocumentClientCapabilities(); + c.setSynchronization(synchronizationCapabilities()); + c.setColorProvider(new ColorProviderCapabilities()); + c.setCompletion(completionCapabilities()); + return c; + } + + private static CompletionCapabilities completionCapabilities() { + CompletionCapabilities c = new CompletionCapabilities(); + return c; + } + + private static SynchronizationCapabilities synchronizationCapabilities() { + SynchronizationCapabilities c = new SynchronizationCapabilities(); + c.setDidSave(true); + c.setWillSave(false); + c.setWillSaveWaitUntil(false); + return c; + } + + private static WindowClientCapabilities getWindowCapabilities() { + WindowClientCapabilities c = new WindowClientCapabilities(); + return c; + } + + private static WorkspaceClientCapabilities getWorkspaceCapabilities() { + WorkspaceClientCapabilities c = new WorkspaceClientCapabilities(); + c.setApplyEdit(true); + c.setConfiguration(true); + c.setDidChangeConfiguration(new DidChangeConfigurationCapabilities(false)); + c.setDidChangeWatchedFiles(new DidChangeWatchedFilesCapabilities(false)); + c.setExecuteCommand(new ExecuteCommandCapabilities(true)); + c.setFileOperations(fileOperationsWorkspaceCapabilities()); + return c; + } + + private static FileOperationsWorkspaceCapabilities fileOperationsWorkspaceCapabilities() { + FileOperationsWorkspaceCapabilities c = new FileOperationsWorkspaceCapabilities(); + c.setDidCreate(true); + c.setDidDelete(true); + c.setDidRename(true); + return c; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java b/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java new file mode 100644 index 000000000..e45dbd9c6 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java @@ -0,0 +1,193 @@ +package com.itsaky.androidide.lsp; + +import com.blankj.utilcode.util.CloseUtils; +import com.itsaky.androidide.utils.Logger; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.Channels; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Future; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageServer; + +/** + * Launches a client for a Language server and handles connection. + * A callback is sent to the IDELanguageClient to tell the client to start the server if necessary. + */ +public class LSPClientLauncher extends Thread { + + private final int PORT; + private Future listeningFuture; + private final IDELanguageClient languageClient; + + private AsynchronousServerSocketChannel serverSocket; + private LanguageServer server; + + public LSPClientLauncher(IDELanguageClient client, int port) { + this.languageClient = client; + this.PORT = port; + + setDaemon(true); + } + + /** + * Returns the connected LanguageServer, or {@code null} + * + * @return The LanguageServer or {@code null} + */ + public LanguageServer getServer() { + return server; + } + + /** + * Are we connected to the server? + */ + public boolean isConnected() { + return languageClient != null && languageClient.isConnected(); + } + + @Override + public void run() { + try { + languageClient.connectionReport("Starting server socket"); + serverSocket = AsynchronousServerSocketChannel.open(); + serverSocket.bind(new InetSocketAddress(PORT)); + languageClient.connectionReport("Waiting for server to connect..."); + languageClient.startServer(); + + final AsynchronousSocketChannel server = serverSocket.accept().get(); + final OutputStream outWriter = new OutputStreamWrapper(Channels.newOutputStream(server)); + final InputStream inReader = Channels.newInputStream(server); + languageClient.connectionReport("Server connected. Launching client..."); + + Launcher launcher = LSPLauncher.createClientLauncher(languageClient, inReader, outWriter); + listeningFuture = launcher.startListening(); + + this.server = launcher.getRemoteProxy(); + languageClient.onServerConnected(this.server); + languageClient.connectionReport("Server is now listening."); + + while (!(listeningFuture.isCancelled() || listeningFuture.isDone())) { + // Take some rest + sleep(50); + } + + // Make sure we close the server. Sockets are limited resources... + CloseUtils.closeIOQuietly(inReader, outWriter, server); + languageClient.connectionReport("Server disconnected."); + languageClient.onServerDisconnected(); + } catch (Throwable th) { + languageClient.connectionError(th); + } + } + + /** + * Shut down the language server + */ + public void shutdown() { + try { + if (this.server != null) { + server.shutdown(); + server.exit(); + } + serverSocket.close(); + } catch (Throwable th) { + // Ignored + } + } + + /** + * Wraps an output stream to make sure that we don't write on UI Thread + * Writing to a socket's output stream is considered as network operation + * Trying to write to a socket on UI Thread will result in NetworkOnMainThreadExeption + */ + + private class OutputStreamWrapper extends OutputStream { + + private final AsyncWriter writer; + private final Thread writerThread; + + public OutputStreamWrapper(OutputStream actualStream) { + this.writer = new AsyncWriter(actualStream); + this.writerThread = new Thread(writer); + + this.writerThread.start(); + } + + @Override + public void write(int p1) throws IOException { + // Not needed + } + + @Override + public void write(byte[] b) throws IOException { + // Do not call super.write(b); + // If done, it will further call write(int) + this.writer.write(b); + } + + @Override + public void close() throws IOException { + super.close(); + this.writer.close(); + } + } + + /** + * Actual implementation of the writer thread + */ + private class AsyncWriter implements Runnable, Closeable { + + private final OutputStream out; + private boolean closed; + + private final BlockingQueue writeQueue = new ArrayBlockingQueue<>(10); + + public AsyncWriter(OutputStream out) { + this.out = out; + this.closed = false; + } + + public void write(byte[] b) { + this.writeQueue.add(new Writable(b)); + } + + @Override + public void run() { + while (!closed) { + try { + Writable data = writeQueue.take(); + this.out.write(data.data); + this.out.flush(); + } catch (Throwable e) { + LOG.error("Error writing to LanguageServer"); + } + } + + LOG.info("Writer thread stopped"); + } + + @Override + public void close() throws IOException { + this.closed = true; + this.out.close(); + } + + class Writable { + byte[] data; + + public Writable(byte[] data) { + this.data = data; + } + } + } + + private static final Logger LOG = Logger.instance("LSPClientLauncher"); +} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/LSPProvider.java b/app/src/main/java/com/itsaky/androidide/lsp/LSPProvider.java new file mode 100644 index 000000000..03203a549 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/lsp/LSPProvider.java @@ -0,0 +1,84 @@ +package com.itsaky.androidide.lsp; + +import java.util.HashMap; +import java.util.Map; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.ServerCapabilities; + +/** + * A provider that holds references to active {@link LanguageServer}s, its capabilites and its client implementation.
+ * To get language server for a specific language, just call {@link #getServerForLanguage(String)} + */ +public class LSPProvider { + + /** + * Language code for Java language + */ + public static final String LANGUAGE_JAVA = "java"; + + /** + * Language code for Kotlin language + */ + public static final String LANGUAGE_KOTLIN = "kotlin"; + + /** + * Language code for XML language + */ + public static final String LANGUAGE_XML = "xml"; + + /** + * Language code for Groovy language + */ + public static final String LANGUAGE_GROOVY = "groovy"; + + /** + * Map a reference to active language servers with its language id as a key + */ + private static final Map serverMap = new HashMap<>(); + + /** + * Maps a reference to the capabilities of a language server with its language id as a key + */ + private static final Map capabilitiesMap = new HashMap<>(); + + /** + * Maps a reference to the client of a language server with its language id as a key + */ + private static final Map clientMap = new HashMap<>(); + + static void setLanguageServer(String languageCode, LanguageServer server) { + serverMap.put(languageCode, server); + } + + static void setServerCapabilitesForLanguage(String languageCode, ServerCapabilities capabilities) { + capabilitiesMap.put(languageCode, capabilities); + } + + static void setClientForLanguage(String languageCode, IDELanguageClient client) { + clientMap.put(languageCode, client); + } + + public static Map getAvailableServers() { + return serverMap; + } + + public static Map getAllServerCapabilities() { + return capabilitiesMap; + } + + public static Map getAllServerClients() { + return clientMap; + } + + public static LanguageServer getServerForLanguage(String languageCode) { + return languageCode == null ? null : serverMap.get(languageCode); + } + + public static ServerCapabilities getServerCapabilitiesForLanguage(String language) { + return language == null ? null : capabilitiesMap.get(language); + } + + public static IDELanguageClient getClientForLanguage(String language) { + return language == null ? null : clientMap.get(language); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/client/java/JavaLanguageClient.java b/app/src/main/java/com/itsaky/androidide/lsp/client/java/JavaLanguageClient.java new file mode 100644 index 000000000..68019376b --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/lsp/client/java/JavaLanguageClient.java @@ -0,0 +1,117 @@ +package com.itsaky.androidide.lsp.client.java; + +import com.itsaky.androidide.lsp.IDELanguageClient; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; +import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.LogTraceParams; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.ProgressParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.SetTraceParams; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.UnregistrationParams; +import org.eclipse.lsp4j.WorkDoneProgressCreateParams; +import org.eclipse.lsp4j.WorkspaceFolder; + +public class JavaLanguageClient extends IDELanguageClient { + + public JavaLanguageClient(StarterListener starterListener, OnConnectedListener onConnectedListener) { + super(starterListener, onConnectedListener); + } + + @Override + protected void connectionReport(String message) { + LOG.info("Connection report: ", message); + } + + @Override + protected void connectionError(Throwable th) { + LOG.info("Connection error: ", th); + } + + @Override + public CompletableFuture applyEdit(ApplyWorkspaceEditParams p1) { + return null; + } + + @Override + public CompletableFuture> configuration(ConfigurationParams p1) { + return null; + } + + @Override + public CompletableFuture createProgress(WorkDoneProgressCreateParams p1) { + return null; + } + + @Override + public void logMessage(MessageParams p1) { + LOG.info("logMessage: ", gson.toJson(p1)); + } + + @Override + public void logTrace(LogTraceParams p1) { + LOG.info("logTrace: ", gson.toJson(p1)); + } + + @Override + public void notifyProgress(ProgressParams p1) { + LOG.info("notifyProgress: ", gson.toJson(p1)); + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams p1) { + } + + @Override + public CompletableFuture refreshCodeLenses() { + return null; + } + + @Override + public CompletableFuture refreshSemanticTokens() { + return null; + } + + @Override + public CompletableFuture registerCapability(RegistrationParams p1) { + return null; + } + + @Override + public void setTrace(SetTraceParams p1) { + LOG.info("setTrace: ", gson.toJson(p1)); + } + + @Override + public CompletableFuture showDocument(ShowDocumentParams p1) { + return null; + } + + @Override + public void showMessage(MessageParams p1) { + LOG.info("showMessage: ", gson.toJson(p1)); + } + + @Override + public CompletableFuture showMessageRequest(ShowMessageRequestParams p1) { + return null; + } + + @Override + public CompletableFuture unregisterCapability(UnregistrationParams p1) { + return null; + } + + @Override + public CompletableFuture> workspaceFolders() { + return null; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java b/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java index 67341a9e8..78d4cfe17 100644 --- a/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java +++ b/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java @@ -17,7 +17,7 @@ public class ToolsManager { private static PreferenceManager prefs; - public static final int JLS_VERSION = 8; + public static final int JLS_VERSION = 12; public static final int LOGSENDER_VERSION = 1; public static final int CLEANER_VERSION = 1; public static final int GRADLE_API_VERSION = 2; @@ -62,18 +62,9 @@ public class ToolsManager { private static void extractJlsIfNeeded() { - boolean gson = false, jls = false, proto = false; - final List files = FileUtils.listFilesInDir(Environment.JLS_HOME, true); - for(File f : files) { - gson = gson == true ? gson : f.getName().equals("gson.jar"); - jls = jls == true ? jls : f.getName().equals("jls.jar"); - proto = proto == true ? proto : f.getName().equals("protobuf.jar"); - } - - final boolean exists = gson && jls && proto; final boolean isOld = JLS_VERSION > prefs.getInt(KEY_JLS_VERSION, 0); - if(!exists || isOld) { + if(!Environment.JLS_JAR.exists() || isOld) { try { extractJls(); } catch (Throwable e) {} @@ -81,14 +72,7 @@ public class ToolsManager { } private static void extractJls(){ - final File jlsZip = new File(Environment.JLS_HOME, "jls.zip"); - ResourceUtils.copyFileFromAssets("data/jls.zip", jlsZip.getAbsolutePath()); - try { - ZipFile file = new ZipFile(jlsZip, ConstantsBridge.JLS_ZIP_PASSWORD_HASH.toCharArray()); - file.extractAll(Environment.JLS_HOME.getAbsolutePath()); - FileUtils.delete(jlsZip); - prefs.putInt(KEY_JLS_VERSION, JLS_VERSION); - } catch (Throwable th) {} + ResourceUtils.copyFileFromAssets("data/jls.jar", Environment.JLS_JAR.getAbsolutePath()); } diff --git a/app/src/main/java/com/itsaky/androidide/models/SearchResult.java b/app/src/main/java/com/itsaky/androidide/models/SearchResult.java index 16fbb5680..3f2e45aba 100644 --- a/app/src/main/java/com/itsaky/androidide/models/SearchResult.java +++ b/app/src/main/java/com/itsaky/androidide/models/SearchResult.java @@ -1,7 +1,7 @@ package com.itsaky.androidide.models; -import com.itsaky.lsp.Range; import java.io.File; +import org.eclipse.lsp4j.Range; public class SearchResult extends Range { public File file; @@ -9,8 +9,8 @@ public class SearchResult extends Range { public String match; public SearchResult(Range src, File file, String line, String match) { - this.start = src.start; - this.end = src.end; + this.setStart(src.getStart()); + this.setEnd(src.getEnd()); this.file = file; this.line = line; this.match = match; diff --git a/app/src/main/java/com/itsaky/androidide/utils/Environment.java b/app/src/main/java/com/itsaky/androidide/utils/Environment.java index 650c663f0..f929e1160 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/Environment.java +++ b/app/src/main/java/com/itsaky/androidide/utils/Environment.java @@ -35,6 +35,7 @@ public final class Environment { public static final File INIT_SCRIPT; public static final File PROJECT_DATA_FILE; + public static final File JLS_JAR; public static final File GRADLE_HOME; public static final File GRADLE_PROPS; @@ -71,7 +72,8 @@ public final class Environment { IDEPROPS = new File(SYSROOT, "etc/ide-environment.properties"); LIBHOOK = new File(LIBDIR, "libhook.so"); PROJECT_DATA_FILE = new File(TMP_DIR, "ide_project"); - + JLS_JAR = new File(JLS_HOME, "jls.jar"); + INIT_SCRIPT = new File(mkdirIfNotExits(new File(app.getIDEDataDir(), "init")), "androidide.init.gradle"); BOOTCLASSPATH = new File(""); GRADLE_USER_HOME = new File(HOME, ".gradle"); diff --git a/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java b/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java new file mode 100644 index 000000000..45d5829c5 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java @@ -0,0 +1,9 @@ +package com.itsaky.androidide.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class JSONUtility { + public static final Gson gson = new Gson(); + public static final Gson prettyPrinter = new GsonBuilder().setPrettyPrinting().create(); +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java b/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java new file mode 100644 index 000000000..3d8475524 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java @@ -0,0 +1,31 @@ +package com.itsaky.androidide.utils; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +public class LSPUtils { + + public static final Position Position_ofZero; + public static final Range Range_ofZero; + + static { + Position_ofZero = new Position(0, 0); + Range_ofZero = new Range(Position_ofZero, Position_ofZero); + } + + public static boolean isEqual(Position p1, Position p2) { + if(p1 == null) return false; + if(p2 == null) return false; + + return p1.getLine() == p2.getLine() + && p1.getCharacter() == p2.getCharacter(); + } + + public static boolean isEqual(Range r1, Range r2) { + if(r1 == null) return false; + if(r2 == null) return false; + + return LSPUtils.isEqual(r1.getStart(), r2.getStart()) + && LSPUtils.isEqual(r1.getEnd(), r2.getEnd()); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/RecursiveFileSearcher.java b/app/src/main/java/com/itsaky/androidide/utils/RecursiveFileSearcher.java index c0133959e..55929586e 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/RecursiveFileSearcher.java +++ b/app/src/main/java/com/itsaky/androidide/utils/RecursiveFileSearcher.java @@ -4,19 +4,19 @@ import com.blankj.utilcode.util.FileIOUtils; import com.blankj.utilcode.util.FileUtils; import com.itsaky.androidide.models.SearchResult; import com.itsaky.androidide.tasks.TaskExecutor; -import com.itsaky.lsp.Position; -import com.itsaky.lsp.Range; import io.github.rosemoe.editor.text.CharPosition; import io.github.rosemoe.editor.text.Content; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.HashMap; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; /** * This class provides API to search in files recursively @@ -84,8 +84,8 @@ public class RecursiveFileSearcher { final Range range = new Range(); final CharPosition start = content.getIndexer().getCharPosition(matcher.start()); final CharPosition end = content.getIndexer().getCharPosition(matcher.end()); - range.start = new Position(start.line, start.column); - range.end = new Position(end.line, end.column); + range.setStart(new Position(start.line, start.column)); + range.setEnd(new Position(end.line, end.column)); String sub = "...".concat(text.substring(Math.max(0, matcher.start() - 30), Math.min(matcher.end() + 31, text.length()))).trim().concat("..."); String match = content.subContent(start.line, start.column, end.line, end.column).toString(); ranges.add(new SearchResult(range, file, sub.replaceAll("\\s+", " "), match)); diff --git a/app/src/main/java/io/github/rosemoe/editor/interfaces/AutoCompleteProvider.java b/app/src/main/java/io/github/rosemoe/editor/interfaces/AutoCompleteProvider.java index 5a2feba88..a13ff2250 100644 --- a/app/src/main/java/io/github/rosemoe/editor/interfaces/AutoCompleteProvider.java +++ b/app/src/main/java/io/github/rosemoe/editor/interfaces/AutoCompleteProvider.java @@ -15,12 +15,11 @@ */ package io.github.rosemoe.editor.interfaces; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.TextAnalyzeResult; import io.github.rosemoe.editor.widget.CodeEditor; import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import com.itsaky.androidide.utils.Logger; /** * Interface for auto completion analysis @@ -28,7 +27,7 @@ import java.util.List; * @author Rose */ public interface AutoCompleteProvider { - + /** * Analyze auto complete items * @@ -38,7 +37,7 @@ public interface AutoCompleteProvider { * @param line The line of cursor * @return Analyzed items */ - List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line); + List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception; } diff --git a/app/src/main/java/io/github/rosemoe/editor/interfaces/EditorLanguage.java b/app/src/main/java/io/github/rosemoe/editor/interfaces/EditorLanguage.java index 52cfcb017..a7e530d40 100644 --- a/app/src/main/java/io/github/rosemoe/editor/interfaces/EditorLanguage.java +++ b/app/src/main/java/io/github/rosemoe/editor/interfaces/EditorLanguage.java @@ -16,6 +16,7 @@ package io.github.rosemoe.editor.interfaces; import io.github.rosemoe.editor.widget.SymbolPairMatch; +import org.eclipse.lsp4j.services.LanguageServer; /** * Language for editor @@ -32,7 +33,19 @@ import io.github.rosemoe.editor.widget.SymbolPairMatch; * @author Rose */ public interface EditorLanguage { - + + /** + * Get the language server for this available + * May return null. + */ + LanguageServer getLanguageServer(); + + /** + * Get the language code of this language. + * This must be the one specified in {@link com.itsaky.androidide.lsp.LSPProvider LSPProvider} + */ + String getLanguageCode(); + /** * Get CodeAnalyzer of this language object * diff --git a/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java b/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java index c24ef47ac..800b7ab30 100644 --- a/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java +++ b/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java @@ -15,27 +15,36 @@ */ package io.github.rosemoe.editor.langs; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; -import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; -import io.github.rosemoe.editor.interfaces.CodeAnalyzer; -import io.github.rosemoe.editor.interfaces.EditorLanguage; -import io.github.rosemoe.editor.interfaces.NewlineHandler; -import io.github.rosemoe.editor.struct.CompletionItem; -import io.github.rosemoe.editor.text.TextAnalyzeResult; -import io.github.rosemoe.editor.text.TextAnalyzer.AnalyzeThread.Delegate; -import io.github.rosemoe.editor.widget.CodeEditor; -import io.github.rosemoe.editor.widget.SymbolPairMatch; -import java.util.ArrayList; -import java.util.List; - /** * Empty language without any effect * * @author Rose */ +import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; +import io.github.rosemoe.editor.interfaces.CodeAnalyzer; +import io.github.rosemoe.editor.interfaces.EditorLanguage; +import io.github.rosemoe.editor.interfaces.NewlineHandler; +import io.github.rosemoe.editor.text.TextAnalyzeResult; +import io.github.rosemoe.editor.widget.CodeEditor; +import io.github.rosemoe.editor.widget.SymbolPairMatch; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.xtend.lib.annotations.Delegate; +import org.eclipse.lsp4j.services.LanguageServer; + public class EmptyLanguage implements EditorLanguage { + @Override + public LanguageServer getLanguageServer() { + return null; + } + + @Override + public String getLanguageCode() { + return null; + } + @Override public CharSequence format(CharSequence text) { return text; @@ -80,8 +89,8 @@ public class EmptyLanguage implements EditorLanguage { public static class EmptyAutoCompleteProvider implements AutoCompleteProvider { @Override - public List> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) { - return new ArrayList>(); + public List getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) { + return new ArrayList(); } } @@ -89,10 +98,9 @@ public class EmptyLanguage implements EditorLanguage { private static class EmptyCodeAnalyzer implements CodeAnalyzer { @Override - public void analyze(CharSequence content, TextAnalyzeResult colors, Delegate delegate) { + public void analyze(CharSequence content, TextAnalyzeResult colors, io.github.rosemoe.editor.text.TextAnalyzer.AnalyzeThread.Delegate delegate) { colors.addNormalIfNull(); } - } } diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java index 9b9b1e5f5..ebf201b3d 100644 --- a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java @@ -58,27 +58,21 @@ import androidx.annotation.Px; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.menu.MenuBuilder; -import com.blankj.utilcode.util.ThrowableUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.itsaky.androidide.R; +import com.itsaky.androidide.app.StudioApp; import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding; import com.itsaky.androidide.interfaces.JLSRequestor; -import com.itsaky.androidide.managers.VersionedFileManager; +import com.itsaky.androidide.lsp.LSPProvider; import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; import com.itsaky.androidide.utils.Logger; import com.itsaky.androidide.utils.Symbols; import com.itsaky.androidide.utils.TypefaceUtils; +import com.itsaky.lsp.CodeAction; import com.itsaky.lsp.Diagnostic; import com.itsaky.lsp.DiagnosticSeverity; -import com.itsaky.lsp.DidChangeTextDocumentParams; -import com.itsaky.lsp.Position; import com.itsaky.lsp.Range; -import com.itsaky.lsp.ReferenceContext; -import com.itsaky.lsp.ReferenceParams; -import com.itsaky.lsp.TextDocumentContentChangeEvent; -import com.itsaky.lsp.TextDocumentIdentifier; -import com.itsaky.lsp.TextDocumentPositionParams; -import com.itsaky.lsp.VersionedTextDocumentIdentifier; +import com.itsaky.toaster.Toaster; import io.github.rosemoe.editor.interfaces.EditorEventListener; import io.github.rosemoe.editor.interfaces.EditorLanguage; import io.github.rosemoe.editor.interfaces.NewlineHandler; @@ -106,8 +100,29 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import com.itsaky.lsp.CodeActionParams; -import com.itsaky.lsp.CodeAction; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageServer; +import android.app.ProgressDialog; +import com.blankj.utilcode.util.ThreadUtils; +import com.itsaky.androidide.lsp.IDELanguageClient; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.CodeActionOptions; +import org.eclipse.lsp4j.DefinitionOptions; +import org.eclipse.lsp4j.ReferenceOptions; /** * CodeEditor is a editor that can highlight text regions by doing basic syntax analyzing @@ -194,6 +209,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca private int mCachedLineNumberWidth; private int mErrorColor; private int mWarningColor; + private int mFileVersion = 0; private float mDpUnit; private float mDividerWidth; private float mDividerMargin; @@ -213,6 +229,9 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca private boolean mLineNumberEnabled; private boolean mBlockLineEnabled; private boolean mAutoCompletionEnabled; + private boolean mFindReferencesEnabled; + private boolean mGotoDefinitionEnabled; + private boolean mCodeActionsEnabled; private boolean mCompletionOnComposing; private boolean mHighlightSelectedText; private boolean mHighlightCurrentBlock; @@ -245,6 +264,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca private EditorColorScheme mColors; private String mLnTip = "Line:"; private EditorLanguage mLanguage; + private LanguageServer mLanguageServer; private long mLastMakeVisible = 0; private EditorAutoCompleteWindow mCompletionWindow; private DiagnosticWindow mDiagnosticWindow; @@ -253,7 +273,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca private GestureDetector mBasicDetector; protected EditorTextActionPresenter mTextActionPresenter; private ScaleGestureDetector mScaleDetector; - EditorInputConnection mConnection; private CursorAnchorInfo.Builder mAnchorInfoBuilder; private MaterialEdgeEffect mVerticalEdgeGlow; private MaterialEdgeEffect mHorizontalGlow; @@ -269,6 +288,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca private SymbolPairMatch mOverrideSymbolPairs; private LongArrayList mPostDrawLineNumbers = new LongArrayList(); private CharPosition mLockedSelection; + EditorInputConnection mConnection; KeyMetaStates mKeyMetaStates = new KeyMetaStates(this); private Map mLineColors; @@ -306,8 +326,42 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca return currentFile; } + /** + * Set the current file whose contents this editor is editing
+ * This method must be called only once in this editors lifetime
+ * This will also make sure to send 'textDocument/didOpen' to any provided language server. + * + * @param file The file that was opened + */ public void setFile(File file) { this.currentFile = file; + + if(mLanguageServer != null) { + TextDocumentItem item = new TextDocumentItem(); + item.setLanguageId(getLanguageIdForFile()); + item.setText(getText().toString()); + item.setUri(file.toURI().toString()); + item.setVersion(mFileVersion = 0); + mLanguageServer.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(item)); + } + } + + private String getLanguageIdForFile() { + final String name = getFile().getName(); + + if(name.endsWith(".java")) { + return "java"; + } + + if(name.endsWith(".xml")) { + return "xml"; + } + + if(name.endsWith(".kt")) { + return "kotlin"; + } + + return "unknown"; } /** @@ -555,14 +609,14 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca int leftCol = getCursor().getLeftColumn(); int rightLine = getCursor().getRightLine(); int rightCol = getCursor().getRightColumn(); - final Range range = new Range(new Position(leftLine, leftCol), new Position(rightLine, rightCol)); + final org.eclipse.lsp4j. Range range = new org.eclipse.lsp4j. Range(new Position(leftLine, leftCol), new Position(rightLine, rightCol)); final Set keys = mDiagnostics.keySet(); Range found = null; for(Range r : keys) { - if(r.start.line <= range.start.line - && r.start.column <= range.start.column - && r.end.line >= range.end.line - && r.end.column >= range.end.column) { + if(r.start.line <= range.getStart().getLine() + && r.start.column <= range.getStart().getCharacter() + && r.end.line >= range.getEnd().getLine() + && r.end.column >= range.getEnd().getCharacter()) { found = r; break; } @@ -749,7 +803,8 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca lang = new EmptyLanguage(); } this.mLanguage = lang; - + this.mLanguageServer = mLanguage.getLanguageServer(); + // Update spanner if (mSpanner != null) { mSpanner.shutdown(); @@ -781,13 +836,63 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca if (mCursor != null) { mCursor.setLanguage(mLanguage); } + + setupLanguageServerCapabilities(); + invalidate(); } public EditorLanguage getEditorLanguage() { return mLanguage; } - + + /** + * Get the {@link org.eclipse.lsp4j.services.LanguageServer current language server} which is set to this editor + * May return null; + * + * @return Current language server or {@code null}. + */ + public LanguageServer getLanguageServer () { + return mLanguageServer; + } + + /** + * Sets up this editor according to the capabilities of the current language server + */ + private void setupLanguageServerCapabilities() { + if(mLanguage == null || mLanguageServer == null) return; + + ServerCapabilities c = LSPProvider.getServerCapabilitiesForLanguage(mLanguage.getLanguageCode()); + if(c == null) { + mCodeActionsEnabled = mGotoDefinitionEnabled = mFindReferencesEnabled = false; + return; + } + + Either codeActionOptions = c.getCodeActionProvider(); + if(codeActionOptions.isLeft()) { + mCodeActionsEnabled = codeActionOptions.getLeft(); + } else { + CodeActionOptions options = codeActionOptions.getRight(); + mCodeActionsEnabled = options != null; + } + + Either definitionOptions = c.getDefinitionProvider(); + if(definitionOptions.isLeft()) { + mGotoDefinitionEnabled = definitionOptions.getLeft(); + } else { + DefinitionOptions options = definitionOptions.getRight(); + mGotoDefinitionEnabled = options != null; + } + + Either referenceOptions = c.getReferencesProvider(); + if(definitionOptions.isLeft()) { + mFindReferencesEnabled = referenceOptions.getLeft(); + } else { + ReferenceOptions options = referenceOptions.getRight(); + mFindReferencesEnabled = options != null; + } + } + /** * Getter * @@ -2575,6 +2680,28 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca mCompletionWindow.hide(); } } + + /** + * Specify whether we should provide any code actions + *

+ */ + public void setCodeActionsEnabled(boolean codeActionsEnabled) { + mCodeActionsEnabled = codeActionsEnabled; + } + + /** + * Specify whether we should provide any 'Go to definition' support + */ + public void setGotoDefinitionEnabled(boolean gotoDefinitionEnabled) { + mGotoDefinitionEnabled = gotoDefinitionEnabled; + } + + /** + * Specify whether we should provide any 'Find references' support + */ + public void setFindReferencesEnabled(boolean findReferencesEnabled) { + mFindReferencesEnabled = findReferencesEnabled; + } /** * @see CodeEditor#setAutoCompletionEnabled(boolean) @@ -2582,6 +2709,18 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca public boolean isAutoCompletionEnabled() { return mAutoCompletionEnabled; } + + public boolean isCodeActionsEnabled() { + return mCodeActionsEnabled && mLanguageServer != null; + } + + public boolean isGotoDefinitionEnabled() { + return mGotoDefinitionEnabled && mLanguageServer != null; + } + + public boolean isFindReferencesEnabled() { + return mFindReferencesEnabled && mLanguageServer != null; + } /** * Sets non-printable painting flags. @@ -3767,8 +3906,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca mListener.onNewTextSet(this); } - notifyChanged(); - if (mInputMethodManager != null) { mInputMethodManager.restartInput(this); } @@ -4495,43 +4632,143 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca } public void findDefinition() { - if(jlsRequestor == null || getFile() == null) return; + if(!isGotoDefinitionEnabled()) return; + + final ProgressDialog pd = ProgressDialog.show(getContext(), null, getContext().getString(R.string.msg_finding_definition)); + try { - TextDocumentPositionParams params = new TextDocumentPositionParams(); - params.position = new Position(getCursor().getLeftLine(), getCursor().getLeftColumn()); - params.textDocument = new TextDocumentIdentifier(getFile().toURI()); - jlsRequestor.findDefinition(params); + final DefinitionParams params = new DefinitionParams(); + params.setTextDocument(new TextDocumentIdentifier(getFile().toURI().toString())); + params.setPosition(getCursorAsLSPPosition()); + final CompletableFuture, List>> future = mLanguageServer.getTextDocumentService().definition(params); + future.whenComplete((e, t) -> { + final Either, List> either = e; + final Throwable th = t; + final IDELanguageClient client = LSPProvider.getClientForLanguage(mLanguage.getLanguageCode()); + if(client == null || future.isCancelled() || future.isCompletedExceptionally()) { + showDefinitionNotFound(pd); + return; + } + + if(either.isLeft()) { + List locations = either.getLeft(); + if(locations == null || locations.size() <= 0) { + showDefinitionNotFound(pd); + return; + } + + if(locations.size() == 1) { + Location location = locations.get(0); + showDocument(client, location.getUri(), location.getRange()); + } else { + client.showLocations(locations); + } + } else if(either.isRight()) { + List locations = either.getRight(); + if(locations == null || locations.size() <= 0) { + showDefinitionNotFound(pd); + return; + } + + if(locations.size() == 1) { + LocationLink location = locations.get(0); + showDocument(client, location.getTargetUri(), location.getTargetSelectionRange()); + } else { + client.showLocationLinks(locations); + } + } + }); } catch (Throwable th) { - LOG.error(ThrowableUtils.getFullStackTrace(th)); + showDefinitionNotFound(pd); } } + private void showDefinitionNotFound(final ProgressDialog pd) { + ThreadUtils.runOnUiThread(() -> { + toast(R.string.msg_no_definition, Toaster.Type.ERROR); + pd.dismiss(); + }); + } + public void findReferences() { - if(jlsRequestor == null || getFile() == null) return; + if(!isFindReferencesEnabled()) return; + + final ProgressDialog pd = ProgressDialog.show(getContext(), null, getContext().getString(R.string.msg_finding_definition)); + try { - ReferenceParams params = new ReferenceParams(); - params.context = new ReferenceContext(true); - params.position = new Position(getCursor().getLeftLine(), getCursor().getLeftColumn()); - params.textDocument = new TextDocumentIdentifier(getFile().toURI()); - jlsRequestor.findReferences(params); + } catch (Throwable th) { - LOG.error(ThrowableUtils.getFullStackTrace(th)); + showReferencesNotFound(pd); } } - public void performCodeActions(List actions) { - if(jlsRequestor != null && getFile() != null) { - jlsRequestor.performCodeActions(getFile(), actions); + private void showReferencesNotFound(final ProgressDialog pd) { + ThreadUtils.runOnUiThread(() -> { + toast(R.string.msg_no_references, Toaster.Type.ERROR); + pd.dismiss(); + }); + } + + /** + * Request the client to show the specific document in an editor and select the provided range + */ + private void showDocument(IDELanguageClient client, String uri, org.eclipse.lsp4j.Range range) { + ShowDocumentParams params = new ShowDocumentParams(); + params.setExternal(false); + params.setSelection(range); + params.setUri(uri); + client.showDocument(params); + } + + public void didSave () { + if(mLanguageServer != null && getFile() != null) { + mLanguageServer.getTextDocumentService().didSave(new DidSaveTextDocumentParams(new org.eclipse.lsp4j.TextDocumentIdentifier(getFile().toURI().toString()))); } } + + public void close() { + if(mLanguageServer != null && getFile() != null) { + mLanguageServer.getTextDocumentService().didClose(new DidCloseTextDocumentParams(new org.eclipse.lsp4j.TextDocumentIdentifier(getFile().toURI().toString()))); + } + } + + public void codeAction() { + + } + + public Position getCursorAsLSPPosition() { + return new Position(getCursor().getLeftLine(), getCursor().getLeftColumn()); + } + + private boolean isInString() { + final int line = getCursor().getLeftLine(); + final int column = getCursor().getLeftColumn(); + if(stringMap == null || stringMap.isEmpty() || !stringMap.containsKey(line)) return false; + List strings = stringMap.get(line); + if(strings == null || strings.isEmpty()) return false; + for(int i=0;i strings = stringMap.get(line); - if(strings == null || strings.isEmpty()) return false; - for(int i=0;i changes = new ArrayList<>(); - event.text = getText().toString(); - event.range = null; - changes.add(event); - id.uri = getFile().toURI(); - id.version = VersionedFileManager.incrementVersion(getFile()); - p.textDocument = id; - p.contentChanges = changes; - jlsRequestor.didChange(p); - } - } - + @Override public void afterDelete(Content content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) { + notifyChanged(); + if(mDiagnosticWindow != null) mDiagnosticWindow.hide(); - - notifyChanged(); if (isSpanMapPrepared(false, endLine - startLine)) { if (startLine == endLine) { @@ -4721,6 +4927,38 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca postInvalidate(); } } + + private void notifyChanged() { + if(mLanguageServer != null) { + DidChangeTextDocumentParams p = didChangeParams(); + if(p != null) { + mLanguageServer.getTextDocumentService().didChange(p); + } + } + } + + protected DidChangeTextDocumentParams didChangeParams() { + if(getFile() == null) { + return null; + } + + VersionedTextDocumentIdentifier doc = new VersionedTextDocumentIdentifier(); + doc.setVersion(++mFileVersion); + doc.setUri(getFile().toURI().toString()); + DidChangeTextDocumentParams p = new DidChangeTextDocumentParams(); + p.setTextDocument(doc); + p.setContentChanges(createChangeEvents()); + + return p; + } + + private List createChangeEvents() { + final List events = new ArrayList<>(); + TextDocumentContentChangeEvent event = new TextDocumentContentChangeEvent(); + event.setText(getText().toString()); + events.add(event); + return events; + } public void onEndTextSelect() { showTextActionPopup(); diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/DefaultCompletionItemAdapter.java b/app/src/main/java/io/github/rosemoe/editor/widget/DefaultCompletionItemAdapter.java index 3748d7f16..7657fc5a8 100644 --- a/app/src/main/java/io/github/rosemoe/editor/widget/DefaultCompletionItemAdapter.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/DefaultCompletionItemAdapter.java @@ -15,6 +15,11 @@ */ package io.github.rosemoe.editor.widget; +/** + * Adapter to display results + * + * @author Rose + */ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.util.TypedValue; @@ -25,15 +30,8 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.core.content.ContextCompat; import com.itsaky.androidide.R; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; -import io.github.rosemoe.editor.struct.CompletionItem; +import org.eclipse.lsp4j.CompletionItem; -/** - * Adapter to display results - * - * @author Rose - */ @SuppressWarnings("CanBeFinal") class DefaultCompletionItemAdapter extends EditorCompletionAdapter { @@ -47,29 +45,24 @@ class DefaultCompletionItemAdapter extends EditorCompletionAdapter { if (view == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.default_completion_result_item, parent, false); } - Either item = getItem(pos); - String label, desc; - Drawable icon; - if(item.isLeft()) { - label = item.getLeft().getName(); - desc = item.getLeft().getDescription(); - icon = ContextCompat.getDrawable(parent.getContext(), R.mipmap.box_red); - } else { - label = item.getRight().label; - desc = item.getRight().desc; - icon = item.getRight().icon; - } - TextView tv = (TextView) view.findViewById(R.id.result_item_label); + final CompletionItem item = getItem(pos); + final String label = item.getLabel(); + final String desc = item.getDetail(); + final Drawable icon = ContextCompat.getDrawable(parent.getContext(), R.mipmap.box_red); + + TextView tv = view.findViewById(R.id.result_item_label); tv.setText(label); - tv = (TextView) view.findViewById(R.id.result_item_desc); + tv = view.findViewById(R.id.result_item_desc); tv.setText(desc); view.setTag(pos); + if (isCurrentCursorPosition) { view.setBackgroundColor(0xffdddddd); } else { view.setBackgroundColor(0xffffffff); } - ImageView iv = (ImageView) view.findViewById(R.id.result_item_image); + + ImageView iv = view.findViewById(R.id.result_item_image); iv.setImageDrawable(icon); return view; } diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/EditorAutoCompleteWindow.java b/app/src/main/java/io/github/rosemoe/editor/widget/EditorAutoCompleteWindow.java index d521d222f..610057408 100644 --- a/app/src/main/java/io/github/rosemoe/editor/widget/EditorAutoCompleteWindow.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/EditorAutoCompleteWindow.java @@ -15,29 +15,30 @@ */ package io.github.rosemoe.editor.widget; +/** + * Auto complete window for editing code quicker + * + * @author Rose + */ + import android.content.Context; import android.graphics.drawable.GradientDrawable; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import com.itsaky.androidide.databinding.LayoutCompletionWindowBinding; -import com.itsaky.androidide.models.CompletionEdit; -import com.itsaky.androidide.models.CompletionListItem; -import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; +import com.itsaky.androidide.language.java.manager.JavaCharacter; import io.github.rosemoe.editor.interfaces.AutoCompleteProvider; -import io.github.rosemoe.editor.struct.CompletionItem; import io.github.rosemoe.editor.text.CharPosition; -import io.github.rosemoe.editor.text.Cursor; +import io.github.rosemoe.editor.text.Content; import io.github.rosemoe.editor.text.TextAnalyzeResult; import java.util.ArrayList; import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; -/** - * Auto complete window for editing code quicker - * - * @author Rose - */ public class EditorAutoCompleteWindow extends EditorBasePopupWindow { private final static String TIP = "Refreshing..."; private final CodeEditor mEditor; @@ -185,47 +186,54 @@ public class EditorAutoCompleteWindow extends EditorBasePopupWindow { * @param pos Index of auto complete item */ public void select(int pos) { - Either either = ((EditorCompletionAdapter) mBinding.list.getAdapter()).getItem(pos); - if(either != null && either.isLeft()) { - SuggestItem item = either.getLeft(); - item.onSelectThis(mEditor); - } else if(either != null && either.isRight()) { - final CompletionItem item = either.getRight(); - Cursor cursor = mEditor.getCursor(); - if (!cursor.isSelected()) { - int l = 1; - if(mLastPrefix.endsWith(".")) - l = 0; - else if(mLastPrefix.contains(".")) - l = mLastPrefix.substring(mLastPrefix.lastIndexOf(".") + 1, mLastPrefix.length() - 1).length(); - else l = mLastPrefix.length() - 1; - l++; - if(mLastPrefix.endsWith(".")) l--; - mEditor.getText().delete(cursor.getLeftLine(), cursor.getLeftColumn() - l, cursor.getLeftLine(), cursor.getLeftColumn()); - cursor.onCommitText(item.commit); - - if (item.cursorOffset != item.commit.length()) { - int delta = (item.commit.length() - item.cursorOffset); - if (delta != 0) { - int newSel = Math.max(mEditor.getCursor().getLeft() - delta, 0); - CharPosition charPosition = mEditor.getCursor().getIndexer().getCharPosition(newSel); - mEditor.setSelection(charPosition.line, charPosition.column); - } - } - - if(item instanceof CompletionListItem && ((CompletionListItem) item).hasAdditionalEdits()) { - for(CompletionEdit edit : ((CompletionListItem) item).getAdditionalEdits()) { - if(edit.position.column == -1) { - int col = mEditor.getText().getColumnCount(edit.position.line); - edit.position.column = col; - } - mEditor.getText().insert(edit.position.line, edit.position.column, edit.insertText); - } - } - } - } + CompletionItem item = ((EditorCompletionAdapter) mBinding.list.getAdapter()).getItem(pos); + Range range = getIdentifierRange(mEditor.getCursor().getRight(), mEditor.getText()); + String text = item.getInsertText() == null ? item.getLabel() : item.getInsertText(); + final boolean shiftLeft = text.contains("$0"); + final boolean atEnd = text.endsWith("$0"); + text = text.replace("$0", ""); + + mEditor.getText().delete( + range.getStart().getLine(), + range.getStart().getCharacter(), + range.getEnd().getLine(), + range.getEnd().getCharacter() + ); + mEditor.getCursor().onCommitText(text); + + if(shiftLeft && !atEnd) { + mEditor.moveSelectionLeft(); + } + + final List edits = item.getAdditionalTextEdits(); + if(edits != null && edits.size() > 0) { + for(int i=0;i 0 && JavaCharacter.isJavaIdentifierPart(content.charAt(start - 1))) { + start--; + } + + CharPosition startPos = content.getIndexer().getCharPosition(start); + CharPosition endPos = content.getIndexer().getCharPosition(end); + return new Range(new Position(startPos.line, startPos.column), new Position(endPos.line, endPos.column)); + } /** * Get prefix set @@ -261,7 +269,7 @@ public class EditorAutoCompleteWindow extends EditorBasePopupWindow { * @param results Items of analysis * @param requestTime The time that this thread starts */ - public void displayResults(final List> results, long requestTime) { + public void displayResults(final List results, long requestTime) { mEditor.post(() -> { setLoading(false); if (results == null || results.isEmpty()) { @@ -287,23 +295,29 @@ public class EditorAutoCompleteWindow extends EditorBasePopupWindow { private class MatchThread extends Thread { private final long mTime; private final String mPrefix; + private final String mFileUri; private final boolean mInner; private final TextAnalyzeResult mColors; + private final int mIndex; private final int mLine; + private final int mColumn; private final AutoCompleteProvider mLocalProvider = mProvider; public MatchThread(long requestTime, String prefix) { mTime = requestTime; mPrefix = prefix; + mFileUri = mEditor.getFile() != null ? mEditor.getFile().toURI().toString() : null; mColors = mEditor.getTextAnalyzeResult(); + mIndex = mEditor.getCursor().getLeft(); mLine = mEditor.getCursor().getLeftLine(); + mColumn = mEditor.getCursor().getLeftColumn(); mInner = (!mEditor.isHighlightCurrentBlock()) || (mEditor.getBlockIndex() != -1); } @Override public void run() { try { - displayResults(mLocalProvider.getAutoCompleteItems(mEditor, mPrefix, mInner, mColors, mLine), mTime); + displayResults(mLocalProvider.getAutoCompleteItems(mFileUri, mPrefix, mInner, mColors, mIndex, mLine, mColumn), mTime); } catch (Exception e) { e.printStackTrace(); displayResults(new ArrayList<>(), mTime); diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/EditorCompletionAdapter.java b/app/src/main/java/io/github/rosemoe/editor/widget/EditorCompletionAdapter.java index 3c3827144..150eaba9c 100644 --- a/app/src/main/java/io/github/rosemoe/editor/widget/EditorCompletionAdapter.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/EditorCompletionAdapter.java @@ -20,9 +20,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import com.itsaky.androidide.models.SuggestItem; -import com.itsaky.androidide.utils.Either; -import io.github.rosemoe.editor.struct.CompletionItem; import java.util.List; +import org.eclipse.lsp4j.CompletionItem; /** * A class to make custom adapter for auto-completion window @@ -32,7 +31,7 @@ import java.util.List; public abstract class EditorCompletionAdapter extends BaseAdapter { private EditorAutoCompleteWindow mWindow; - private List> mItems; + private List mItems; public void clear() { if(mItems != null) @@ -42,13 +41,13 @@ public abstract class EditorCompletionAdapter extends BaseAdapter { /** * Called by {@link EditorAutoCompleteWindow} to attach some arguments */ - public void attachAttributes(EditorAutoCompleteWindow window, List> items) { + public void attachAttributes(EditorAutoCompleteWindow window, List items) { mWindow = window; mItems = items; } @Override - public Either getItem(int position) { + public CompletionItem getItem(int position) { return mItems.get(position); } diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/EditorTextActionModeStarter.java b/app/src/main/java/io/github/rosemoe/editor/widget/EditorTextActionModeStarter.java index 9647cb442..49b428518 100644 --- a/app/src/main/java/io/github/rosemoe/editor/widget/EditorTextActionModeStarter.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/EditorTextActionModeStarter.java @@ -54,8 +54,6 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente } mActionMode = ((AppCompatActivity) mEditor.getContext()).startSupportActionMode(new ActionMode.Callback() { - private List actions; - @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_SELECT_TEXT; @@ -71,10 +69,7 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente android.R.attr.actionModePasteDrawable, }); - // TODO: Request code actions on selection change - actions = null; - - if(actions != null && actions.size() > 0) { + if(mEditor.isCodeActionsEnabled()) { menu.add(0, 8, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.msg_code_actions)) .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_quickfix)) .setShowAsActionFlags(1); @@ -100,13 +95,17 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente .setIcon(array.getDrawable(3)); } - menu.add(0, 4, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition)) - .setShowAsActionFlags(1) - .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition)); + if(mEditor.isGotoDefinitionEnabled()) { + menu.add(0, 4, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition)) + .setShowAsActionFlags(1) + .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition)); + } - menu.add(0, 5, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references)) - .setShowAsActionFlags(1) - .setIcon(createDrawable( com.itsaky.androidide.R.drawable.ic_find_references)); + if(mEditor.isFindReferencesEnabled()) { + menu.add(0, 5, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references)) + .setShowAsActionFlags(1) + .setIcon(createDrawable( com.itsaky.androidide.R.drawable.ic_find_references)); + } menu.add(0, 6, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_comment_line)) .setShowAsActionFlags(1) @@ -139,19 +138,25 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente final MenuItem uncomment = menu.findItem(7); final MenuItem codeActions = menu.findItem(8); - def.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76); - ref.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76); comment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76); uncomment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76); /** - * CodeActions menu item may, or may not be added + * Thesw menu items may, or may not be added * So we need to check if its null or not * before we perform any further actions */ if(codeActions != null) codeActions.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76); + if(def != null) { + def.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76); + } + + if(ref != null) { + ref.setEnabled(isJava).getIcon().setAlpha(isJava ? 255 : 76); + } + return true; } @@ -183,7 +188,7 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente mEditor.uncommentLine(); break; case 8: - mEditor.performCodeActions(actions); + mEditor.codeAction(); break; } if(menuItem.getItemId() != 0) { diff --git a/build.gradle b/build.gradle index d14e576ff..93e29af00 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,14 @@ buildscript { ext { - build_gradle_version = '4.0.1' - androidide_plugin_version = '1.0.6' + build_gradle_version = '7.0.1' } project.ext { - compileSdk = 30 - buildTools = "30.0.2" + compileSdk = 31 + buildTools = "31.0.0" minSdk = 21 targetSdk = 28 - versionCode = 102210808 - versionName = "1.0.2.1-alpha" + versionCode = 103210928 + versionName = "1.0.3-alpha" packageName = "com.itsaky.androidide" } @@ -27,7 +26,6 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:$build_gradle_version" - classpath "com.itsaky.androidide:gradle:$androidide_plugin_version" classpath 'com.github.megatronking.stringfog:gradle-plugin:2.2.1' classpath 'com.github.megatronking.stringfog:xor:1.1.0' classpath 'com.google.gms:google-services:4.3.8'