Use ViewFlipper instead of ViewPager2 for editor fragments

This commit is contained in:
Akash Yadav
2022-01-07 20:36:18 +05:30
parent 1942020695
commit 119eb169f1
90 changed files with 8569 additions and 10118 deletions
+1
View File
@@ -13,6 +13,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/activity_designer.xml" value="0.264" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/activity_download.xml" value="0.1188034188034188" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/activity_editor.xml" value="0.264" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/fragment_editor.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/fragment_non_editable_editor.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_about_items.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_diagnostic_info.xml" value="0.20512820512820512" />
@@ -64,13 +64,11 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.EditorBottomSheetTabAdapter;
import com.itsaky.androidide.adapters.EditorPagerAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityEditorBinding;
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding;
import com.itsaky.androidide.databinding.LayoutSearchProjectBinding;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.fragments.FileTreeFragment;
import com.itsaky.androidide.fragments.SearchResultFragment;
import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
@@ -87,7 +85,6 @@ import com.itsaky.androidide.lsp.LSPProvider;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.models.OpenedFile;
import com.itsaky.androidide.models.SaveResult;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.models.SheetOption;
@@ -104,6 +101,7 @@ import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.utils.RecursiveFileSearcher;
import com.itsaky.androidide.utils.Symbols;
import com.itsaky.androidide.viewmodel.EditorViewModel;
import com.itsaky.androidide.views.CodeEditorView;
import com.itsaky.androidide.views.MaterialBanner;
import com.itsaky.androidide.views.SymbolInputView;
import com.itsaky.inflater.ILayoutInflater;
@@ -126,6 +124,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import io.github.rosemoe.editor.widget.CodeEditor;
@@ -136,13 +135,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
TabLayout.OnTabSelectedListener,
NavigationView.OnNavigationItemSelectedListener,
DiagnosticClickListener,
EditorFragment.FileOpenListener,
IDEHandler.Provider,
EditorActivityProvider {
private ActivityEditorBinding mBinding;
private LayoutDiagnosticInfoBinding mDiagnosticInfoBinding;
private EditorPagerAdapter mPagerAdapter;
private EditorBottomSheetTabAdapter bottomSheetTabAdapter;
private FileTreeFragment mFileTreeFragment;
private TreeNode mLastHeld;
@@ -155,10 +152,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
private ProgressSheet mSearchingProgress;
private AlertDialog mFindInProjectDialog;
private ActivityResultLauncher<Intent> mUIDesignerLauncher;
private EditorFragment mCurrentFragment;
public static File mCurrentFile;
@SuppressWarnings("rawtypes")
private EditorBottomSheetBehavior mEditorBottomSheet;
private EditorBottomSheetBehavior<? extends View> mEditorBottomSheet;
private EditorViewModel mViewModel;
@@ -206,33 +200,17 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
mViewModel = new ViewModelProvider (this).get (EditorViewModel.class);
getProjectFromIntent ();
mPagerAdapter = new EditorPagerAdapter (this, this);
mFileTreeFragment = FileTreeFragment.newInstance (this.getAndroidProject ()).setFileActionListener (this);
mDaemonStatusFragment = new TextSheetFragment ().setTextSelectable (true);
setupDrawerToggle ();
loadFragment (mFileTreeFragment);
mViewModel.observeFiles (this, files -> {
mPagerAdapter.setFiles (files);
});
symbolInput = new SymbolInputView (this);
mBinding.bottomSheet.textContainer.addView (symbolInput, 0, new ViewGroup.LayoutParams (-1, -2));
final var mediator = new TabLayoutMediator (mBinding.tabs,
mBinding.editorViewPager,
true,
false, // Do NOT enable smooth scrolls. Doing so results in error. Any workaround or fix will be appreciated.
(tab, position) -> tab.setText (mPagerAdapter.getEditorTitle (position)));
mBinding.editorViewPager.setUserInputEnabled (false);
mBinding.editorViewPager.setOffscreenPageLimit (9);
mBinding.editorViewPager.setAdapter (mPagerAdapter);
mBinding.tabs.addOnTabSelectedListener (this);
mediator.attach ();
setupEditorBottomSheet ();
createQuickActions ();
mBuildServiceHandler = new BuildServiceHandler (this);
@@ -245,7 +223,6 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
setupContainers ();
setupSignatureText ();
setupDiagnosticInfo ();
startLanguageServers ();
mUIDesignerLauncher = registerForActivityResult (new ActivityResultContracts.StartActivityForResult (), this::onGetUIDesignerResult);
@@ -272,8 +249,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
mBinding.bottomSheet.pager.setUserInputEnabled (false);
mBinding.bottomSheet.pager.setOffscreenPageLimit (bottomSheetTabAdapter.getItemCount () - 1); // DO not remove any views
//noinspection rawtypes
mEditorBottomSheet = (EditorBottomSheetBehavior) EditorBottomSheetBehavior.from (mBinding.bottomSheet.getRoot ());
mEditorBottomSheet = (EditorBottomSheetBehavior<? extends View>) EditorBottomSheetBehavior.from (mBinding.bottomSheet.getRoot ());
mEditorBottomSheet.setBinding (mBinding.bottomSheet);
mEditorBottomSheet.addBottomSheetCallback (new BottomSheetBehavior.BottomSheetCallback () {
@Override
@@ -316,30 +292,19 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
@Override
protected void onPause () {
new Thread (() -> {
boolean saved;
try {
saveAll (false /* No notification */);
saved = true;
} catch (Throwable th) {
LOG.error (getString (R.string.err_cannot_save_files), th);
saved = false;
}
if (!saved) {
ThreadUtils.runOnUiThread (() -> getApp ().toast (R.string.msg_failed_save, Toaster.Type.ERROR));
}
}, "AndroidIDE FileSaver").start ();
dispatchOnPauseToEditors ();
super.onPause ();
}
@Override
protected void onResume () {
super.onResume ();
try {
dispatchOnResumeToEditors ();
mFileTreeFragment.listProjectFiles ();
} catch (Throwable th) {
LOG.error ("Failed to update files list", th);
getApp ().toast (R.string.msg_failed_list_files, Toaster.Type.ERROR);
}
}
@@ -395,10 +360,15 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
viewLayout.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS);
}
final boolean notNull = mCurrentFile != null;
final boolean isJava = notNull && mCurrentFile.getName ().endsWith (".java");
final boolean isXml = notNull && mCurrentFile.getName ().endsWith (".xml");
final boolean isLayout = isXml && mCurrentFile.getParentFile () != null && Pattern.compile (FileOptionsHandler.LAYOUTRES_PATH_REGEX).matcher (mCurrentFile.getParentFile ().getAbsolutePath ()).matches ();
final var editor = getCurrentEditor ();
final var file = editor != null ? editor.getFile () : null;
final var notNull = editor != null && file != null;
final boolean isJava = notNull && file.getName ().endsWith (".java");
final boolean isXml = notNull && file.getName ().endsWith (".xml");
final boolean isLayout = isXml && file.getParentFile () != null &&
Pattern.compile (FileOptionsHandler.LAYOUTRES_PATH_REGEX)
.matcher (file.getParentFile ().getAbsolutePath ())
.matches ();
final int nullableAlpha = notNull ? 255 : 76;
final int javaFileAlpha = isJava ? 255 : 76;
final int layoutFileAlpha = isLayout ? 255 : 76;
@@ -435,6 +405,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
MenuBuilder builder = (MenuBuilder) menu;
builder.setOptionalIconsVisible (true);
}
getMenuInflater ().inflate (R.menu.menu_editor, menu);
return true;
}
@@ -442,8 +413,10 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
@Override
public boolean onOptionsItemSelected (MenuItem item) {
if (getBuildService () == null) {
LOG.error ("Build service is null. Cannot perform option menu actions.");
return false;
}
int id = item.getItemId ();
if (id == R.id.menuEditor_runDebug || id == R.id.menuEditor_quickRun) {
getBuildServiceHandler ().assembleDebug (true);
@@ -466,34 +439,32 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
} else if (id == R.id.menuEditor_lintRelease) {
getBuildService ().lintRelease ();
} else if (id == R.id.menuEditor_save) {
// * 1. Notify that all files are saved
// * 2. If there were any XML files modified, call ':app:processDebugResources' task
// *
// * This will make sure that we generate view bindings and R.jar at proper time
// * This will further result in updated code completion
saveAll (true, true);
} else if (id == R.id.menuEditor_undo && this.mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.undo ();
} else if (id == R.id.menuEditor_redo && this.mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.redo ();
} else if (id == R.id.menuEditor_gotoDefinition && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.findDefinition ();
} else if (id == R.id.menuEditor_findReferences && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.findReferences ();
} else if (id == R.id.menuEditor_commentLine && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.commentLine ();
} else if (id == R.id.menuEditor_uncommentLine && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.uncommentLine ();
} else if (id == R.id.menuEditor_findFile && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) {
this.mCurrentFragment.beginSearch ();
} else if (id == R.id.menuEditor_findProject) {
AlertDialog d = getFindInProjectDialog ();
if (d != null) {
d.show ();
}
} else if (id == R.id.menuEditor_viewLayout && mCurrentFile != null) {
} else if (id == R.id.menuEditor_viewLayout) {
previewLayout ();
} else if (getCurrentEditor () != null) { // Should be checked at last
if (id == R.id.menuEditor_undo) {
this.getCurrentEditor ().undo ();
} else if (id == R.id.menuEditor_redo) {
this.getCurrentEditor ().redo ();
} else if (id == R.id.menuEditor_gotoDefinition) {
this.getCurrentEditor ().findDefinition ();
} else if (id == R.id.menuEditor_findReferences) {
this.getCurrentEditor ().findReferences ();
} else if (id == R.id.menuEditor_commentLine) {
this.getCurrentEditor ().commentLine ();
} else if (id == R.id.menuEditor_uncommentLine) {
this.getCurrentEditor ().uncommentLine ();
} else if (id == R.id.menuEditor_findFile) {
this.getCurrentEditor ().beginSearch ();
}
}
invalidateOptionsMenu ();
return true;
}
@@ -578,11 +549,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
public void handleDiagnosticsResultVisibility (boolean errorVisible) {
bottomSheetTabAdapter.getDiagnosticsFragment ().handleResultVisibility (errorVisible);
runOnUiThread (() -> bottomSheetTabAdapter.getDiagnosticsFragment ().handleResultVisibility (errorVisible));
}
public void handleSearchResultVisibility (boolean errorVisible) {
bottomSheetTabAdapter.getSearchResultFragment ().handleResultVisibility (errorVisible);
runOnUiThread (() -> bottomSheetTabAdapter.getSearchResultFragment ().handleResultVisibility (errorVisible));
}
public void setStatus (final CharSequence text) {
@@ -619,10 +590,6 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
hideViewOptions ();
}
public EditorPagerAdapter getPagerAdapter () {
return mPagerAdapter;
}
public ActivityEditorBinding getBinding () {
return mBinding;
}
@@ -661,7 +628,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
public void openFileAndSelect (File file, org.eclipse.lsp4j.Range range) {
openFile (file, range);
EditorFragment opened = getEditorForFile (file);
final var opened = getEditorForFile (file);
if (opened != null && opened.getEditor () != null) {
CodeEditor editor = opened.getEditor ();
editor.post (() -> {
@@ -676,39 +643,49 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
@Override
public void onTabSelected (@NonNull TabLayout.Tab tab) {
final var opened = mPagerAdapter.getOpenedFile (tab.getPosition ());
final var fragment = getEditorAtIndex (tab.getPosition ());
if (opened != null && opened.getFile () != null && fragment != null) {
mCurrentFragment = fragment;
mCurrentFile = opened.getFile ();
refreshSymbolInput (mCurrentFragment);
}
final var position = tab.getPosition ();
mBinding.editorContainer.setDisplayedChild (position);
final var editorView = getEditorAtIndex (position);
mViewModel.setCurrentFile (position, Objects.requireNonNull (editorView).getFile ());
refreshSymbolInput (editorView);
invalidateOptionsMenu ();
}
@Override
public void onTabUnselected (@NonNull TabLayout.Tab tab) {
final var opened = mPagerAdapter.getOpenedFile (tab.getPosition ());
final var position = mViewModel.getCurrentFileIndex ();
if (position < 0 || position >= mViewModel.getOpenedFileCount ()) {
// This might happen when the file is being closed.
// In this case, the file has already been saved, so we don't need to save it again.
// see #close(int)
return;
}
final var opened = mViewModel.getOpenedFile (position);
if (opened == null) {
LOG.error ("No opened file at index", position);
return;
}
final var frag = getEditorForFile (opened.getFile ());
final var frag = getEditorAtIndex (position);
if (frag == null) {
LOG.error ("Cannot save unselected editor. Editor is null.");
return;
}
boolean isGradle = frag.isModified () && opened.getFile ().getName ().endsWith (EditorFragment.EXT_GRADLE);
boolean isGradle = frag.isModified () && opened.getName ().endsWith (".gradle");
frag.save ();
if (isGradle) {
LOG.info ("Gradle files have been modified. Sync needed.");
notifySyncNeeded ();
}
}
@Override
public void onTabReselected (TabLayout.Tab p1) {
mTabCloseAction.show (mBinding.tabs);
public void onTabReselected (@NonNull TabLayout.Tab tab) {
mTabCloseAction.show (tab.view);
}
@Override
@@ -783,16 +760,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
public void setDiagnosticsAdapter (@NonNull final DiagnosticsAdapter adapter) {
bottomSheetTabAdapter.getDiagnosticsFragment ().setAdapter (adapter);
runOnUiThread (() -> bottomSheetTabAdapter.getDiagnosticsFragment ().setAdapter (adapter));
}
public void setSearchResultAdapter (@NonNull final SearchListAdapter adapter) {
bottomSheetTabAdapter.getSearchResultFragment ().setAdapter (adapter);
}
@Override
public void onOpenSuccessful (File file, String text) {
// textDocument/didOpen is now handled by CodeEditor
runOnUiThread (() -> bottomSheetTabAdapter.getSearchResultFragment ().setAdapter (adapter));
}
@Override
@@ -838,8 +810,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
public void saveResult (int index, SaveResult result) {
if (index >= 0 && index < mViewModel.getOpenedFileCount ()) {
EditorFragment frag = getEditorAtIndex (index);
if (frag == null || frag.getFile () == null) {
var frag = getEditorAtIndex (index);
if (frag == null) {
return;
}
@@ -849,8 +821,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
frag.save ();
final boolean isGradle = frag.getFile ().getName ().endsWith (EditorFragment.EXT_GRADLE);
final boolean isXml = frag.getFile ().getName ().endsWith (EditorFragment.EXT_XML);
final boolean isGradle = frag.getFile ().getName ().endsWith (".gradle");
final boolean isXml = frag.getFile ().getName ().endsWith (".xml");
if (!result.gradleSaved) {
result.gradleSaved = modified && isGradle;
@@ -875,6 +847,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
new TaskExecutor ().executeAsync (() -> {
IDELanguageServer javaServer = LSPProvider.getServerForLanguage (LSPProvider.LANGUAGE_JAVA);
if (javaServer == null) {
LOG.error ("Cannot create services as java language server instance is null");
return null;
}
@@ -900,11 +873,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public EditorFragment openFile (File file) {
public CodeEditorView openFile (File file) {
return openFile (file, null);
}
public EditorFragment openFile (File file, Range selection) {
public CodeEditorView openFile (File file, Range selection) {
if (selection == null) {
selection = Range_ofZero;
}
@@ -919,13 +892,14 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
mBinding.editorDrawerLayout.closeDrawer (GravityCompat.END);
}
mBinding.editorViewPager.setCurrentItem (index);
mBinding.editorContainer.setDisplayedChild (index);
invalidateOptionsMenu ();
try {
return getEditorAtIndex (index);
} catch (Throwable th) {
LOG.error ("Unable to get editor fragment at opened file index (" + index + ").", th);
LOG.error ("Unable to get editor fragment at opened file index", index);
LOG.error (th);
return null;
}
}
@@ -942,86 +916,112 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
final var position = mViewModel.getOpenedFileCount ();
LOG.info ("Opening file at index:", position, "file: ", file);
mViewModel.addFile (new OpenedFile (file, selection));
final var editor = new CodeEditorView (this, file, selection);
editor.setLayoutParams (new ViewGroup.LayoutParams (
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
mBinding.editorContainer.addView (editor);
mBinding.editorContainer.setDisplayedChild (position);
mBinding.tabs.addTab (mBinding.tabs.newTab ().setText (file.getName ()));
mViewModel.addFile (file);
mViewModel.setCurrentFile (position, file);
return position;
}
public void closeFile (int index) {
mBinding.tabs.removeOnTabSelectedListener (this);
if (index >= 0 && index < mPagerAdapter.getItemCount ()) {// Save the file before closing
if (index >= 0 && index < mViewModel.getOpenedFileCount ()) {
var opened = mViewModel.getOpenedFile (index);
LOG.info ("Saving file before close. File: " + opened.getFile ());
final var fragment = getEditorAtIndex (index);
if (fragment != null) {
fragment.save ();
LOG.info ("Language server will be notified if available");
fragment.getEditor ().close (); // Send 'textDocument/didClose' to language servers.
LOG.info ("Closing file:", opened);
final var editor = getEditorAtIndex (index);
if (editor != null) {
editor.save ();
editor.getEditor ().close ();
} else {
LOG.error ("Cannot save file before close. Editor instance is null");
}
opened = null;
LOG.debug ("Removing file from file list...");
mViewModel.removeFile (index);
mBinding.tabs.removeTabAt (index);
mBinding.editorContainer.removeViewAt (index);
} else {
LOG.error ("Invalid file index. Cannot close any files");
mBinding.tabs.addOnTabSelectedListener (this);
LOG.error ("Invalid file index. Cannot close.");
return;
}
mBinding.tabs.addOnTabSelectedListener (this);
if (mPagerAdapter.getItemCount () <= 0) {
mCurrentFragment = null;
mCurrentFile = null;
}
mBinding.tabs.requestLayout ();
invalidateOptionsMenu ();
if (mPagerAdapter.getItemCount () == 0 &&
(mBinding.editorViewPager.getChildCount () != 0 || mBinding.tabs.getChildCount () != 0)) {
// TODO Find out why this happens
// Mostly for java files...
mBinding.editorViewPager.removeAllViews ();
mBinding.tabs.removeAllViews ();
}
}
public void closeAll () {
mBinding.tabs.removeOnTabSelectedListener (this);
// Close all files one by one
// This will make sure that we save all the files and send 'textDocument/didClose' to language servers.
for (int i = 0; i < mPagerAdapter. getItemCount (); i++) {
closeFile (i);
final var count = mViewModel.getOpenedFileCount ();
// Save and close all files one by one
for (int i = 0; i < count; i++) {
final var editor = getEditorAtIndex (i);
if (editor != null) {
editor.save ();
editor.getEditor ().close ();
} else {
LOG.error ("Unable to save file at index:", i);
}
}
mBinding.tabs.addOnTabSelectedListener (this);
mViewModel.removeAllFiles ();
mBinding.tabs.removeAllTabs ();
mBinding.tabs.requestLayout ();
mBinding.editorContainer.removeAllViews ();
invalidateOptionsMenu ();
}
public void closeOthers () {
final var index = mBinding.tabs.getSelectedTabPosition ();
for (int i = 0; i < mPagerAdapter. getItemCount (); i++) {
if (i != index) {
closeFile (i);
final var file = mViewModel.getCurrentFile ();
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
final var editor = getEditorAtIndex (i);
// Index of files changes as we keep close files
// So we compare the files instead of index
if (editor != null) {
if (!file.equals (editor.getFile ())) {
closeFile (i);
}
} else {
LOG.error ("Unable to save file at index:", i);
}
}
}
public EditorFragment getEditorAtIndex (final int index) {
return getEditorForFile (mViewModel.getOpenedFile (index).getFile ());
@Nullable
public CodeEditorView getCurrentEditor () {
if (mViewModel.getCurrentFileIndex () != -1) {
return getEditorAtIndex (mViewModel.getCurrentFileIndex ());
}
return null;
}
@Nullable
public EditorFragment getEditorForFile (@NonNull final File file) {
final var manager = getSupportFragmentManager ();
public CodeEditorView getEditorAtIndex (final int index) {
return (CodeEditorView) mBinding.editorContainer.getChildAt (index);
}
final var tag = "f" + file.getAbsolutePath ().hashCode ();
final var fragment = manager.findFragmentByTag (tag);
if (fragment == null) {
LOG.error ("No editor found for file: " + file);
return null;
@Nullable
public CodeEditorView getEditorForFile (@NonNull final File file) {
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
final CodeEditorView editor = (CodeEditorView) mBinding.editorContainer.getChildAt (i);
if (editor.getFile ().equals (file)) {
return editor;
}
}
return (EditorFragment) fragment;
return null;
}
public int findIndexOfEditorByFile (File file) {
@@ -1032,7 +1032,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
final var opened = mViewModel.getOpenedFile (i);
if (opened.getFile ().equals (file)) {
if (opened.equals (file)) {
return i;
}
}
@@ -1044,13 +1044,37 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
////////////// PRIVATE APIS /////////////////////
/////////////////////////////////////////////////
private void dispatchOnPauseToEditors () {
CompletableFuture.runAsync (() -> {
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
final var editor = getEditorAtIndex (i);
if (editor != null) {
editor.onPause ();
}
}
});
}
private void dispatchOnResumeToEditors () {
CompletableFuture.runAsync (() -> {
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
final var editor = getEditorAtIndex (i);
if (editor != null) {
editor.onResume ();
}
}
});
}
@Contract(pure = true)
private void onGetUIDesignerResult (@NonNull ActivityResult result) {
if (mCurrentFragment != null && mCurrentFragment.getEditor () != null && result.getResultCode () == RESULT_OK) {
final var index = mBinding.editorContainer.getDisplayedChild ();
final var editor = getEditorAtIndex (index);
if (editor != null && result.getResultCode () == RESULT_OK) {
final var data = result.getData ();
if (data != null && data.hasExtra (DesignerActivity.KEY_GENERATED_CODE)) {
final var code = data.getStringExtra (DesignerActivity.KEY_GENERATED_CODE);
mCurrentFragment.getEditor ().setText (code);
editor.getEditor ().setText (code);
saveAll ();
} else {
final var msg = getString (R.string.msg_invalid_designer_result);
@@ -1065,6 +1089,12 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
private void previewLayout () {
try {
if (getCurrentEditor () == null) {
LOG.error ("No file is opened. Cannot preview layout.");
return;
}
saveAll (false);
if (getApp ().getLayoutInflater () == null) {
LOG.info ("Creating layout inflater instance...");
@@ -1072,7 +1102,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
final Intent intent = new Intent (this, DesignerActivity.class);
intent.putExtra (DesignerActivity.KEY_LAYOUT_PATH, mCurrentFile.getAbsolutePath ());
intent.putExtra (DesignerActivity.KEY_LAYOUT_PATH, getCurrentEditor ().getFile ().getAbsolutePath ());
LOG.info ("Launching UI Designer...");
mUIDesignerLauncher.launch (intent);
@@ -1204,11 +1234,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
}
private void refreshSymbolInput (@NonNull EditorFragment frag) {
if (frag.getEditor () == null || frag.getFile () == null) {
return;
}
private void refreshSymbolInput (@NonNull CodeEditorView frag) {
symbolInput.bindEditor (frag.getEditor ());
symbolInput.setSymbols (Symbols.forFile (frag.getFile ()));
}
@@ -1225,7 +1251,11 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
private void closeProject (boolean manualFinish) {
if (getBuildService () != null) {
getBuildService ().setListener (null);
getBuildService ().exit ();
if (mBuildServiceHandler != null) {
mBuildServiceHandler.stop ();
} else {
getBuildService ().exit ();
}
}
// Make sure we close files
@@ -1300,7 +1330,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
final List<File> searchDirs = new ArrayList<> ();
for (int i = 0; i < binding.modulesContainer.getChildCount (); i++) {
for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) {
CheckBox check = (CheckBox) binding.modulesContainer.getChildAt (i);
if (check.isChecked ()) {
searchDirs.add (srcDirs.get (i));
@@ -1,4 +1,4 @@
/************************************************************************************
/*
* This file is part of AndroidIDE.
*
*
@@ -16,19 +16,15 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
*/
package com.itsaky.androidide;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -56,6 +52,8 @@ import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Callable;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
public class MainActivity extends StudioActivity implements View.OnClickListener, ProjectWriterCallback {
private ActivityMainBinding binding;
@@ -1,133 +0,0 @@
/*
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.adapters;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.DiffUtil;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.models.OpenedFile;
import com.itsaky.androidide.utils.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class EditorPagerAdapter extends FragmentStateAdapter implements EditorFragment.ModificationStateListener {
private final List<OpenedFile> mFiles;
private final EditorFragment.FileOpenListener mFileOpenListener;
private static final Logger LOG = Logger.instance ("EditorPagerAdapter");
public EditorPagerAdapter (final FragmentActivity activity, EditorFragment.FileOpenListener fileOpenListener) {
super (activity);
this.mFileOpenListener = fileOpenListener;
this.mFiles = new ArrayList<> ();
}
public void setFiles (List<OpenedFile> files) {
final var result = DiffUtil.calculateDiff (new DiffUtil.Callback () {
@Override
public int getOldListSize () {
return mFiles.size ();
}
@Override
public int getNewListSize () {
return files.size ();
}
@Override
public boolean areItemsTheSame (int oldItemPosition, int newItemPosition) {
return Objects.equals (mFiles.get (oldItemPosition), files.get (newItemPosition));
}
@Override
public boolean areContentsTheSame (int oldItemPosition, int newItemPosition) {
return areItemsTheSame (oldItemPosition, newItemPosition);
}
});
mFiles.clear ();
mFiles.addAll (files);
result.dispatchUpdatesTo (this);
}
public String getEditorTitle (int index) {
return mFiles.get (index).getFile ().getName ();
}
public List<OpenedFile> getFiles () {
return mFiles;
}
public OpenedFile getOpenedFile (int index) {
return mFiles.get (index);
}
@Override
public void onModified (EditorFragment editor) {
}
@Override
public void onSaved (EditorFragment editor) {
}
@NonNull
@Override
public Fragment createFragment (int position) {
return mFiles.get (position)
.createFragment (this.mFileOpenListener, this);
}
@Override
public int getItemCount () {
return mFiles.size ();
}
@Override
public long getItemId (int position) {
if (mFiles.isEmpty ()) {
LOG.warn ("File list is empty but getItemId(int) is being called. Returning -1");
return -1;
}
final var id = mFiles.get (position).hashCode ();
LOG.debug ("Item ID for editor fragment at position " + position + " is " + id);
return id;
}
@Override
public boolean containsItem (long itemId) {
for (final var file : mFiles) {
if (itemId == file.hashCode ()) { // see getItemId(int)
return true;
}
}
LOG.warn ("EditorPagerAdapter does not contain any file with item ID : " + itemId);
return false;
}
}
@@ -1,60 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.adapters;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
public class MainPagerAdapter extends FragmentPagerAdapter
{
private ArrayList<Fragment> mFragmentList;
public MainPagerAdapter(FragmentManager manager)
{
super(manager);
mFragmentList = new ArrayList<>();
}
@Override
public Fragment getItem(int p1)
{
return mFragmentList != null && p1 < mFragmentList.size() ? mFragmentList.get(p1) : null;
}
@Override
public int getCount()
{
return mFragmentList != null ? mFragmentList.size() : 0;
}
public MainPagerAdapter addFragment(Fragment frag)
{
getFragments().add(frag);
return this;
}
public ArrayList<Fragment> getFragments()
{
return mFragmentList == null ? mFragmentList = new ArrayList<>() : mFragmentList;
}
}
@@ -1,88 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.adapters;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.databinding.LayoutModuleInfoItemBinding;
import com.itsaky.androidide.project.IDEModule;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class ModuleInfoAdapter extends RecyclerView.Adapter<ModuleInfoAdapter.VH> {
private List<IDEModule> modules;
public ModuleInfoAdapter(List<IDEModule> modules) {
this.modules = modules;
if(this.modules == null)
this.modules = new ArrayList<>();
}
@Override
public ModuleInfoAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
return new VH(LayoutModuleInfoItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
@SuppressLint("SetTextI18n")
public void onBindViewHolder(ModuleInfoAdapter.VH p1, int p2) {
final IDEModule module = modules.get(p2);
final LayoutModuleInfoItemBinding binding = p1.binding;
final Context ctx = binding.getRoot().getContext();
binding.moduleName.setText(module.displayName.substring(0, 1).toUpperCase(Locale.US) + module.displayName.substring(1));
SpannableStringBuilder summaryBuilder = new SpannableStringBuilder();
if(!module.isLibrary) {
summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_package_name, module.applicationId));
summaryBuilder.append("\n");
}
summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_min_sdk, module.minSdk.apiString));
summaryBuilder.append("\n");
summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_target_sdk, module.targetSdk.apiString));
summaryBuilder.append("\n");
summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_dependency_count, module.dependencies.size()));
summaryBuilder.append("\n");
summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_task_count, module.tasks.size()));
binding.moduleSummary.setText(summaryBuilder);
}
@Override
public int getItemCount() {
return modules.size();
}
public class VH extends RecyclerView.ViewHolder {
LayoutModuleInfoItemBinding binding;
public VH(LayoutModuleInfoItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -24,19 +24,16 @@ import com.google.firebase.messaging.FirebaseMessaging;
import com.itsaky.androidide.CrashHandlerActivity;
import com.itsaky.androidide.language.xml.completion.XMLCompletionService;
import com.itsaky.androidide.project.ProjectResourceFinder;
import com.itsaky.androidide.services.IDELogService;
import com.itsaky.androidide.services.MessagingService;
import com.itsaky.inflater.ILayoutInflater;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.LayoutInflaterConfiguration;
import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.apiinfo.ApiInfo;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.inflater.ILayoutInflater;
import com.itsaky.inflater.IResourceFinder;
import com.itsaky.inflater.LayoutInflaterConfiguration;
import com.itsaky.widgets.WidgetInfo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Set;
public class StudioApp extends BaseApplication {
@@ -1,463 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.adapters.CompletionListAdapter;
import com.itsaky.androidide.databinding.FragmentEditorBinding;
import com.itsaky.androidide.language.groovy.GroovyLanguage;
import com.itsaky.androidide.language.java.JavaLanguage;
import com.itsaky.androidide.language.xml.XMLLanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ReadFileTask;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.utils.TypefaceUtils;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.github.rosemoe.editor.interfaces.EditorEventListener;
import io.github.rosemoe.editor.langs.EmptyLanguage;
import io.github.rosemoe.editor.widget.CodeEditor;
public class EditorFragment extends BaseFragment implements EditorEventListener {
public FragmentEditorBinding mBinding;
private File mFile;
private boolean isModified = false;
private boolean isFirstCreate = false;
private ModificationStateListener mModificationStateListener;
private FileOpenListener mOpenListener;
private static final Logger LOG = Logger.instance ("EditorFragment");
public static final String KEY_FILE_PATH = "file_path";
public static final String KEY_LINE_START = "line_start";
public static final String KEY_LINE_END = "line_end";
public static final String KEY_COLUMN_START = "col_start";
public static final String KEY_COLUMN_END = "col_end";
public static final String EXT_JAVA = ".java";
public static final String EXT_XML = ".xml";
public static final String EXT_HTML = ".html";
public static final String EXT_GRADLE = ".gradle";
public static final String EXT_GROOVY = ".groovy";
public static final String EXT_KOTLIN = ".kt";
public static final String EXT_JSON = ".json";
public EditorFragment setModificationStateListener (ModificationStateListener listener) {
this.mModificationStateListener = listener;
return this;
}
public FragmentEditorBinding getBinding () {
return mBinding;
}
@NonNull
public static EditorFragment newInstance (@NonNull File file, @NonNull Range selection) {
Bundle bundle = new Bundle ();
bundle.putString (KEY_FILE_PATH, file.getAbsolutePath ());
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 ());
EditorFragment frag = new EditorFragment ();
frag.setArguments (bundle);
return frag;
}
public EditorFragment setFileOpenListener (FileOpenListener openListener) {
this.mOpenListener = openListener;
return this;
}
public CodeEditor getEditor () {
if (mBinding == null) {
return null;
}
return mBinding.editor;
}
@Override
public void onCreate (Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
isFirstCreate = true;
}
@Override
public void onSaveInstanceState (@NonNull Bundle outState) {
save ();
super.onSaveInstanceState (outState);
}
@Override
public View onCreateView (@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mBinding = FragmentEditorBinding.inflate (inflater, container, false);
return mBinding.getRoot ();
}
@Override
public void onViewCreated (@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated (view, savedInstanceState);
LOG.debug ("onViewCreated() called");
mFile = new File (requireArguments ().getString (KEY_FILE_PATH));
mBinding.editor.setOverScrollEnabled (false);
mBinding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ());
mBinding.editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE);
mBinding.editor.setHighlightCurrentBlock (true);
mBinding.editor.setEventListener (this);
mBinding.editor.setAutoCompletionOnComposing (true);
mBinding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ());
mBinding.editor.setLineColorsEnabled (true);
mBinding.editor.setDividerWidth (SizeUtils.dp2px (1));
mBinding.diagnosticTextContainer.setVisibility (View.GONE);
LOG.debug ("Configuring editor from preferences...");
configureEditorIfNeeded ();
LOG.debug ("Reading initial selection range");
final Range range = fromArgs (requireArguments ());
new TaskExecutor ().executeAsync (new ReadFileTask (mFile), result -> {
mBinding.editor.setText (result);
postRead ();
mBinding.editor.post (() -> {
if (LSPUtils.isEqual (range.getStart (), range.getEnd ())) {
getEditor ().setSelection (range.getStart ().getLine (), range.getStart ().getCharacter ());
} else {
getEditor ().setSelectionRegion (range.getStart ().getLine (), range.getStart ().getCharacter (), range.getEnd ().getLine (), range.getEnd ().getCharacter ());
}
});
});
}
private Range fromArgs (@NonNull Bundle args) {
if (!(args.containsKey (KEY_LINE_START)
&& args.containsKey (KEY_COLUMN_START)
&& args.containsKey (KEY_LINE_END)
&& args.containsKey (KEY_COLUMN_END))) {
return LSPUtils.Range_ofZero;
}
return new Range (
new Position (
args.getInt (KEY_LINE_START),
args.getInt (KEY_COLUMN_START)),
new Position (
args.getInt (KEY_LINE_END),
args.getInt (KEY_COLUMN_END)
));
}
public void setDiagnostics (List<Diagnostic> diags) {
if (diags == null) {
mBinding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ());
return;
}
Map<Integer, Map<Integer, Diagnostic>> map = new HashMap<> ();
for (int i = 0; i < diags.size (); i++) {
final Diagnostic d = diags.get (i);
if (d == null) {
continue;
}
final Range range = d.getRange ();
final int line = range.getStart ().getLine ();
final int column = range.getStart ().getCharacter ();
Map<Integer, Diagnostic> mappedByColumn = map.get (line);
if (mappedByColumn == null) {
mappedByColumn = new HashMap<> ();
}
mappedByColumn.put (column, d);
map.put (line, mappedByColumn);
}
mBinding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (map);
}
private void configureEditorIfNeeded () {
boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITORPREF_SIZE_CHANGED;
boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITORPREF_FLAGS_CHANGED;
boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED;
final PreferenceManager prefs = getStudioActivity ().getApp ().getPrefManager ();
if (sizeChanged) {
float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE);
if (textSize < 6 || textSize > 32) {
textSize = 14;
}
mBinding.editor.setTextSize (textSize);
ConstantsBridge.EDITORPREF_SIZE_CHANGED = false;
}
if (flagsChanged) {
int flags = 0;
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_LEADING, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_LEADING;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_TRAILING, false)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_INNER, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_INNER;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_EMPTY_LINE, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_LINE_BREAK, true)) {
flags |= CodeEditor.FLAG_DRAW_LINE_SEPARATOR;
}
mBinding.editor.setNonPrintablePaintingFlags (flags);
ConstantsBridge.EDITORPREF_FLAGS_CHANGED = false;
}
if (drawHexChanged) {
mBinding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true));
ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED = false;
}
isFirstCreate = false;
}
public String getText () {
return mBinding.editor.getText ().toString ();
}
public boolean isModified () {
return isModified;
}
public void undo () {
if (mBinding.editor.canUndo ()) {
mBinding.editor.undo ();
}
}
public void redo () {
if (mBinding.editor.canRedo ()) {
mBinding.editor.redo ();
}
}
public void findDefinition () {
mBinding.editor.findDefinition ();
}
public void findReferences () {
mBinding.editor.findReferences ();
}
public void commentLine () {
mBinding.editor.commentLine ();
}
public void uncommentLine () {
mBinding.editor.uncommentLine ();
}
public void beginSearch () {
mBinding.editor.beginSearchMode ();
}
public void save () {
if (mFile == null || mBinding == null) {
return;
}
final String text = mBinding.editor.getText ().toString ();
FileIOUtils.writeFileFromString (mFile, text);
notifySaved ();
isModified = false;
}
public File getFile () {
return mFile;
}
@Override
public void onResume () {
super.onResume ();
configureEditorIfNeeded ();
}
private void postRead () {
if (mFile.isFile () && mFile.getName ().endsWith (EXT_JAVA)) {
mBinding.editor.setEditorLanguage (new JavaLanguage (getFile ()));
} else if (mFile.isFile () && mFile.getName ().endsWith (EXT_XML)) {
mBinding.editor.setEditorLanguage (new XMLLanguage (getFile ()));
} else if (mFile.isFile () && mFile.getName ().endsWith (EXT_GRADLE)) {
mBinding.editor.setEditorLanguage (new GroovyLanguage (getFile ()));
} else {
mBinding.editor.setEditorLanguage (new EmptyLanguage ());
}
// File must be set only after setting the language
// This will make sure that textDocument/didOpen is sent
mBinding.editor.setFile (getFile ());
mBinding.editor.setColorScheme (new SchemeAndroidIDE ());
if (mOpenListener != null) {
mOpenListener.onOpenSuccessful (getFile (), getText ());
}
}
private void closeCurrentTag (String text, int line, int col) {
try {
XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text)));
Token token;
boolean wasSlash = false, wasOpen = false;
ArrayList<String> currentNames = new ArrayList<> ();
while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) {
final int type = token.getType ();
if (type == XMLLexer.OPEN) {
wasOpen = true;
} else if (type == XMLLexer.Name) {
if (wasOpen && wasSlash && currentNames.size () > 0) {
currentNames.remove (0);
} else if (wasOpen) {
currentNames.add (0, token.getText ());
wasOpen = false;
}
} else if (type == XMLLexer.OPEN_SLASH) {
int l = token.getLine () - 1;
int c = token.getCharPositionInLine ();
if (l == line && c == col) {
break;
} else if (currentNames.size () > 0) {
currentNames.remove (0);
}
} else if (type == XMLLexer.SLASH_CLOSE
|| type == XMLLexer.SPECIAL_CLOSE) {
if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) {
currentNames.remove (0);
}
} else if (type == XMLLexer.SLASH) {
wasSlash = true;
} else {
wasOpen = wasSlash = false;
}
}
if (currentNames.size () > 0) {
mBinding.editor.getText ().insert (line, col + 2, currentNames.get (0));
}
} catch (Throwable th) {
LOG.error ("Unable to close current tag", th);
}
}
@Override
public boolean onRequestFormat (CodeEditor editor, boolean async) {
return false;
}
@Override
public boolean onFormatFail (CodeEditor editor, Throwable cause) {
return false;
}
@Override
public void onFormatSucceed (CodeEditor editor) {
}
@Override
public void onNewTextSet (CodeEditor editor) {
}
@Override
public void onSetSelection (int startLine, int startCol, int endLine, int endCol) {
}
@Override
public void afterDelete (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) {
isModified = true;
notifyModified ();
}
@Override
public void afterInsert (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) {
isModified = true;
if (getFile () != null && getFile ().getName ().endsWith (EXT_XML)) {
boolean isOpen = false;
try {
isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<';
} catch (Throwable th) {
// ignored
}
if (isOpen && insertedContent.toString ().equals ("/")) {
closeCurrentTag (editor.getText ().toString (), endLine, endColumn);
}
}
notifyModified ();
}
@Override
public void beforeReplace (CodeEditor editor, CharSequence content) {
}
private void notifySaved () {
mBinding.editor.didSave ();
if (mModificationStateListener != null) {
mModificationStateListener.onSaved (this);
}
}
private void notifyModified () {
if (mModificationStateListener != null) {
mModificationStateListener.onModified (this);
}
}
public interface FileOpenListener {
void onOpenSuccessful (File file, String text);
}
public interface ModificationStateListener {
void onModified (EditorFragment editor);
void onSaved (EditorFragment editor);
}
}
@@ -28,10 +28,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileIOUtils;
import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
@@ -41,11 +42,10 @@ import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.toaster.Toaster;
import com.itsaky.androidide.views.CodeEditorView;
import com.unnamed.b.atv.model.TreeNode;
import com.unnamed.b.atv.view.AndroidTreeView;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import com.blankj.utilcode.util.FileUtils;
@@ -65,6 +65,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
return this;
}
@NonNull
public static FileTreeFragment newInstance(AndroidProject project) {
Bundle bundle = new Bundle();
bundle.putParcelable("project", project);
@@ -82,8 +83,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (getActivity() == null || getArguments() == null || !getArguments().containsKey("project")) return;
mProject = getArguments().getParcelable("project");
mProject = requireArguments ().getParcelable("project");
listProjectFiles();
}
@@ -102,12 +102,12 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
expandNode(node);
} else {
setLoading(node);
listNode(node, true);
listNode(node);
}
}
}
private void listNode(TreeNode node, boolean wasClicked) {
private void listNode (@NonNull TreeNode node) {
node.getChildren().clear();
node.setExpanded(false);
new TaskExecutor().executeAsync(() -> {
@@ -136,13 +136,13 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
}
}
private void setLoading(TreeNode node) {
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void updateChevron(TreeNode node) {
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
@@ -180,9 +180,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
final File gradleProps = Environment.GRADLE_PROPS;
final File gradleHome = Environment.GRADLE_USER_HOME;
final File root = StudioApp.getInstance().getRootDir().getParentFile();
File projectDir = new File(mProject.getProjectPath());
// projectDir = root;
mRoot = TreeNode.root(projectDir);
if(gradleHome.exists() && gradleHome.isDirectory()) {
if(!gradleProps.exists())
@@ -235,7 +233,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN
}
public interface FileActionListener {
public EditorFragment openFile(File file);
public void showFileOptions(File file, TreeNode node);
CodeEditorView openFile(File file);
void showFileOptions(File file, TreeNode node);
}
}
@@ -28,6 +28,8 @@ import android.view.View;
import android.widget.LinearLayout;
import com.itsaky.androidide.databinding.LayoutProgressSheetBinding;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.utils.Logger;
import android.widget.RelativeLayout;
public class ProgressSheet extends BaseBottomSheetFragment {
@@ -39,6 +41,8 @@ public class ProgressSheet extends BaseBottomSheetFragment {
private boolean subMessageEnabled = false;
private boolean welcomTextEnabled = false;
private final Logger LOG = Logger.instance ("ProgressSheet");
@Override
protected void bind(LinearLayout container) {
binding = LayoutProgressSheetBinding.inflate(LayoutInflater.from(getContext()));
@@ -56,14 +60,18 @@ public class ProgressSheet extends BaseBottomSheetFragment {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) binding.message.getLayoutParams();
try {
p.removeRule(RelativeLayout.CENTER_VERTICAL);
} catch (Throwable th) {}
} catch (Throwable th) {
LOG.error ("Unable to remove center_vertical rule.", th);
}
binding.message.setLayoutParams(p);
} else {
binding.subMessage.setVisibility(View.GONE);
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) binding.message.getLayoutParams();
try {
p.addRule(RelativeLayout.CENTER_VERTICAL);
} catch (Throwable th) {}
} catch (Throwable th) {
LOG.error ("Unable to remove center_vertical rule.", th);
}
binding.message.setLayoutParams(p);
}
@@ -26,7 +26,6 @@ import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding;
import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
import com.itsaky.androidide.lsp.LSP;
import com.itsaky.androidide.models.SheetOption;
@@ -298,7 +297,7 @@ public class FileOptionsHandler extends IDEHandler implements OptionsListFragmen
activity().getFileTreeFragment().listProjectFiles();
}
EditorFragment frag = activity().getEditorForFile (f);
final var frag = activity().getEditorForFile (f);
if(frag != null) {
activity().closeFile(activity().findIndexOfEditorByFile (frag.getFile ()));
}
@@ -58,7 +58,7 @@ public abstract class IDEHandler {
/**
* An interface to communicate between a handler and its client
*/
public static interface Provider {
public interface Provider {
/**
* Called by handler to get a reference to {@link EditorActivity}
@@ -1,4 +1,4 @@
/************************************************************************************
/*
* This file is part of AndroidIDE.
*
*
@@ -16,15 +16,14 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
*/
package com.itsaky.androidide.lsp;
import android.content.DialogInterface;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.transition.TransitionManager;
@@ -39,9 +38,7 @@ import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.FragmentEditorBinding;
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.models.DiagnosticGroup;
@@ -50,11 +47,39 @@ import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.views.CodeEditorView;
import com.itsaky.lsp.SemanticHighlight;
import com.itsaky.lsp.services.IDELanguageClient;
import com.itsaky.toaster.Toaster;
import io.github.rosemoe.editor.text.Content;
import io.github.rosemoe.editor.widget.CodeEditor;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.ConfigurationParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.LogTraceParams;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.ParameterInformation;
import org.eclipse.lsp4j.ProgressParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
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.SignatureHelp;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.UnregistrationParams;
import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
@@ -62,35 +87,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.LogTraceParams;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.ParameterInformation;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ShowDocumentParams;
import org.eclipse.lsp4j.ShowDocumentResult;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.SetTraceParams;
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
import org.eclipse.lsp4j.ProgressParams;
import org.eclipse.lsp4j.ConfigurationParams;
import org.eclipse.lsp4j.RegistrationParams;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.eclipse.lsp4j.UnregistrationParams;
import io.github.rosemoe.editor.text.Content;
import io.github.rosemoe.editor.widget.CodeEditor;
/**
* AndroidIDE specific implementation of the LanguageClient
@@ -99,13 +101,11 @@ public class IDELanguageClientImpl implements IDELanguageClient {
protected static final Gson gson = new Gson();
protected static final Logger LOG = Logger.instance("AbstractLanguageClient");
public static final int DIAGNOSTIC_TRANSITION_DURATION = 80;
private final Map<File, List<Diagnostic>> diagnostics = new HashMap<>();
private static IDELanguageClientImpl mInstance;
protected EditorActivityProvider activityProvider;
private boolean isConnected;
private IDELanguageClientImpl (EditorActivityProvider provider) {
setActivityProvider(provider);
@@ -137,26 +137,19 @@ public class IDELanguageClientImpl implements IDELanguageClient {
this.activityProvider = provider;
}
/**
* Are we connected to the server?
*/
public boolean isConnected() {
return isConnected;
}
protected EditorActivity activity() {
if (activityProvider == null) return null;
return activityProvider.provide();
}
private EditorFragment findEditorByFile (File file) {
private CodeEditorView findEditorByFile (File file) {
return activity ().getEditorForFile (file);
}
@Override
public void semanticHighlights(SemanticHighlight highlights) {
final File file = new File(URI.create(highlights.uri));
final EditorFragment editor = findEditorByFile(file);
final var editor = findEditorByFile(file);
if(editor != null) {
editor.getEditor().setSemanticHighlights(highlights);
@@ -200,13 +193,13 @@ public class IDELanguageClientImpl implements IDELanguageClient {
return;
}
final EditorFragment frag = findEditorByFile(file);
if(frag == null || frag.getBinding() == null) {
final var frag = findEditorByFile(file);
if(frag == null) {
hideBottomDiagnosticView(file);
return;
}
final FragmentEditorBinding binding = frag.getBinding();
final var binding = frag.getBinding();
binding.diagnosticTextContainer.setVisibility(View.VISIBLE);
binding.diagnosticText.setVisibility(View.VISIBLE);
binding.diagnosticText.setClickable(false);
@@ -219,13 +212,12 @@ public class IDELanguageClientImpl implements IDELanguageClient {
}
future.whenComplete((a, b) -> {
final Throwable error = b;
if(a == null || a.isEmpty()) {
hideBottomDiagnosticView(file);
return;
}
final List<CodeAction> actions = a.stream().filter(e -> e.isRight()).map (e -> e.getRight()).collect(Collectors.toList());
if(actions == null || actions.isEmpty()) {
final List<CodeAction> actions = a.stream().filter(Either::isRight).map (Either::getRight).collect(Collectors.toList());
if(actions.isEmpty()) {
hideBottomDiagnosticView(file);
return;
}
@@ -236,9 +228,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
sb.append(diagnostic.getMessage());
binding.diagnosticText.setText(sb);
binding.diagnosticText.setClickable(true);
binding.diagnosticText.setOnClickListener(v -> {
showAvailableQuickfixes(editor, actions);
});
binding.diagnosticText.setOnClickListener(v -> showAvailableQuickfixes(editor, actions));
});
});
}
@@ -256,7 +246,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
return;
}
final EditorFragment frag = findEditorByFile(file);
final var frag = findEditorByFile(file);
if(frag == null || frag.getBinding() == null) {
return;
}
@@ -277,7 +267,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
SignatureInformation info = signatureWithMostParams(signature);
if(info == null) return;
activity().getBinding().symbolText.setText(formatSignature(info, signature.getActiveParameter()));
final EditorFragment frag = findEditorByFile(file);
final var frag = findEditorByFile(file);
if(frag != null) {
final CodeEditor editor = frag.getEditor();
final float[] cursor = editor.getCursorPosition();
@@ -325,7 +315,8 @@ public class IDELanguageClientImpl implements IDELanguageClient {
* @param signature Signature information
* @param paramIndex Currently active parameter index
*/
private CharSequence formatSignature(SignatureInformation signature, int paramIndex) {
@NonNull
private CharSequence formatSignature(@NonNull SignatureInformation signature, int paramIndex) {
String name = signature.getLabel();
name = name.substring(0, name.indexOf("("));
@@ -362,7 +353,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
diagnostics.put(file, params.getDiagnostics());
activity().setDiagnosticsAdapter(newDiagnosticsAdapter());
EditorFragment editor;
CodeEditorView editor;
if((editor = findEditorByFile(file)) != null) {
editor.setDiagnostics(params.getDiagnostics());
}
@@ -394,13 +385,13 @@ public class IDELanguageClientImpl implements IDELanguageClient {
if(loc == null || loc.getUri() == null || loc.getRange() == null) continue;
final File file = new File(URI.create(loc.getUri()));
if(!file.exists() || !file.isFile()) continue;
EditorFragment frag = findEditorByFile(file);
var frag = 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(
Objects.requireNonNull (matches).add(
new SearchResult(
loc.getRange(),
file,
@@ -415,7 +406,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
);
results.put(file, matches);
} catch (Throwable th) {
LOG.error ("Failed to show file location", th);
}
}
@@ -434,9 +425,9 @@ public class IDELanguageClientImpl implements IDELanguageClient {
showLocations(locations
.stream()
.filter(l -> l != null)
.map(l -> asLocation(l))
.filter(l -> l != null)
.filter(Objects::nonNull)
.map(this::asLocation)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
}
@@ -462,14 +453,9 @@ public class IDELanguageClientImpl implements IDELanguageClient {
progress.show(activity().getSupportFragmentManager(), "quick_fix_progress");
new TaskExecutor().executeAsyncProvideError(() -> performCodeActionAsync(editor, action), (a, b) -> {
final Boolean complete = a;
final Throwable error = b;
progress.dismiss();
if(complete == null || error != null || !complete.booleanValue()) {
if(a == null || b != null || !a) {
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
return;
}
});
}
@@ -491,7 +477,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
File file = new File(URI.create(params.getUri()));
if(file.exists() && file.isFile() && FileUtils.isUtf8(file)) {
final Range range = params.getSelection();
EditorFragment frag = activity().getEditorAtIndex (
var frag = activity().getEditorAtIndex (
activity().getBinding ().tabs.getSelectedTabPosition ());
if(frag != null
&& frag.getFile() != null
@@ -554,13 +540,10 @@ public class IDELanguageClientImpl implements IDELanguageClient {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ());
builder.setTitle(R.string.msg_code_actions);
builder.setItems(asArray(actions), (d, w) -> {
final DialogInterface dialog = d;
final int which = w;
dialog.dismiss();
d.dismiss();
hideDiagnostics();
hideBottomDiagnosticView(editor.getFile());
performCodeAction(editor, actions.get(which));
performCodeAction(editor, actions.get(w));
});
builder.show();
}
@@ -589,7 +572,7 @@ public class IDELanguageClientImpl implements IDELanguageClient {
// Edit is in the same editor which requested the code action
editInEditor(editor, edit);
} else {
EditorFragment openedFrag = findEditorByFile(file);
var openedFrag = findEditorByFile(file);
if(openedFrag != null && openedFrag.getEditor() != null) {
// Edit is in another 'opened' file
@@ -67,13 +67,6 @@ public abstract class LSPClientLauncher extends Thread {
return this.server;
}
/**
* Are we connected to the server?
*/
public boolean isConnected() {
return languageClient != null && languageClient.isConnected();
}
/**
* Actual logic to start the server
*/
@@ -1,467 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.lsp;
import com.itsaky.lsp.SemanticHighlight;
import com.itsaky.lsp.SemanticHighlightsParams;
import com.itsaky.lsp.services.IDELanguageServer;
import com.itsaky.lsp.services.IDETextDocumentService;
import com.itsaky.lsp.services.IDEWorkspaceService;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams;
import org.eclipse.lsp4j.CallHierarchyItem;
import org.eclipse.lsp4j.CallHierarchyOutgoingCall;
import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams;
import org.eclipse.lsp4j.CallHierarchyPrepareParams;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.ColorInformation;
import org.eclipse.lsp4j.ColorPresentation;
import org.eclipse.lsp4j.ColorPresentationParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.CreateFilesParams;
import org.eclipse.lsp4j.DeclarationParams;
import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.DeleteFilesParams;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentColorParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentHighlight;
import org.eclipse.lsp4j.DocumentHighlightParams;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentLinkParams;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.FoldingRangeRequestParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.ImplementationParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.LinkedEditingRangeParams;
import org.eclipse.lsp4j.LinkedEditingRanges;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Moniker;
import org.eclipse.lsp4j.MonikerParams;
import org.eclipse.lsp4j.PrepareRenameParams;
import org.eclipse.lsp4j.PrepareRenameResult;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameFilesParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.ResolveTypeHierarchyItemParams;
import org.eclipse.lsp4j.SelectionRange;
import org.eclipse.lsp4j.SelectionRangeParams;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.SemanticTokensDelta;
import org.eclipse.lsp4j.SemanticTokensDeltaParams;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensRangeParams;
import org.eclipse.lsp4j.SetTraceParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.TypeDefinitionParams;
import org.eclipse.lsp4j.TypeHierarchyItem;
import org.eclipse.lsp4j.TypeHierarchyParams;
import org.eclipse.lsp4j.WillSaveTextDocumentParams;
import org.eclipse.lsp4j.WorkDoneProgressCancelParams;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
/**
* Wraps a {@link LanguageServer} to an {@link IDELanguageServer }
*/
public class LanguageServerWrapper implements IDELanguageServer {
private final LanguageServer server;
private final IDETextDocumentService textDocumentServiceWrapper;
private final IDEWorkspaceService workspaceService;
public LanguageServerWrapper(LanguageServer server) {
this.server = server;
this.textDocumentServiceWrapper = new TextDocumentServiceWrapper(server.getTextDocumentService());
this.workspaceService = new WorkspaceServiceWrapper(server.getWorkspaceService());
}
public LanguageServer getServer() {
return this.server;
}
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
return server.initialize(params);
}
@Override
public void initialized(InitializedParams params) {
server.initialized(params);
}
@Override
public void initialized() {
server.initialized();
}
@Override
public CompletableFuture<Object> shutdown() {
return server.shutdown();
}
@Override
public void exit() {
server.exit();
}
@Override
public IDETextDocumentService getTextDocumentService() {
return textDocumentServiceWrapper;
}
@Override
public IDEWorkspaceService getWorkspaceService() {
return workspaceService;
}
@Override
public void cancelProgress(WorkDoneProgressCancelParams params) {
server.cancelProgress(params);
}
@Override
public void setTrace(SetTraceParams params) {
}
class TextDocumentServiceWrapper implements IDETextDocumentService {
private final TextDocumentService service;
public TextDocumentServiceWrapper(TextDocumentService service) {
this.service = service;
}
@Override
public CompletableFuture<List<CallHierarchyIncomingCall>> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams p1) {
return service.callHierarchyIncomingCalls(p1);
}
@Override
public CompletableFuture<List<CallHierarchyOutgoingCall>> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams p1) {
return service.callHierarchyOutgoingCalls(p1);
}
@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams p1) {
return service.codeAction(p1);
}
@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams p1) {
return service.codeLens(p1);
}
@Override
public CompletableFuture<List<ColorPresentation>> colorPresentation(ColorPresentationParams p1) {
return service.colorPresentation(p1);
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams p1) {
return service.completion(p1);
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> declaration(DeclarationParams p1) {
return service.declaration(p1);
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams p1) {
return service.definition(p1);
}
@Override
public void didChange(DidChangeTextDocumentParams p1) {
service.didChange(p1);
}
@Override
public void didClose(DidCloseTextDocumentParams p1) {
service.didClose(p1);
}
@Override
public void didOpen(DidOpenTextDocumentParams p1) {
service.didOpen(p1);
}
@Override
public void didSave(DidSaveTextDocumentParams p1) {
service.didSave(p1);
}
@Override
public CompletableFuture<List<ColorInformation>> documentColor(DocumentColorParams p1) {
return service.documentColor(p1);
}
@Override
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(DocumentHighlightParams p1) {
return service.documentHighlight(p1);
}
@Override
public CompletableFuture<List<DocumentLink>> documentLink(DocumentLinkParams p1) {
return service.documentLink(p1);
}
@Override
public CompletableFuture<DocumentLink> documentLinkResolve(DocumentLink p1) {
return service.documentLinkResolve(p1);
}
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams p1) {
return service.documentSymbol(p1);
}
@Override
public CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams p1) {
return service.foldingRange(p1);
}
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams p1) {
return service.formatting(p1);
}
@Override
public CompletableFuture<Hover> hover(HoverParams p1) {
return service.hover(p1);
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> implementation(ImplementationParams p1) {
return service.implementation(p1);
}
@Override
public CompletableFuture<LinkedEditingRanges> linkedEditingRange(LinkedEditingRangeParams p1) {
return service.linkedEditingRange(p1);
}
@Override
public CompletableFuture<List<Moniker>> moniker(MonikerParams p1) {
return service.moniker(p1);
}
@Override
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams p1) {
return service.onTypeFormatting(p1);
}
@Override
public CompletableFuture<List<CallHierarchyItem>> prepareCallHierarchy(CallHierarchyPrepareParams p1) {
return service.prepareCallHierarchy(p1);
}
@Override
public CompletableFuture<Either<Range, PrepareRenameResult>> prepareRename(PrepareRenameParams p1) {
return service.prepareRename(p1);
}
@Override
public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams p1) {
return service.rangeFormatting(p1);
}
@Override
public CompletableFuture<List<? extends Location>> references(ReferenceParams p1) {
return service.references(p1);
}
@Override
public CompletableFuture<WorkspaceEdit> rename(RenameParams p1) {
return service.rename(p1);
}
@Override
public CompletableFuture<CodeAction> resolveCodeAction(CodeAction p1) {
return service.resolveCodeAction(p1);
}
@Override
public CompletableFuture<CodeLens> resolveCodeLens(CodeLens p1) {
return service.resolveCodeLens(p1);
}
@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem p1) {
return service.resolveCompletionItem(p1);
}
@Override
public CompletableFuture<TypeHierarchyItem> resolveTypeHierarchy(ResolveTypeHierarchyItemParams p1) {
return service.resolveTypeHierarchy(p1);
}
@Override
public CompletableFuture<List<SelectionRange>> selectionRange(SelectionRangeParams p1) {
return service.selectionRange(p1);
}
@Override
public CompletableFuture<SemanticTokens> semanticTokensFull(SemanticTokensParams p1) {
return service.semanticTokensFull(p1);
}
@Override
public CompletableFuture<Either<SemanticTokens, SemanticTokensDelta>> semanticTokensFullDelta(SemanticTokensDeltaParams p1) {
return service.semanticTokensFullDelta(p1);
}
@Override
public CompletableFuture<SemanticTokens> semanticTokensRange(SemanticTokensRangeParams p1) {
return service.semanticTokensRange(p1);
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams p1) {
return service.signatureHelp(p1);
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> typeDefinition(TypeDefinitionParams p1) {
return service.typeDefinition(p1);
}
@Override
public CompletableFuture<TypeHierarchyItem> typeHierarchy(TypeHierarchyParams p1) {
return service.typeHierarchy(p1);
}
@Override
public void willSave(WillSaveTextDocumentParams p1) {
service.willSave(p1);
}
@Override
public CompletableFuture<List<TextEdit>> willSaveWaitUntil(WillSaveTextDocumentParams p1) {
return service.willSaveWaitUntil(p1);
}
@Override
public CompletableFuture<List<SemanticHighlight>> semanticHighlights(SemanticHighlightsParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
}
class WorkspaceServiceWrapper implements IDEWorkspaceService {
private final WorkspaceService service;
public WorkspaceServiceWrapper(WorkspaceService service) {
this.service = service;
}
@Override
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
return service.executeCommand(params);
}
@Override
public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) {
return service.symbol(params);
}
@Override
public void didChangeConfiguration(DidChangeConfigurationParams params) {
service.didChangeConfiguration(params);
}
@Override
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
service.didChangeWatchedFiles(params);
}
@Override
public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) {
service.didChangeWorkspaceFolders(params);
}
@Override
public CompletableFuture<WorkspaceEdit> willCreateFiles(CreateFilesParams params) {
return service.willCreateFiles(params);
}
@Override
public void didCreateFiles(CreateFilesParams params) {
service.didCreateFiles(params);
}
@Override
public CompletableFuture<WorkspaceEdit> willRenameFiles(RenameFilesParams params) {
return service.willRenameFiles(params);
}
@Override
public void didRenameFiles(RenameFilesParams params) {
service.didRenameFiles(params);
}
@Override
public CompletableFuture<WorkspaceEdit> willDeleteFiles(DeleteFilesParams params) {
return service.willDeleteFiles(params);
}
@Override
public void didDeleteFiles(DeleteFilesParams params) {
service.didDeleteFiles(params);
}
}
}
@@ -1,56 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.lsp;
import org.eclipse.lsp4j.*;
public enum TokenModifiers {
PUBLIC("public"),
PRIVATE("private"),
PROTECTED("protected"),
NATIVE("native"),
DEFAULT("default"),
STRICTFP("strictp"),
SYNCHRONIZED("synchronized"),
TRANSIENT("transient"),
VOLATILE("volatile"),
ABSTRACT(SemanticTokenModifiers.Abstract),
STATIC(SemanticTokenModifiers.Static),
FINAL(SemanticTokenModifiers.Readonly),
DEPRECATED(SemanticTokenModifiers.Deprecated);
public final int bitmask = 1 << ordinal();
public final int invertBitmask = ~bitmask;
private String name;
private TokenModifiers (String name) {
this.name = name;
}
@Override
public String toString () {
return name;
}
}
@@ -1,66 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.lsp;
import org.eclipse.lsp4j.SemanticTokenTypes;
import io.github.rosemoe.editor.widget.EditorColorScheme;
public enum TokenType {
/**
* Order of the member declaration must be preseved!
*/
NAMESPACE(SemanticTokenTypes.Namespace, EditorColorScheme.PACKAGE_NAME),
CLASS(SemanticTokenTypes.Class, EditorColorScheme.TYPE_NAME),
INTERFACE(SemanticTokenTypes.Interface, EditorColorScheme.INTERFACE),
ENUM(SemanticTokenTypes.Enum, EditorColorScheme.ENUM),
ENUM_MEMBER(SemanticTokenTypes.EnumMember, EditorColorScheme.STATIC_FIELD),
TYPE(SemanticTokenTypes.Type, EditorColorScheme.TYPE_NAME),
TYPE_PARAMETER(SemanticTokenTypes.TypeParameter, EditorColorScheme.TYPE_PARAM),
METHOD(SemanticTokenTypes.Method, EditorColorScheme.METHOD_DECLARATION),
PROPERTY(SemanticTokenTypes.Property, EditorColorScheme.FIELD),
VARIABLE(SemanticTokenTypes.Variable, EditorColorScheme.LOCAL_VARIABLE),
PARAMETER(SemanticTokenTypes.Parameter, EditorColorScheme.PARAMETER),
MODIFIER(SemanticTokenTypes.Modifier, EditorColorScheme.KEYWORD),
KEYWORD(SemanticTokenTypes.Keyword, EditorColorScheme.KEYWORD),
ANNOTATION("annotation", EditorColorScheme.ANNOTATION),
ANNOTATION_TYPE("annotationType", EditorColorScheme.TYPE_NAME),
UNKNOWN("unknown", EditorColorScheme.TEXT_NORMAL);
private String name;
private int editorType;
private TokenType (String name, int editorType) {
this.name = name;
this.editorType = editorType;
}
public int getEditorType() {
return editorType;
}
@Override
public String toString () {
return name;
}
}
@@ -1,60 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.project.AndroidProject;
import org.eclipse.lsp4j.Range;
import java.io.File;
/**
* Represents a file that has been opened in the editor,
*
* @author Akash Yadav
*/
public class OpenedFile {
private final File file;
private final Range selection;
public OpenedFile (File file, Range selection) {
this.file = file;
this.selection = selection;
}
public File getFile () {
return file;
}
public Range getSelection () {
return selection;
}
public int hashCode () {
return this.file.getAbsolutePath ().hashCode ();
}
public EditorFragment createFragment (final EditorFragment.FileOpenListener fileOpenListener,
EditorFragment.ModificationStateListener modificationStateListener) {
return EditorFragment.newInstance (this.file, this.selection)
.setModificationStateListener (modificationStateListener)
.setFileOpenListener (fileOpenListener);
}
}
@@ -1,69 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.services;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.shell.ShellServer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class IDELogService {
private final ShellServer shell;
private final List<LogReceiver.LogListener> logListeners;
private boolean started;
public IDELogService () {
this.shell = StudioApp.getInstance ().newShell (this::output);
this.logListeners = new ArrayList<> ();
}
public void start () {
if (started) {
return;
}
this.shell.bgAppend ("logcat -v threadtime");
started = true;
}
public boolean isStarted () {
return started;
}
public void addLogListener (LogReceiver.LogListener listener) {
this.logListeners.add (Objects.requireNonNull (listener));
}
public void removeLogListener (LogReceiver.LogListener listener) {
this.logListeners.remove (Objects.requireNonNull (listener));
}
private void output (@NonNull String log) {
final var line = LogLine.forLogString (log);
for (final var listener : this.logListeners) {
listener.appendLogLine (line);
}
}
}
@@ -25,6 +25,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.utils.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -35,6 +37,8 @@ public class LogReceiver extends BroadcastReceiver {
public static final String APPEND_LOG = "com.itsaky.androidide.logs.APPEND_LOG";
public static final String EXTRA_LINE = "log_line";
private final Logger LOG = Logger.instance ("LogReceiver");
public LogReceiver setLogListener(LogListener listener) {
this.listener = listener;
return this;
@@ -47,7 +51,9 @@ public class LogReceiver extends BroadcastReceiver {
if(line == null) return;
try {
sendLogLine(line);
} catch (Throwable th) {}
} catch (Throwable th) {
LOG.error ("Unable to parse log line from app.", th);
}
}
}
@@ -58,7 +64,7 @@ public class LogReceiver extends BroadcastReceiver {
}
}
public static interface LogListener {
public void appendLogLine(LogLine line);
public interface LogListener {
void appendLogLine (LogLine line);
}
}
@@ -34,6 +34,8 @@ public class TaskExecutor {
private final Executor executor = Executors.newSingleThreadExecutor();
private final Handler handler = new Handler(Looper.getMainLooper());
private final Logger LOG = Logger.instance ("TaskExecutor");
public interface Callback<R> {
void complete(R result);
}
@@ -49,7 +51,9 @@ public class TaskExecutor {
handler.post(() -> {
callback.complete(result);
});
} catch (Throwable th) {}
} catch (Throwable th) {
LOG.error ("Callable task was not able to finish", th);
}
});
}
@@ -61,6 +65,7 @@ public class TaskExecutor {
try {
result = callable.call();
} catch (Throwable th) {
LOG.error ("Callable task was not able to finish", th);
error = th;
}
final R resultCopied = result;
@@ -1,40 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.callables;
import com.blankj.utilcode.util.FileIOUtils;
import java.io.File;
import java.util.concurrent.Callable;
public class ReadFileTask implements Callable<String> {
private final File file;
public ReadFileTask(File file) {
this.file = file;
}
@Override
public String call() throws Exception {
return FileIOUtils.readFile2String(file);
}
}
@@ -1,276 +0,0 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.utils;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {@code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*
* <h3>Serialization and deserialization</h3>
* In order to serialize and deserialize a polymorphic object,
* you must specify the base type explicitly.
* <pre> {@code
* Diamond diamond = new Diamond();
* String json = gson.toJson(diamond, Shape.class);
* }</pre>
* And then:
* <pre> {@code
* Shape shape = gson.fromJson(json, Shape.class);
* }</pre>
*/
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private final boolean maintainType;
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
this.maintainType = maintainType;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
* {@code maintainType} flag decide if the type will be stored in pojo or not.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, maintainType);
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, false);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(baseType, "type", false);
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement;
if (maintainType) {
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
@Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (maintainType) {
Streams.write(jsonObject, out);
return;
}
JsonObject clone = new JsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}.nullSafe();
}
}
@@ -22,7 +22,6 @@
package com.itsaky.androidide.utils;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.views.SymbolInputView.Symbol;
import java.io.File;
@@ -30,11 +29,11 @@ public class Symbols {
public static Symbol[] forFile(File file) {
if (file.isFile()) {
if (file.getName().endsWith(EditorFragment.EXT_JAVA)
|| file.getName().endsWith(EditorFragment.EXT_GRADLE))
if (file.getName().endsWith(".java")
|| file.getName().endsWith(".gradle"))
return javaSymbols();
if (file.getName().endsWith(EditorFragment.EXT_XML))
if (file.getName().endsWith(".xml"))
return xmlSymbols();
}
@@ -17,6 +17,7 @@
package com.itsaky.androidide.viewmodel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.lifecycle.LifecycleOwner;
@@ -24,7 +25,6 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import com.itsaky.androidide.models.OpenedFile;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEProject;
@@ -38,7 +38,7 @@ import java.util.Objects;
*/
public class EditorViewModel extends ViewModel {
private final MutableLiveData <List<OpenedFile>> mFiles = new MutableLiveData<> (new ArrayList<> ());
private final MutableLiveData <List<File>> mFiles = new MutableLiveData<> (new ArrayList<> ());
private final MutableLiveData <AndroidProject> mProject = new MutableLiveData<> (null);
private final MutableLiveData <IDEProject> mIDEProject = new MutableLiveData<> (null);
@@ -87,7 +87,7 @@ public class EditorViewModel extends ViewModel {
* Add the given file to the list of opened files.
* @param file The file that has been opened.
*/
public void addFile (final OpenedFile file) {
public void addFile (final File file) {
final var files = mFiles.getValue ();
Objects.requireNonNull (files).add (file);
mFiles.setValue (files);
@@ -103,12 +103,17 @@ public class EditorViewModel extends ViewModel {
mFiles.setValue (files);
}
public void removeAllFiles () {
mFiles.setValue (new ArrayList<> ());
setCurrentFile (-1, null);
}
/**
* Get the opened file at the given index.
* @param index The index of the file.
* @return The file at the given index.
*/
public OpenedFile getOpenedFile (final int index) {
public File getOpenedFile (final int index) {
return Objects.requireNonNull (mFiles.getValue ()).get (index);
}
@@ -125,7 +130,27 @@ public class EditorViewModel extends ViewModel {
* @param lifecycleOwner The lifecycle owner.
* @param observer The observer.
*/
public void observeFiles (LifecycleOwner lifecycleOwner, Observer<List<OpenedFile>> observer) {
public void observeFiles (LifecycleOwner lifecycleOwner, Observer<List<File>> observer) {
this.mFiles.observe (lifecycleOwner, observer);
}
public int getCurrentFileIndex () {
if (mCurrentFile.getValue () == null) {
return -1;
}
return mCurrentFile.getValue ().first;
}
public File getCurrentFile () {
if (mCurrentFile.getValue () == null) {
return null;
}
return mCurrentFile.getValue ().second;
}
public void setCurrentFile (final int index, @Nullable final File file) {
mCurrentFile.setValue (Pair.create (index, file));
}
}
@@ -0,0 +1,378 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.adapters.CompletionListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutCodeEditorBinding;
import com.itsaky.androidide.language.groovy.GroovyLanguage;
import com.itsaky.androidide.language.java.JavaLanguage;
import com.itsaky.androidide.language.xml.XMLLanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.utils.TypefaceUtils;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;
import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import io.github.rosemoe.editor.interfaces.EditorEventListener;
import io.github.rosemoe.editor.langs.EmptyLanguage;
import io.github.rosemoe.editor.widget.CodeEditor;
/**
* A view that handles opened code editors.
*
* @author Akash Yadav
*/
@SuppressLint("ViewConstructor") // This view is always dynamically created.
public class CodeEditorView extends FrameLayout {
private final File file;
private final LayoutCodeEditorBinding binding;
private boolean isModified;
private boolean isFirstCreate;
private static final Logger LOG = Logger.instance ("CodeEditorView");
private final EditorEventListener mEventListener = new EditorEventListener () {
@Override
public boolean onRequestFormat (CodeEditor editor, boolean async) {
return false;
}
@Override
public boolean onFormatFail (CodeEditor editor, Throwable cause) {
return false;
}
@Override
public void onFormatSucceed (CodeEditor editor) {
}
@Override
public void onNewTextSet (CodeEditor editor) {
}
@Override
public void beforeReplace (CodeEditor editor, CharSequence content) {
}
@Override
public void onSetSelection (int startLine, int startCol, int endLine, int endCol) {
}
@Override
public void afterDelete (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) {
isModified = true;
}
@Override
public void afterInsert (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) {
isModified = true;
if (file.getName ().endsWith (".xml")) {
boolean isOpen = false;
try {
isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<';
} catch (Throwable th) {
LOG.error (th);
}
if (isOpen && insertedContent.toString ().equals ("/")) {
closeCurrentTag (editor.getText ().toString (), endLine, endColumn);
}
}
}
};
public CodeEditorView (@NonNull Context context, @NonNull File file, final @NonNull Range selection) {
super (context);
this.file = file;
this.isModified = false;
this.isFirstCreate = true;
this.binding = LayoutCodeEditorBinding.inflate (LayoutInflater.from (context));
this.binding.editor.setOverScrollEnabled (false);
this.binding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ());
this.binding.editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE);
this.binding.editor.setHighlightCurrentBlock (true);
this.binding.editor.setEventListener (mEventListener);
this.binding.editor.setAutoCompletionOnComposing (true);
this.binding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ());
this.binding.editor.setLineColorsEnabled (true);
this.binding.editor.setDividerWidth (SizeUtils.dp2px (1));
this.binding.editor.setColorScheme (new SchemeAndroidIDE ());
this.binding.diagnosticTextContainer.setVisibility (GONE);
removeAllViews ();
addView (this.binding.getRoot ());
CompletableFuture.runAsync (() -> {
final var contents = FileIOUtils.readFile2String (file);
binding.editor.post (() -> {
binding.editor.setText (contents);
postRead ();
if (LSPUtils.isEqual (selection.getStart (), selection.getEnd ())) {
getEditor ().setSelection (selection.getStart ().getLine (), selection.getStart ().getCharacter ());
} else {
getEditor ().setSelectionRegion (
selection.getStart ().getLine (),
selection.getStart ().getCharacter (),
selection.getEnd ().getLine (),
selection.getEnd ().getCharacter ()
);
}
});
});
}
public boolean save () {
final var text = getText ();
final var saved = FileIOUtils.writeFileFromString (getFile (), text);
notifySaved ();
isModified = false;
return saved;
}
public Editor getEditor () {
return binding.editor;
}
public LayoutCodeEditorBinding getBinding () {
return binding;
}
public File getFile () {
return file;
}
public String getText () {
return binding.editor.getText ().toString ();
}
public boolean isModified () {
return isModified;
}
public void onPause () {
CompletableFuture.runAsync (this::save);
}
public void onResume () {
configureEditorIfNeeded ();
}
public void undo () {
if (binding.editor.canUndo ()) {
binding.editor.undo ();
}
}
public void redo () {
if (binding.editor.canRedo ()) {
binding.editor.redo ();
}
}
public void findDefinition () {
binding.editor.findDefinition ();
}
public void findReferences () {
binding.editor.findReferences ();
}
public void commentLine () {
binding.editor.commentLine ();
}
public void uncommentLine () {
binding.editor.uncommentLine ();
}
public void beginSearch () {
binding.editor.beginSearchMode ();
}
public void setDiagnostics (List<Diagnostic> diagnostics) {
if (diagnostics == null) {
LOG.info ("Clearing diagnostics of code editor", "file:", file);
binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ());
return;
}
Map<Integer, Map<Integer, Diagnostic>> map = new HashMap<> ();
for (int i = 0; i < diagnostics.size (); i++) {
final Diagnostic d = diagnostics.get (i);
if (d == null) {
continue;
}
final Range range = d.getRange ();
final int line = range.getStart ().getLine ();
final int column = range.getStart ().getCharacter ();
Map<Integer, Diagnostic> mappedByColumn = map.get (line);
if (mappedByColumn == null) {
mappedByColumn = new HashMap<> ();
}
mappedByColumn.put (column, d);
map.put (line, mappedByColumn);
}
binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (map);
}
protected void postRead () {
if (file.isFile () && file.getName ().endsWith (".java")) {
binding.editor.setEditorLanguage (new JavaLanguage (getFile ()));
} else if (file.isFile () && file.getName ().endsWith (".xml")) {
binding.editor.setEditorLanguage (new XMLLanguage (getFile ()));
} else if (file.isFile () && file.getName ().endsWith (".gradle")) {
binding.editor.setEditorLanguage (new GroovyLanguage (getFile ()));
} else {
binding.editor.setEditorLanguage (new EmptyLanguage ());
}
// File must be set only after setting the language
// This will make sure that textDocument/didOpen is sent
binding.editor.setFile (getFile ());
}
private void configureEditorIfNeeded () {
boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITORPREF_SIZE_CHANGED;
boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITORPREF_FLAGS_CHANGED;
boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED;
final PreferenceManager prefs = StudioApp.getInstance ().getPrefManager ();
if (sizeChanged) {
float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE);
if (textSize < 6 || textSize > 32) {
textSize = 14;
}
binding.editor.setTextSize (textSize);
ConstantsBridge.EDITORPREF_SIZE_CHANGED = false;
}
if (flagsChanged) {
int flags = 0;
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_LEADING, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_LEADING;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_TRAILING, false)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_INNER, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_INNER;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_EMPTY_LINE, true)) {
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE;
}
if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_LINE_BREAK, true)) {
flags |= CodeEditor.FLAG_DRAW_LINE_SEPARATOR;
}
binding.editor.setNonPrintablePaintingFlags (flags);
ConstantsBridge.EDITORPREF_FLAGS_CHANGED = false;
}
if (drawHexChanged) {
binding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true));
ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED = false;
}
isFirstCreate = false;
}
private void closeCurrentTag (String text, int line, int col) {
try {
XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text)));
Token token;
boolean wasSlash = false, wasOpen = false;
ArrayList<String> currentNames = new ArrayList<> ();
while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) {
final int type = token.getType ();
if (type == XMLLexer.OPEN) {
wasOpen = true;
} else if (type == XMLLexer.Name) {
if (wasOpen && wasSlash && currentNames.size () > 0) {
currentNames.remove (0);
} else if (wasOpen) {
currentNames.add (0, token.getText ());
wasOpen = false;
}
} else if (type == XMLLexer.OPEN_SLASH) {
int l = token.getLine () - 1;
int c = token.getCharPositionInLine ();
if (l == line && c == col) {
break;
} else if (currentNames.size () > 0) {
currentNames.remove (0);
}
} else if (type == XMLLexer.SLASH_CLOSE
|| type == XMLLexer.SPECIAL_CLOSE) {
if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) {
currentNames.remove (0);
}
} else if (type == XMLLexer.SLASH) {
wasSlash = true;
} else {
wasOpen = wasSlash = false;
}
}
if (currentNames.size () > 0) {
binding.editor.getText ().insert (line, col + 2, currentNames.get (0));
}
} catch (Throwable th) {
LOG.error ("Unable to close current tag", th);
}
}
private void notifySaved () {
binding.editor.didSave ();
}
}
@@ -1,77 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.views;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.LinearLayout;
import android.graphics.Path;
import android.graphics.Paint;
import android.graphics.Color;
public class TooltipLayout extends LinearLayout {
private final float dp8;
private final Path path;
private final Paint paint;
public TooltipLayout (Context ctx) {
this(ctx, null);
}
public TooltipLayout (Context ctx, AttributeSet attrs) {
this(ctx, attrs, 0);
}
public TooltipLayout (Context ctx, AttributeSet attrs, int style) {
super(ctx, attrs, style);
dp8 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
path = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.parseColor("#424242"));
}
@Override
protected void onDraw(Canvas canvas) {
final int width = getWidth();
final int height = getHeight();
final float midX = width / 2;
path.moveTo(0, 0);
path.lineTo(width, 0);
path.lineTo(width, height - dp8);
path.lineTo(midX + dp8, height - dp8);
path.lineTo(midX, height );
path.lineTo(midX - dp8, height - dp8);
path.lineTo(0, height - dp8);
path.close();
canvas.drawPath(path, paint);
super.onDraw(canvas);
}
}
@@ -1,113 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.viewpager.widget.ViewPager;
public class VerticalViewPager extends ViewPager
{
public static final String TAG = "VerticalViewPager";
private boolean scrollEnabled = true;
public VerticalViewPager(Context context)
{
super(context);
init();
}
public VerticalViewPager(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void setScrollEnabled(boolean scrollEnabled)
{
this.scrollEnabled = scrollEnabled;
}
public boolean isScrollEnabled()
{
return scrollEnabled;
}
private void init()
{
setPageTransformer(true, new VerticalPageTransformer());
setOverScrollMode(OVER_SCROLL_NEVER);
}
private class VerticalPageTransformer implements ViewPager.PageTransformer
{
@Override
public void transformPage(View view, float position)
{
if (position < -1)
{
view.setAlpha(0);
} else if (position <= 1)
{
view.setAlpha(1);
view.setTranslationX(view.getWidth() * -position);
float yPosition = position * view.getHeight();
view.setTranslationY(yPosition);
} else
{
view.setAlpha(0);
}
}
}
/**
* Swaps the X and Y coordinates of your touch event.
*/
private MotionEvent swapXY(MotionEvent ev)
{
float width = getWidth();
float height = getHeight();
float newX = (ev.getY() / height) * width;
float newY = (ev.getX() / width) * height;
ev.setLocation(newX, newY);
return ev;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
swapXY(ev); // return touch coordinates to original reference frame for any child views
return intercepted;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev)
{
return isScrollEnabled() ? super.onTouchEvent(swapXY(ev)) : true;
}
}
@@ -4618,16 +4618,15 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
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;
if (either == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
if (e == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
LOG.error ("An error occurred while finding definition", t);
showDefinitionNotFound (pd);
return;
}
if (either.isLeft ()) {
final List<? extends Location> locations = either.getLeft ();
if (e.isLeft ()) {
final List<? extends Location> locations = e.getLeft ();
if (locations == null || locations.size () <= 0) {
showDefinitionNotFound (pd);
return;
@@ -4643,8 +4642,8 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
});
dismissOnUiThread (pd);
} else if (either.isRight ()) {
final List<? extends LocationLink> locations = either.getRight ();
} else if (e.isRight ()) {
final List<? extends LocationLink> locations = e.getRight ();
if (mLanguageClient == null || locations == null || locations.size () <= 0) {
showDefinitionNotFound (pd);
return;
@@ -4663,6 +4662,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
}
});
} catch (Throwable th) {
LOG.error ("An error occurred while finding definition", th);
showDefinitionNotFound (pd);
}
}
@@ -4690,26 +4690,26 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
final CompletableFuture<List<? extends Location>> future = mLanguageServer.getTextDocumentService ().references (params);
future.whenComplete ((l, t) -> {
final List<? extends Location> locations = l;
final Throwable th = t;
if (locations == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
if (l == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
LOG.error ("An error occurred while finding references", t);
showReferencesNotFound (pd);
return;
}
if (locations.isEmpty ()) {
if (l.isEmpty ()) {
showReferencesNotFound (pd);
return;
} else {
ThreadUtils.runOnUiThread (() -> {
mLanguageClient.showLocations (locations);
mLanguageClient.showLocations (l);
});
}
dismissOnUiThread (pd);
});
} catch (Throwable th) {
LOG.error ("An error occurred while finding references", th);
showReferencesNotFound (pd);
}
}
@@ -4741,24 +4741,20 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
CompletableFuture<SignatureHelp> future = mLanguageServer.getTextDocumentService ().signatureHelp (params);
future.whenComplete ((h, t) -> {
final SignatureHelp help = h;
final Throwable th = t;
if (help == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
if (h == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) {
LOG.error ("An error occurred while finding signature help", t);
return;
}
ThreadUtils.runOnUiThread (() -> {
mLanguageClient.showSignatureHelp (help, getFile ());
mLanguageClient.showSignatureHelp (h, getFile ());
});
});
}
}
private void dismissOnUiThread (final Dialog dialog) {
ThreadUtils.runOnUiThread (() -> {
dialog.dismiss ();
});
ThreadUtils.runOnUiThread (dialog::dismiss);
}
/**
+12 -9
View File
@@ -45,21 +45,24 @@
android:layout_width="match_parent"
android:layout_height="@dimen/file_tab_height"
android:background="@color/primaryDarkColor"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorGravity="stretch"
app:tabIndicatorHeight="@dimen/file_tab_height"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title"
app:tabSelectedTextColor="@color/primaryTextColor"
app:tabMode="scrollable" />
app:tabGravity="start"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorFullWidth="false"
app:tabIndicatorGravity="bottom"
app:tabIndicatorHeight="4dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/secondaryColor"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title"
app:tabTextColor="@color/secondaryTextColor_light" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/editor_viewPager"
<ViewFlipper
android:id="@+id/editor_container"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@color/content_background"
android:layout_gravity="top"
android:layout_marginBottom="@dimen/editor_sheet_peek_height"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>