Working to support lsp4j

This commit is contained in:
Akash Yadav
2021-09-30 14:56:13 +05:30
committed by Akash Yadav
parent 9c6db435ad
commit 6e242015f4
39 changed files with 1637 additions and 501 deletions
+2 -1
View File
@@ -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'])
Binary file not shown.
Binary file not shown.
@@ -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<String> cps = mProject.getClassPaths();
JsonObject settings = new JsonObject();
JsonObject java = new JsonObject();
JsonArray classPath = new JsonArray();
for(int i=0;i<cps.size();i++) {
classPath.add(cps.get(i));
}
java.add("classPath", classPath);
settings.add("java", java);
DidChangeConfigurationParams params = new DidChangeConfigurationParams();
params.setSettings(settings);
javaServer.getWorkspaceService().didChangeConfiguration(params);
return null;
}, __ -> {
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<InitializeResult> 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());
@@ -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<SuggestItem, CompletionItem> 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 (
/**
@@ -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);
@@ -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);
@@ -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);
}
@@ -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<File, List<SearchResult>> results = new HashMap<>();
for(int i=0;i<references.size();i++) {
try {
final Location loc = references.get(i);
if(loc == null || loc.uri == null || loc.range == null) continue;
final File file = new File(loc.uri);
if(!file.exists() || !file.isFile()) continue;
EditorFragment frag = activity().getPagerAdapter().findEditorByFile(file);
Content content;
if(frag != null && frag.getEditor() != null)
content = frag.getEditor().getText();
else content = new Content(null, FileIOUtils.readFile2String(file));
final List<SearchResult> 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<references.size();i++) {
// try {
// final Location loc = references.get(i);
// if(loc == null || loc.uri == null || loc.range == null) continue;
// final File file = new File(loc.uri);
// if(!file.exists() || !file.isFile()) continue;
// EditorFragment frag = activity().getPagerAdapter().findEditorByFile(file);
// Content content;
// if(frag != null && frag.getEditor() != null)
// content = frag.getEditor().getText();
// else content = new Content(null, FileIOUtils.readFile2String(file));
// final List<SearchResult> 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);
}
@@ -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();
}
@@ -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;
@@ -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<String> ANDROIDX_ARTIFACTS = createArtifacts();
private static final List<String> CONFIGURATIONS = createConfigs();
private static final List<String> OTHERS = createOtherCompletions();
@Override
public List<Either<SuggestItem, CompletionItem>> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) {
List<Either<SuggestItem, CompletionItem>> 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<SuggestItem, CompletionItem> 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<SuggestItem, CompletionItem> 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<SuggestItem, CompletionItem> 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<CompletionItem> getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception {
List<CompletionItem> 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<String> createArtifacts() {
@@ -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() {
@@ -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<Either<List<CompletionItem>, CompletionList>> future;
public JavaAutoComplete(JavaLanguageAnalyzer analyzer) {
this.analyzer = analyzer;
public JavaAutoComplete(JavaLanguage lang) {
this.language = lang;
}
@Override
public List<Either<SuggestItem, CompletionItem>> 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<Integer, CompletionList> response = server.completion(p);
lastId = response.first;
CompletionList list = response.second;
if(list != null && list.items != null) {
List<Either<SuggestItem, CompletionItem>> result = new ArrayList<>();
for(int i=0;i<list.items.size();i++) {
com.itsaky.lsp.CompletionItem item = list.items.get(i);
if(item == null) continue;
Either<SuggestItem, CompletionItem> either = Either.forLeft(new CompletionItemWrapper(item, prefix));
result.add(either);
}
Collections.sort(result, RESULT_SORTER);
return result;
public List<CompletionItem> 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<CompletionItem>();
try {
Either<List<CompletionItem>, 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<Either<SuggestItem, CompletionItem>>();
return new ArrayList<CompletionItem>();
}
private static final Comparator<Either<SuggestItem, CompletionItem>> RESULT_SORTER = new Comparator<Either<SuggestItem, CompletionItem>>(){
private List<CompletionItem> sort(List<CompletionItem> items) {
Collections.sort(items, RESULT_SORTER);
return items;
}
private static final Comparator<CompletionItem> RESULT_SORTER = new Comparator<CompletionItem>(){
@Override
public int compare(Either<SuggestItem, CompletionItem> p1, Either<SuggestItem, CompletionItem> 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");
}
@@ -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() {
@@ -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<Either<SuggestItem, CompletionItem>> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) {
return new ArrayList<Either<SuggestItem, CompletionItem>>();
public List<CompletionItem> getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) {
return new ArrayList<CompletionItem>();
}
}
}
@@ -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<Either<SuggestItem, CompletionItem>> 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<CompletionItem> getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) {
final XMLCompletionService service = StudioApp.getInstance().getXmlCompletionService();
List<CompletionItem> list = new ArrayList<CompletionItem>(sort(service.complete(editor, prefix.trim())));
List<Either<SuggestItem, CompletionItem>> 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<CompletionListItem> sort(List<CompletionListItem> result) {
private List<CompletionItem> sort(List<CompletionItem> result) {
Collections.sort(result, RESULTS_SORTER);
return result;
}
@@ -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;
@@ -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<String, Attr> attrs;
@@ -50,11 +52,8 @@ public class XMLCompletionService {
}
public List<CompletionListItem> 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<CompletionListItem> result = new ArrayList<>();
public List<CompletionItem> complete(int index, int line, int column, String prefix) {
final List<CompletionItem> result = new ArrayList<>();
if(prefix.startsWith("<") || prefix.startsWith("</")) {
boolean slash = false;
prefix = prefix.substring(1).trim();
@@ -63,13 +62,17 @@ public class XMLCompletionService {
prefix = prefix.substring(1);
}
for(String name : widgets) {
if(simpleName(name).toLowerCase(Locale.US).contains(prefix))
if(simpleName(name).toLowerCase(Locale.US).startsWith(prefix))
result.add(widgetNameAsCompletion(name, slash));
}
return handleResults(result);
} else {
final IsInValueScanner scanner = new IsInValueScanner(editor.getCursor().getLeft());
final String name = scanner.scan(editor.getText().toString());
/**
* TODO: Change to LSP
*/
final String content = "";
final IsInValueScanner scanner = new IsInValueScanner(index);
final String name = scanner.scan(content);
if(name != null) {
final String attrName = name.contains(":") ? name.substring(name.indexOf(":") + 1) : name;
if (attrs.containsKey(attrName)) {
@@ -77,14 +80,14 @@ public class XMLCompletionService {
if(attr.hasPossibleValues()) {
Set<String> 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<String, Attr> 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<CompletionListItem> handleResults(List<CompletionListItem> result) {
private List<CompletionItem> handleResults(List<CompletionItem> result) {
return result;
}
@@ -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. <br>
* 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<? extends Location> locations) {
}
/**
* Called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show locations in EditorActivity
*/
public void showLocationLinks(List<? extends LocationLink> 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<ShowDocumentResult> 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);
}
}
@@ -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<InitializeResult> init(String rootPath) {
final LanguageServer server = LSPProvider.getServerForLanguage(LSPProvider.LANGUAGE_JAVA);
if (server == null)
return Optional.empty();
final List<WorkspaceFolder> 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<InitializeResult> 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;
}
}
@@ -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<Void> 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<LanguageServer> 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<Writable> 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");
}
@@ -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. <br>
* 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<String, LanguageServer> serverMap = new HashMap<>();
/**
* Maps a reference to the capabilities of a language server with its language id as a key
*/
private static final Map<String, ServerCapabilities> capabilitiesMap = new HashMap<>();
/**
* Maps a reference to the client of a language server with its language id as a key
*/
private static final Map<String, IDELanguageClient> 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<String, LanguageServer> getAvailableServers() {
return serverMap;
}
public static Map<String, ServerCapabilities> getAllServerCapabilities() {
return capabilitiesMap;
}
public static Map<String, IDELanguageClient> 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);
}
}
@@ -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<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams p1) {
return null;
}
@Override
public CompletableFuture<List<Object>> configuration(ConfigurationParams p1) {
return null;
}
@Override
public CompletableFuture<Void> 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<Void> refreshCodeLenses() {
return null;
}
@Override
public CompletableFuture<Void> refreshSemanticTokens() {
return null;
}
@Override
public CompletableFuture<Void> registerCapability(RegistrationParams p1) {
return null;
}
@Override
public void setTrace(SetTraceParams p1) {
LOG.info("setTrace: ", gson.toJson(p1));
}
@Override
public CompletableFuture<ShowDocumentResult> showDocument(ShowDocumentParams p1) {
return null;
}
@Override
public void showMessage(MessageParams p1) {
LOG.info("showMessage: ", gson.toJson(p1));
}
@Override
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams p1) {
return null;
}
@Override
public CompletableFuture<Void> unregisterCapability(UnregistrationParams p1) {
return null;
}
@Override
public CompletableFuture<List<WorkspaceFolder>> workspaceFolders() {
return null;
}
}
@@ -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<File> 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());
}
@@ -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;
@@ -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");
@@ -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();
}
@@ -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());
}
}
@@ -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));
@@ -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<Either<SuggestItem, CompletionItem>> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line);
List<CompletionItem> getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception;
}
@@ -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
*
@@ -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<Either<SuggestItem, CompletionItem>> getAutoCompleteItems(CodeEditor editor, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int line) {
return new ArrayList<Either<SuggestItem, CompletionItem>>();
public List<CompletionItem> getAutoCompleteItems(String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) {
return new ArrayList<CompletionItem>();
}
}
@@ -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();
}
}
}
@@ -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<HexColor, Integer> 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 <br>
* This method must be called only once in this editors lifetime <br>
* 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<Range> 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<Boolean, CodeActionOptions> codeActionOptions = c.getCodeActionProvider();
if(codeActionOptions.isLeft()) {
mCodeActionsEnabled = codeActionOptions.getLeft();
} else {
CodeActionOptions options = codeActionOptions.getRight();
mCodeActionsEnabled = options != null;
}
Either<Boolean, DefinitionOptions> definitionOptions = c.getDefinitionProvider();
if(definitionOptions.isLeft()) {
mGotoDefinitionEnabled = definitionOptions.getLeft();
} else {
DefinitionOptions options = definitionOptions.getRight();
mGotoDefinitionEnabled = options != null;
}
Either<Boolean, ReferenceOptions> 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
* <p>
*/
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<Either<List<? extends Location>, List<? extends LocationLink>>> future = mLanguageServer.getTextDocumentService().definition(params);
future.whenComplete((e, t) -> {
final Either<List<? extends Location>, List<? extends LocationLink>> 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<? extends Location> 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<? extends LocationLink> 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<CodeAction> 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<Range> strings = stringMap.get(line);
if(strings == null || strings.isEmpty()) return false;
for(int i=0;i<strings.size();i++) {
Range r = strings.get(i);
if(r.start.column <= column && column <= r.end.column)
return true;
}
return false;
}
private void toast(int msg, Toaster.Type type) {
StudioApp.getInstance().toast(msg, type);
}
private void toast(String msg, Toaster.Type type) {
StudioApp.getInstance().toast(msg, type);
}
@Override
public void afterInsert(Content content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) {
notifyChanged();
if(mDiagnosticWindow != null)
mDiagnosticWindow.hide();
notifyChanged();
// Update spans
if (isSpanMapPrepared(true, endLine - startLine)) {
if (startLine == endLine) {
@@ -4591,44 +4828,13 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
mListener.afterInsert(this, mText, startLine, startColumn, endLine, endColumn, insertedContent);
}
}
private boolean isInString() {
final int line = getCursor().getLeftLine();
final int column = getCursor().getLeftColumn();
if(stringMap == null || stringMap.isEmpty() || !stringMap.containsKey(line)) return false;
List<Range> strings = stringMap.get(line);
if(strings == null || strings.isEmpty()) return false;
for(int i=0;i<strings.size();i++) {
Range r = strings.get(i);
if(r.start.column <= column && column <= r.end.column)
return true;
}
return false;
}
private void notifyChanged() {
if (getFile() != null && getFile().getName().endsWith(".java") && jlsRequestor != null) {
TextDocumentContentChangeEvent event = new TextDocumentContentChangeEvent();
VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier();
DidChangeTextDocumentParams p = new DidChangeTextDocumentParams();
List<TextDocumentContentChangeEvent> 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<TextDocumentContentChangeEvent> createChangeEvents() {
final List<TextDocumentContentChangeEvent> events = new ArrayList<>();
TextDocumentContentChangeEvent event = new TextDocumentContentChangeEvent();
event.setText(getText().toString());
events.add(event);
return events;
}
public void onEndTextSelect() {
showTextActionPopup();
@@ -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<SuggestItem, CompletionItem> 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;
}
@@ -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<SuggestItem, CompletionItem> 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<TextEdit> edits = item.getAdditionalTextEdits();
if(edits != null && edits.size() > 0) {
for(int i=0;i<edits.size();i++) {
final TextEdit edit = edits.get(i);
if(edit == null || edit.getRange() == null || edit.getNewText() == null) continue;
final Position start = edit.getRange().getStart();
final Position end = edit.getRange().getEnd();
if(start == null || end == null) continue;
if(start.equals(end)) {
mEditor.getText().insert(start.getLine(), start.getCharacter(), edit.getNewText());
} else {
mEditor.getText().replace(start.getLine(), start.getCharacter(), end.getLine(), end.getCharacter(), edit.getNewText());
}
}
}
mEditor.postHideCompletionWindow();
}
private Range getIdentifierRange(int end, Content content) {
int start = end;
while(start > 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<Either<SuggestItem, CompletionItem>> results, long requestTime) {
public void displayResults(final List<CompletionItem> 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);
@@ -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<Either<SuggestItem, CompletionItem>> mItems;
private List<CompletionItem> 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<Either<SuggestItem, CompletionItem>> items) {
public void attachAttributes(EditorAutoCompleteWindow window, List<CompletionItem> items) {
mWindow = window;
mItems = items;
}
@Override
public Either<SuggestItem, CompletionItem> getItem(int position) {
public CompletionItem getItem(int position) {
return mItems.get(position);
}
@@ -54,8 +54,6 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
}
mActionMode = ((AppCompatActivity) mEditor.getContext()).startSupportActionMode(new ActionMode.Callback() {
private List<CodeAction> 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) {
+5 -7
View File
@@ -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'