diff --git a/app/build.gradle b/app/build.gradle index 38694bb..2b9fb9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,8 @@ plugins { android { namespace "com.thatmg393.esmanager" - compileSdk 33 - buildToolsVersion '33.0.1' - + compileSdk 34 + defaultConfig { applicationId "com.thatmg393.esmanager" minSdk 26 @@ -30,50 +29,59 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + + coreLibraryDesugaringEnabled true } } dependencies { - def LSP_VERSION = '0.21.0' + def LSP_VERSION = '0.21.1' + // General implementation 'androidx.appcompat:appcompat:1.7.0-alpha02' - implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.security:security-crypto:1.1.0-alpha06' // UI + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.11.0' implementation 'com.github.delight-im:Android-AdvancedWebView:v3.2.1' implementation 'com.github.cachapa:ExpandableLayout:2.9.2' + implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.haroldadmin:WhatTheStack:1.0.0-alpha04' implementation 'com.intuit.sdp:sdp-android:1.1.0' implementation 'com.intuit.ssp:ssp-android:1.1.0' implementation 'io.github.amrdeveloper:treeview:1.1.4' // Code Editor - implementation platform('io.github.Rosemoe.sora-editor:bom:0.22.0-77372aa-SNAPSHOT') + implementation platform('io.github.Rosemoe.sora-editor:bom:0.23.4-f620608-SNAPSHOT') implementation 'io.github.Rosemoe.sora-editor:editor' implementation 'io.github.Rosemoe.sora-editor:editor-lsp' implementation 'io.github.Rosemoe.sora-editor:language-textmate' + implementation project(path: ':tree-sitter') + implementation project(path: ':sora-editor-treesitter') + + implementation 'com.itsaky.androidide.treesitter:android-tree-sitter:4.1.0' + implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:$LSP_VERSION" implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:$LSP_VERSION" // IO / Network implementation 'com.anggrayudi:storage:1.5.5' - implementation 'com.lazygeniouz:dfc:1.0.7' + implementation 'com.lazygeniouz:dfc:1.0.8' implementation 'com.squareup.picasso:picasso:2.8' implementation 'commons-io:commons-io:2.11.0' - implementation 'org.java-websocket:Java-WebSocket:1.5.3' + implementation 'org.java-websocket:Java-WebSocket:1.5.4' // Collections implementation 'org.apache.commons:commons-collections4:4.4' - debugImplementation 'com.itsaky.androidide:logsender:2.5.1-beta-27c6ea872-SNAPSHOT' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' implementation fileTree(dir: 'libs', include: ['*.jar']) @@ -85,19 +93,4 @@ task updateLibs(type: Download) { dest "$rootProject.projectDir/app/libs" overwrite true } - -tasks.whenTaskAdded { task -> - if (task.name == 'preDebugBuild') { - task.dependsOn updateLibs - } - - if (task.name == 'compileDebugJavaWithJavac') { - // Error Prone must be available in the annotation processor path - // options.annotationProcessorPath = configurations.errorprone - // Enable Error Prone - options.errorprone.enabled = true - // It can then be configured for the task - options.errorprone.disableWarningsInGeneratedCode = true - } -} */ diff --git a/app/src/main/assets/esluastubs/es.lua b/app/src/main/assets/esluastubs/es.lua index 62bc84e..a8894c9 100644 --- a/app/src/main/assets/esluastubs/es.lua +++ b/app/src/main/assets/esluastubs/es.lua @@ -38,4 +38,4 @@ function es.Quaternion.Euler(x, y, z) end function es.Quaternion.LookRotation(v3f) end function es.Quaternion.LookRotation(v3f, v3u) end function es.Quaternion.ToEulerAngles(quaternion) end -function es.Quaternion.FromToRotation(v3from, v3to) end +function es.Quaternion.FromToRotation(v3f, v3t) end diff --git a/app/src/main/assets/tm/languages/json/language-configuration.json b/app/src/main/assets/tm/languages/json/language-configuration.json index 75351dd..06ddf43 100644 --- a/app/src/main/assets/tm/languages/json/language-configuration.json +++ b/app/src/main/assets/tm/languages/json/language-configuration.json @@ -17,7 +17,7 @@ ], "indentationRules": { - "increaseIndentPattern": "^.*([\\{\\[]|(\"(?:[^\"\\\\]|\\\\.)*\"))[^}\\]]*([\\}\\]])[ \\t]*[^ \\t\\r\\n]*$", + "increaseIndentPattern": "({+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"}]*)$)|(\\[+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"\\]]*)$)", "decreaseIndentPattern": "^\\s*[}\\]],?\\s*$" } } \ No newline at end of file diff --git a/app/src/main/java/com/thatmg393/esmanager/GlobalConstants.java b/app/src/main/java/com/thatmg393/esmanager/GlobalConstants.java index ad0a806..fe2bdf3 100644 --- a/app/src/main/java/com/thatmg393/esmanager/GlobalConstants.java +++ b/app/src/main/java/com/thatmg393/esmanager/GlobalConstants.java @@ -7,7 +7,7 @@ import android.os.Environment; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.StorageUtils; +import com.thatmg393.esmanager.utils.io.StorageUtils; public final class GlobalConstants { private static volatile GlobalConstants INSTANCE; diff --git a/app/src/main/java/com/thatmg393/esmanager/MainApplication.java b/app/src/main/java/com/thatmg393/esmanager/MainApplication.java index c78706b..28a122e 100644 --- a/app/src/main/java/com/thatmg393/esmanager/MainApplication.java +++ b/app/src/main/java/com/thatmg393/esmanager/MainApplication.java @@ -6,9 +6,12 @@ import android.util.Log; import com.thatmg393.esmanager.managers.editor.language.LanguageManager; -import com.thatmg393.esmanager.utils.EditorUtils; +import com.thatmg393.esmanager.managers.editor.themes.ThemeManager; +import com.thatmg393.esmanager.utils.sora.EditorUtils; -import com.thatmg393.esmanager.utils.FileUtils; +import com.thatmg393.esmanager.utils.logging.ErrorHandler; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.io.FileUtils; import io.github.rosemoe.sora.langs.textmate.registry.FileProviderRegistry; import io.github.rosemoe.sora.langs.textmate.registry.GrammarRegistry; import io.github.rosemoe.sora.langs.textmate.registry.provider.AssetsFileResolver; @@ -17,26 +20,38 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; +import java.util.concurrent.CompletableFuture; public class MainApplication extends Application { + private static final Logger LOG = new Logger("ESM/MainApplication"); private Thread.UncaughtExceptionHandler thrUEH; @Override public void onCreate() { super.onCreate(); - FileProviderRegistry.getInstance().addFileProvider( - new AssetsFileResolver(getAssets()) - ); + LOG.d("Application launched!"); - try { - GrammarRegistry.getInstance().loadGrammars("tm/languages/languages.json"); - EditorUtils.loadTMThemes(); - LanguageManager.getInstance().registerLanguages(); - } catch (Exception e) { - e.printStackTrace(); - } - - setUEH(); + CompletableFuture.runAsync(() -> { + try { + LOG.d("Loading early editor resources in another thread"); + FileProviderRegistry.getInstance().addFileProvider( + new AssetsFileResolver(getAssets()) + ); + + GrammarRegistry.getInstance().loadGrammars("tm/languages/languages.json"); + + ThemeManager.getInstance().registerThemes(); + + LanguageManager.getInstance().registerTreeSitterLanguages(getAssets()); + LanguageManager.getInstance().registerTextMateLanguages(); + + LOG.d("Done loading!"); + } catch (Exception e) { + e.printStackTrace(); + LOG.d("Error occurred!"); + LOG.e(e.toString()); + } + }); } @Override @@ -45,10 +60,9 @@ public void onTerminate() { unsetUEH(); } - private final void setUEH() { if (thrUEH != null) { - Log.w("BaseActivity", "Attempt to initialize 'Thread.UncaughtExceptionHandler' even though it's already initialized!"); + LOG.w("Attempt to initialize 'Thread.UncaughtExceptionHandler' even though it's already initialized!"); return; } @@ -56,8 +70,8 @@ private final void setUEH() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread curThr, Throwable ex) { - Log.e("BaseActivity", curThr + " made an exception! " + fullStacktrace(ex)); - FileUtils.appendToFile(GlobalConstants.getInstance().getESMRootFolder() + "/crash.txt", fullStacktrace(ex)); + LOG.e(curThr + " made an exception! " + fullStacktrace(ex)); + ErrorHandler.writeError(ex); android.os.Process.killProcess(android.os.Process.myPid()); @@ -77,7 +91,7 @@ public void uncaughtException(Thread curThr, Throwable ex) { public void unsetUEH() { if (thrUEH == null) { - Log.w("BaseActivity", "Attempt to unset 'Thread.UncaughtExceptionHandler' even though it's already unset!"); + LOG.w("Attempt to unset 'Thread.UncaughtExceptionHandler' even though it's already unset!"); return; } diff --git a/app/src/main/java/com/thatmg393/esmanager/activities/MainActivity.java b/app/src/main/java/com/thatmg393/esmanager/activities/MainActivity.java index e2e43db..793fac2 100644 --- a/app/src/main/java/com/thatmg393/esmanager/activities/MainActivity.java +++ b/app/src/main/java/com/thatmg393/esmanager/activities/MainActivity.java @@ -6,6 +6,7 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.Toast; import androidx.appcompat.widget.Toolbar; import com.google.android.material.bottomnavigation.BottomNavigationView; @@ -15,12 +16,15 @@ import com.thatmg393.esmanager.fragments.main.ModsFragment; import com.thatmg393.esmanager.fragments.main.ProjectsFragment; import com.thatmg393.esmanager.fragments.main.SettingsFragmentActivity; +import com.thatmg393.esmanager.interfaces.IOnProcessListener; import com.thatmg393.esmanager.managers.editor.lsp.LSPManager; import com.thatmg393.esmanager.managers.rpc.DRPCManager; import com.thatmg393.esmanager.utils.ActivityUtils; import com.thatmg393.esmanager.utils.PermissionUtils; +import com.thatmg393.esmanager.utils.ProcessListener; import com.thatmg393.esmanager.utils.SharedPreference; -import com.thatmg393.esmanager.utils.StorageUtils; +import com.thatmg393.esmanager.utils.io.AndroidFolderUtils; +import com.thatmg393.esmanager.utils.io.StorageUtils; public class MainActivity extends BaseActivity { private Toolbar mainToolbar; @@ -67,7 +71,31 @@ public void onInit(Bundle savedInstanceState) { } // Shared preference stuff - if (SharedPreference.getInstance().getBool("main_rpc_active")) DRPCManager.getInstance().startDiscordRPC(); + // if (SharedPreference.getInstance().getBool("main_rpc_active")) DRPCManager.getInstance().startDiscordRPC(); + + ProcessListener.getInstance().startService(); + ProcessListener.getInstance().startListening("com.facebook.katana", new IOnProcessListener() { + @Override + public void onProcessForeground() { + System.out.println("YESSIR"); + } + + @Override + public void onProcessBackground() { + System.out.println("AAAAAAAAHEJEJOROR"); + } + }); + + try { + ActivityUtils.getInstance().showToast( + AndroidFolderUtils.getDataFolder(getApplicationContext()).getFolderName(), + Toast.LENGTH_LONG + ); + } catch (Exception e) { + ActivityUtils.getInstance().showToast( + e.toString(), Toast.LENGTH_SHORT + ); + } } @Override diff --git a/app/src/main/java/com/thatmg393/esmanager/activities/ProjectActivity.java b/app/src/main/java/com/thatmg393/esmanager/activities/ProjectActivity.java index 0a88d5d..65e2793 100644 --- a/app/src/main/java/com/thatmg393/esmanager/activities/ProjectActivity.java +++ b/app/src/main/java/com/thatmg393/esmanager/activities/ProjectActivity.java @@ -20,15 +20,18 @@ import com.thatmg393.esmanager.adapters.TabEditorAdapter; import com.thatmg393.esmanager.fragments.project.FileTreeViewFragment; import com.thatmg393.esmanager.fragments.project.TabEditorFragment; +import com.thatmg393.esmanager.fragments.project.base.BaseTabFragment; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; +import com.thatmg393.esmanager.interfaces.IOnTabUpdateListener; import com.thatmg393.esmanager.managers.editor.EditorManager; import com.thatmg393.esmanager.managers.editor.lsp.LSPManager; import com.thatmg393.esmanager.managers.editor.project.ProjectManager; import com.thatmg393.esmanager.models.ProjectModel; import com.thatmg393.esmanager.utils.ActivityUtils; - import com.thatmg393.esmanager.utils.compat.IntentCompat; -import io.github.rosemoe.sora.lsp.editor.LspEditorManager; + import io.github.rosemoe.sora.widget.SymbolInputView; +import org.apache.commons.io.FilenameUtils; public class ProjectActivity extends BaseActivity implements TabLayout.OnTabSelectedListener { private DrawerLayout editorDrawerLayout; @@ -66,10 +69,10 @@ public void onInit(Bundle savedInstanceState) { editorTabLayout = findViewById(R.id.project_editors_tab); editorTabLayout.addOnTabSelectedListener(this); editorTabAdapter = new TabEditorAdapter(getLifecycle(), getSupportFragmentManager(), findViewById(android.R.id.content)); - editorTabAdapter.addOnTabUpdateListener(new TabEditorAdapter.OnTabUpdateListener() { + editorTabAdapter.addOnTabUpdateListener(new IOnTabUpdateListener() { @Override public void onRemoveTab(int position) { - invalidateOptionsMenu(); + supportInvalidateOptionsMenu(); } }); @@ -87,7 +90,18 @@ public void onRemoveTab(int position) { ); editorFileTreeViewFragment.addOnFileClickListener((path) -> { - editorTabAdapter.newTab(path); + String pathExt = FilenameUtils.getExtension(path); + + switch (pathExt) { + case "png": + case "jpg": + case "gif": + editorTabAdapter.newTab(path, TabEditorAdapter.TabType.PICTURE); + break; + default: + editorTabAdapter.newTab(path, TabEditorAdapter.TabType.EDITOR); + } + if (editorDrawerLayout.isDrawerOpen(GravityCompat.END)) editorDrawerLayout.closeDrawer(GravityCompat.END); }); @@ -127,8 +141,8 @@ public boolean onCreateOptionsMenu(Menu menu) { public void invalidateOptionsMenu() { super.invalidateOptionsMenu(); - TabEditorFragment frag = EditorManager.getInstance().getFocusedTabEditor(); - if (frag != null) editorSymbolInput.bindEditor(frag.getEditor()); + PathedTabFragment frag = EditorManager.getInstance().getFocusedTabEditor(); + if (frag != null && frag instanceof TabEditorFragment) editorSymbolInput.bindEditor(((TabEditorFragment)frag).getEditor()); } @Override @@ -150,22 +164,28 @@ public boolean onPrepareOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem menuItem) { if (menuItem.getItemId() == R.id.project_action_editor_save) { - TabEditorFragment editorFragment = EditorManager.getInstance().getFocusedTabEditor(); - if (editorFragment != null) editorFragment.save(); + BaseTabFragment editorFragment = EditorManager.getInstance().getFocusedTabEditor(); + if (editorFragment != null) { + if (editorFragment instanceof TabEditorFragment) { + ((TabEditorFragment) editorFragment).save(); + } + } return true; } else if (menuItem.getItemId() == R.id.project_action_editor_save_all) { - editorTabAdapter.getFragmentList().forEach((fragment) -> fragment.save()); + editorTabAdapter.getFragmentList().forEach((model) -> { + if (model.fragment instanceof TabEditorFragment) ((TabEditorFragment) model.fragment).save(); + }); ActivityUtils.getInstance().showToast("All files saved!", Toast.LENGTH_SHORT); return true; } else if (menuItem.getItemId() == R.id.project_action_import_obj) { ActivityUtils.getInstance().showToast("Function not implemented", Toast.LENGTH_SHORT); return true; } else if (menuItem.getItemId() == R.id.project_action_editor_undo) { - TabEditorFragment editorFragment = EditorManager.getInstance().getFocusedTabEditor(); + TabEditorFragment editorFragment = (TabEditorFragment) EditorManager.getInstance().getFocusedTabEditor(); if (editorFragment != null & editorFragment.getEditor().canUndo()) editorFragment.getEditor().undo(); return true; } else if (menuItem.getItemId() == R.id.project_action_editor_redo) { - TabEditorFragment editorFragment = EditorManager.getInstance().getFocusedTabEditor(); + TabEditorFragment editorFragment = (TabEditorFragment) EditorManager.getInstance().getFocusedTabEditor(); if (editorFragment != null & editorFragment.getEditor().canRedo()) editorFragment.getEditor().redo(); return true; } else if (menuItem.getItemId() == R.id.project_action_drawer_file_open) { @@ -214,7 +234,7 @@ private void _destroy() { .remove(editorFileTreeViewFragment) .commit(); - LspEditorManager.getOrCreateEditorManager(ProjectManager.getInstance().getCurrentProject().projectPath).closeAllEditor(); + ProjectManager.getInstance().getCurrentLspProject().closeAllEditors(); LSPManager.getInstance().stopLSPForAllLanguage(); } catch (RuntimeException ignore) { } } diff --git a/app/src/main/java/com/thatmg393/esmanager/adapters/ModListAdapter.java b/app/src/main/java/com/thatmg393/esmanager/adapters/ModListAdapter.java index bbdc00a..453bb9c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/adapters/ModListAdapter.java +++ b/app/src/main/java/com/thatmg393/esmanager/adapters/ModListAdapter.java @@ -16,7 +16,7 @@ import com.thatmg393.esmanager.adapters.base.IBaseRecyclerAdapter; import com.thatmg393.esmanager.interfaces.IOnRecyclerItemClickListener; import com.thatmg393.esmanager.models.ModPropertiesModel; -import com.thatmg393.esmanager.utils.BitmapUtils; +import com.thatmg393.esmanager.utils.io.BitmapUtils; import com.thatmg393.esmanager.viewholders.mod.ModViewHolder; import java.util.ArrayList; @@ -83,7 +83,7 @@ public ModViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) @Override public void onBindViewHolder(@NonNull ModViewHolder viewHolder, int position) { - ModPropertiesModel modProp = (ModPropertiesModel) data.get(position); + ModPropertiesModel modProp = data.get(position); String modName = modProp.getModName(); String modDesc = modProp.getModDescription(); diff --git a/app/src/main/java/com/thatmg393/esmanager/adapters/TabEditorAdapter.java b/app/src/main/java/com/thatmg393/esmanager/adapters/TabEditorAdapter.java index 198b204..10e6347 100644 --- a/app/src/main/java/com/thatmg393/esmanager/adapters/TabEditorAdapter.java +++ b/app/src/main/java/com/thatmg393/esmanager/adapters/TabEditorAdapter.java @@ -4,6 +4,8 @@ import android.widget.HorizontalScrollView; import android.widget.RelativeLayout; +import android.widget.Toast; +import androidx.collection.ArrayMap; import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -14,14 +16,18 @@ import com.google.android.material.tabs.TabLayout; import com.thatmg393.esmanager.R; import com.thatmg393.esmanager.fragments.project.TabEditorFragment; - +import com.thatmg393.esmanager.fragments.project.base.BaseTabFragment; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; +import com.thatmg393.esmanager.fragments.project.editor.TabPictureFragment; +import com.thatmg393.esmanager.interfaces.IOnTabUpdateListener; +import com.thatmg393.esmanager.models.TabModel; import com.thatmg393.esmanager.utils.ActivityUtils; -import org.apache.commons.io.FilenameUtils; import java.util.ArrayList; +import org.apache.commons.io.FilenameUtils; public class TabEditorAdapter extends FragmentStateAdapter { - public ArrayList fragments = new ArrayList<>(); + private ArrayList fragments = new ArrayList<>(); private final RelativeLayout noEditorContainer; private final TabLayout tabLayout; @@ -42,78 +48,89 @@ public TabEditorAdapter(Lifecycle lifecycle, FragmentManager fragmentManager, Vi updateViewsIfNeeded(); } - public void newTab(String path) { - if (getItemCount() <= 0) newTabInternal(path); + public void newTab(String path, TabType type) { + if (getItemCount() <= 0) newTabInternal(path, type); else { - TabEditorFragment fragmentInTab = null; - for (TabEditorFragment fragment : fragments) { - if (fragment.getCurrentFilePath() == path) { - fragmentInTab = fragment; - break; + PathedTabFragment fragmentInTab = null; + for (TabModel model : fragments) { + if (model.fragment.getCurrentFilePath() == path) { + fragmentInTab = model.fragment; } } if (fragmentInTab != null) { TabLayout.Tab fragmentTab = fragmentInTab.getCurrentTab(); - if (fragmentTab.getPosition() != tabLayout.getSelectedTabPosition()) { - fragmentTab.select(); + + if (fragmentTab != null) { + if (fragmentTab.getPosition() != tabLayout.getSelectedTabPosition()) fragmentTab.select(); } - } else newTabInternal(path); + } else newTabInternal(path, type); } } public void removeTab(int position) { - TabEditorFragment fragment = fragments.get(position); - if (fragment.isFileModified()) { - ActivityUtils.getInstance().createAlertDialog( - "Unsaved file", - "Would you like to save: " + fragment.getCurrentFilePath() + "?", - new Pair<>("No", (dialog, which) -> { - dialog.dismiss(); - removeTabInternal(position); - }), - new Pair<>("Yes", (dialog, which) -> { - dialog.dismiss(); - fragment.save(); - removeTabInternal(position); - }) - ).show(); + PathedTabFragment fragment = fragments.get(position).fragment; + + if (fragment instanceof TabEditorFragment) { + TabEditorFragment fragmentCasted = (TabEditorFragment) fragment; + if (fragmentCasted.getEditorState() == TabEditorFragment.EditorState.MODIFIED) { + ActivityUtils.getInstance().createAlertDialog( + "Unsaved file", + "Would you like to save: " + fragmentCasted.getCurrentFilePath() + "?", + new Pair<>("No", (dialog, which) -> { + dialog.dismiss(); + removeTabInternal(position); + }), + new Pair<>("Yes", (dialog, which) -> { + dialog.dismiss(); + fragmentCasted.save(); + removeTabInternal(position); + }) + ).show(); + } else { + removeTabInternal(position); + } } else { removeTabInternal(position); } } - private void newTabInternal(String path) { - TabEditorFragment fragment = new TabEditorFragment(path); + private void newTabInternal(String path, TabType type) { + PathedTabFragment fragment = null; - fragments.add(fragment); - notifyItemInserted(fragments.size()); + switch (type.getType()) { + case 0: + fragment = new TabEditorFragment(path); + break; + case 1: + fragment = new TabPictureFragment(path); + break; + } TabLayout.Tab tab = tabLayout.newTab(); tab.setText(FilenameUtils.getName(path)); + + fragments.add(new TabModel( + tab, fragment + )); tabLayout.addTab(tab); + notifyItemInserted(fragments.size()); + fragment.setCurrentTabObject(tab); + if (tab.getPosition() != tabLayout.getSelectedTabPosition()) { tab.select(); } - fragment.setCurrentTabObject(tab); - dispatchOnNewTab(tab, fragment); updateViewsIfNeeded(); } private void removeTabInternal(int position) { - tabLayout.removeTabAt(position); fragments.remove(position); + tabLayout.removeTabAt(position); notifyItemRemoved(position); - - // Bug is when removing a tab at pos 0, the viewpager doesnt update properly - // and desyncing our tabs, fragments, and viewpager. - // Only happens on pos 0 and getItemCount() > 0 - if (position == 0 && getItemCount() > 0) viewPager.setAdapter(this); - // Also happens on pos 0 and getItemCount() == 0 - else if (position == 0 && getItemCount() == 0) viewPager.setAdapter(this); + notifyItemRangeChanged(position, getItemCount()); dispatchOnRemoveTab(position); updateViewsIfNeeded(); @@ -143,33 +160,61 @@ public void updateViewsIfNeeded() { public int getItemCount() { return fragments.size(); } - + + @Override + public long getItemId(int position) { + return fragments.get(position).itemId; + } + + @Override + public boolean containsItem(long itemId) { + boolean found = false; + for (TabModel model : fragments) { + if (model.itemId == itemId) { + found = true; + break; + } + } + + return found; + } + @Override public Fragment createFragment(int position) { - return fragments.get(position); + return fragments.get(position).fragment; } - public ArrayList getFragmentList() { + public ArrayList getFragmentList() { return this.fragments; } - private final ArrayList tabUpdateListener = new ArrayList<>(); - public void addOnTabUpdateListener(OnTabUpdateListener listener) { + private final ArrayList tabUpdateListener = new ArrayList<>(); + public void addOnTabUpdateListener(IOnTabUpdateListener listener) { tabUpdateListener.add(listener); } - public void removeOnTabUpdateListener(OnTabUpdateListener listener) { + public void removeOnTabUpdateListener(IOnTabUpdateListener listener) { tabUpdateListener.remove(listener); } - private void dispatchOnNewTab(TabLayout.Tab tab, TabEditorFragment fragment) { + private void dispatchOnNewTab(TabLayout.Tab tab, BaseTabFragment fragment) { tabUpdateListener.forEach((listener) -> listener.onNewTab(tab, fragment)); } private void dispatchOnRemoveTab(int position) { tabUpdateListener.forEach((listener) -> listener.onRemoveTab(position)); } - public static interface OnTabUpdateListener { - public default void onNewTab(TabLayout.Tab tab, TabEditorFragment fragment) { } - public default void onRemoveTab(int position) { } + public enum TabType { + EDITOR(0), + PICTURE(1); + + private final int type; + + private TabType(int type) { + this.type = type; + } + + public final int getType() { + return this.type; + } } } diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/ModsFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/ModsFragment.java index 316e117..657668a 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/ModsFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/ModsFragment.java @@ -97,12 +97,12 @@ public void initViews() { try (InputStream jsonIS = requireContext().getContentResolver().openInputStream(jsonFile.getUri())) { JsonObject j = GSON.fromJson(IOUtils.toString(jsonIS, StandardCharsets.UTF_8), JsonObject.class); + String modPath = folder.getUri().toString(); String modName = j.get("name").getAsString(); String modDesc = j.get("description").getAsString(); String modAuthor = j.get("author").getAsString(); String modVersion = j.get("version").getAsString(); - String modPreview = folder.getUri().toString() + "%2F" + j.get("preview").getAsString().replace("/", "%2F"); - String modPath = folder.getUri().toString(); + String modPreview = modPath + "%2F" + j.get("preview").getAsString().replace("/", "%2F"); modsRecyclerView.post(() -> modsRecyclerAdapter.addData(new ModPropertiesModel(modName, modDesc, modVersion, modAuthor, modPreview, modPath))); } catch (IOException | JsonSyntaxException e) { diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/NewProjectDialogFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/NewProjectDialogFragment.java index 67edc04..f38e799 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/NewProjectDialogFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/NewProjectDialogFragment.java @@ -20,8 +20,8 @@ import com.squareup.picasso.Picasso; import com.thatmg393.esmanager.R; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.BitmapUtils; -import com.thatmg393.esmanager.utils.StorageUtils; +import com.thatmg393.esmanager.utils.io.BitmapUtils; +import com.thatmg393.esmanager.utils.io.StorageUtils; import net.cachapa.expandablelayout.ExpandableLayout; diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/ProjectsFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/ProjectsFragment.java index 6eb50c2..499866c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/ProjectsFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/ProjectsFragment.java @@ -28,7 +28,7 @@ import com.thatmg393.esmanager.models.ModPropertiesModel; import com.thatmg393.esmanager.models.ProjectModel; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.FileUtils; +import com.thatmg393.esmanager.utils.io.FileUtils; import org.apache.commons.io.IOUtils; diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/SettingsFragmentActivity.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/SettingsFragmentActivity.java index df27e70..d22f30c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/SettingsFragmentActivity.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/SettingsFragmentActivity.java @@ -3,11 +3,19 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.view.MenuItem; + +import android.widget.Toast; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.preference.PreferenceFragmentCompat; import com.thatmg393.esmanager.R; import com.thatmg393.esmanager.activities.BaseActivity; import com.thatmg393.esmanager.fragments.main.settings.SettingsFragment; import com.thatmg393.esmanager.utils.ActivityUtils; +import java.util.Arrays; +import java.util.List; public class SettingsFragmentActivity extends BaseActivity { public static void start(Context context) { @@ -24,7 +32,78 @@ public void onInit(Bundle savedInstanceState) { ActivityUtils.getInstance().registerActivity(this); setContentView(R.layout.activity_fragment_base); setSupportActionBar(findViewById(R.id.fragment_base_toolbar)); + getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_undo); getSupportFragmentManager().beginTransaction().replace(R.id.fragment_base_frame, new SettingsFragment()).commit(); } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + if (prevFrag != null) { + try { + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_base_frame, (PreferenceFragmentCompat)prevFrag.newInstance()).commit(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + return false; + } + + @Override + public void onBackPressed() { + if (prevFrag != null) { + try { + removeAndReplaceFragment((PreferenceFragmentCompat)prevFrag.newInstance()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private Class prevFrag; + public void updateToolbarState() { + PreferenceFragmentCompat f = getFragmentPreviousOfCurrent(); + if (f != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + prevFrag = f.getClass(); + + return; + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + prevFrag = null; + } + + public PreferenceFragmentCompat getFragmentPreviousOfCurrent() { + List fragments = getSupportFragmentManager().getFragments(); + if (fragments != null) { + for (int i = 0; i < fragments.size(); i++) { + if (fragments.get(i) != null + && fragments.get(i) instanceof PreferenceFragmentCompat + && fragments.get(i).isVisible()) { + ActivityUtils.getInstance().showToast(fragments.toString(), Toast.LENGTH_SHORT); + Fragment tmpPrevFrag = null; + try { + for (int j = i; 0 > j; j--) { + if (fragments.get(j) != null + && fragments.get(j) instanceof PreferenceFragmentCompat) { + tmpPrevFrag = fragments.get(i-1); + } + } + } catch (IndexOutOfBoundsException e) { } + + return (PreferenceFragmentCompat)tmpPrevFrag; + } + }; + } + return null; + } + + private void removeAndReplaceFragment(PreferenceFragmentCompat frag) { + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_base_frame, frag).commit(); + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/base/ListFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/base/ListFragment.java index 95beabf..98bc003 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/base/ListFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/base/ListFragment.java @@ -13,7 +13,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.thatmg393.esmanager.adapters.base.IBaseRecyclerAdapter; -import com.thatmg393.esmanager.utils.ThreadPlus; +import com.thatmg393.esmanager.utils.threading.ThreadPlus; import java.util.ArrayList; diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/AboutFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/AboutFragment.java index 31e8212..485b45b 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/AboutFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/AboutFragment.java @@ -5,11 +5,13 @@ import androidx.preference.PreferenceFragmentCompat; import com.thatmg393.esmanager.R; +import com.thatmg393.esmanager.fragments.main.SettingsFragmentActivity; import com.thatmg393.esmanager.utils.SharedPreference; public class AboutFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + ((SettingsFragmentActivity) requireActivity()).updateToolbarState(); getPreferenceManager().setPreferenceDataStore(SharedPreference.getInstance().getDefaultEncryptedPreferenceDataStore()); setPreferencesFromResource(R.xml.xml_about_preference, rootKey); } diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/SettingsFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/SettingsFragment.java index 451970e..6535a4c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/SettingsFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/main/settings/SettingsFragment.java @@ -2,15 +2,18 @@ import android.os.Bundle; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.thatmg393.esmanager.R; +import com.thatmg393.esmanager.fragments.main.SettingsFragmentActivity; import com.thatmg393.esmanager.managers.rpc.DRPCManager; import com.thatmg393.esmanager.utils.SharedPreference; public class SettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + ((SettingsFragmentActivity) requireActivity()).updateToolbarState(); getPreferenceManager().setPreferenceDataStore(SharedPreference.getInstance().getDefaultEncryptedPreferenceDataStore()); setPreferencesFromResource(R.xml.xml_main_preference, rootKey); @@ -28,7 +31,18 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { return; } }); - - findPreference("main_about"); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + switch (preference.getKey()) { + case "main_about": + requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.fragment_base_frame, new AboutFragment()).commit(); + return true; + default: + System.out.println(preference.getKey()); + return true; + } + // return false; } } \ No newline at end of file diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/project/FileTreeViewFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/project/FileTreeViewFragment.java index a15bc60..0936aa2 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/project/FileTreeViewFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/project/FileTreeViewFragment.java @@ -26,6 +26,7 @@ import com.thatmg393.esmanager.viewholders.tree.FileViewHolder; import com.thatmg393.esmanager.viewholders.tree.FolderViewHolder; +import com.thatmg393.esmanager.viewholders.tree.NoFileViewHolder; import org.apache.commons.io.FilenameUtils; import java.io.File; @@ -85,48 +86,48 @@ public synchronized void refreshOrPopulateTreeView() { isRefreshing = false; treeAdapter.expandNode(rootNode); - // if (lastNodeThatGotClick != null) treeAdapter.expandNodeBranch(lastNodeThatGotClick); + if (lastNodeThatGotClick != null) treeAdapter.expandNodeToLevel(lastNodeThatGotClick, 0); } private void listTopLevelOfDirectory(TreeNode rootDir) { String rootPath = (String) rootDir.getValue(); if (Files.exists(Paths.get(rootPath))) { - try (DirectoryStream dirStream = Files.newDirectoryStream(Paths.get(rootPath), entry -> Files.isDirectory(entry) || Files.isRegularFile(entry))) { - try (DirectoryStream dirStream2 = Files.newDirectoryStream(Paths.get(rootPath), entry -> Files.isDirectory(entry) || Files.isRegularFile(entry))) { - if (!dirStream2.iterator().hasNext()) { - rootDir.addChild(new TreeNode(getString(R.string.file_drawer_no_files), R.layout.project_folder_tree_view)); - } else { - for (Path path : dirStream) { - if (Files.isDirectory(path)) { - rootDir.addChild(new TreeNode(path.toRealPath().toString(), R.layout.project_folder_tree_view)); - } else { - rootDir.addChild(new TreeNode(path.toRealPath().toString(), R.layout.project_file_tree_view)); - } + try (DirectoryStream dirStream = Files.newDirectoryStream(Paths.get(rootPath), entry -> Files.isDirectory(entry) || Files.isRegularFile(entry)); + DirectoryStream dirStream2 = Files.newDirectoryStream(Paths.get(rootPath), entry -> Files.isDirectory(entry) || Files.isRegularFile(entry))) { + if (!dirStream2.iterator().hasNext()) { + rootDir.addChild(new TreeNode("nofile", R.layout.project_nofile_tree_view)); + } else { + for (Path path : dirStream) { + if (Files.isDirectory(path)) { + rootDir.addChild(new TreeNode(path.toRealPath().toString(), R.layout.project_folder_tree_view)); + } else { + rootDir.addChild(new TreeNode(path.toRealPath().toString(), R.layout.project_file_tree_view)); } } - } catch (IOException e) { - e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } } else { - rootDir.addChild(new TreeNode(getString(R.string.file_drawer_no_files), R.layout.project_folder_tree_view)); + rootDir.addChild(new TreeNode("nofile", R.layout.project_nofile_tree_view)); } } private void init() { TreeViewHolderFactory treeFactory = (view, layout) -> { - if (layout == R.layout.project_folder_tree_view) return new FolderViewHolder(view); - return new FileViewHolder(view); + if (layout == R.layout.project_file_tree_view) + return new FileViewHolder(view); + if (layout == R.layout.project_folder_tree_view) + return new FolderViewHolder(view); + return new NoFileViewHolder(view); }; treeAdapter = new TreeViewAdapter(treeFactory); treeAdapter.setTreeNodeClickListener((node, treeView) -> { - if (validateNodeValue((String) node.getValue())) return; - lastNodeThatGotClick = node; + if (!validateNodeValue((String) node.getValue())) return; if (node.getLayoutId() == R.layout.project_file_tree_view) { + lastNodeThatGotClick = node; dispatchOnFileClick((String) node.getValue()); } else { if (node.getChildren().size() == 0) { @@ -135,10 +136,12 @@ private void init() { } else { node.getChildren().clear(); } + lastNodeThatGotClick = node; } }); treeAdapter.setTreeNodeLongClickListener((node, treeView) -> { - if (validateNodeValue((String) node.getValue())) return false; + if (!validateNodeValue((String) node.getValue())) return false; + lastNodeThatGotClick = node; // FIXME: Refactor or something if (node.getLayoutId() == R.layout.project_file_tree_view) { @@ -297,7 +300,7 @@ private void init() { } private boolean validateNodeValue(String value) { - return value.equals(getString(R.string.file_drawer_no_files)); + return !value.equals(getString(R.string.file_drawer_no_files)); } private ArrayList fileClickListeners = new ArrayList<>(); diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/project/TabEditorFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/project/TabEditorFragment.java index 7dec45b..eea29c9 100644 --- a/app/src/main/java/com/thatmg393/esmanager/fragments/project/TabEditorFragment.java +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/project/TabEditorFragment.java @@ -5,46 +5,40 @@ import android.view.View; import android.view.ViewGroup; -import androidx.annotation.RestrictTo; -import androidx.fragment.app.Fragment; - -import com.google.android.material.tabs.TabLayout.Tab; +import android.widget.Toast; import com.thatmg393.esmanager.activities.ProjectActivity; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; import com.thatmg393.esmanager.interfaces.ILanguageServiceCallback; import com.thatmg393.esmanager.managers.editor.EditorManager; +import com.thatmg393.esmanager.managers.editor.language.LanguageManager; import com.thatmg393.esmanager.managers.editor.lsp.LSPManager; +import com.thatmg393.esmanager.managers.editor.themes.ThemeManager; import com.thatmg393.esmanager.models.LanguageServerModel; -import com.thatmg393.esmanager.utils.EditorUtils; -import com.thatmg393.esmanager.utils.LSPUtils; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.utils.ActivityUtils; import com.thatmg393.esmanager.utils.SharedPreference; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.sora.EditorUtils; +import com.thatmg393.esmanager.utils.sora.LSPUtils; +import io.github.rosemoe.sora.editor.ts.TsLanguage; import io.github.rosemoe.sora.event.ContentChangeEvent; import io.github.rosemoe.sora.lang.Language; import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry; -import io.github.rosemoe.sora.lsp.utils.URIUtils; +import io.github.rosemoe.sora.lsp.utils.URIUtilsKt; import io.github.rosemoe.sora.widget.CodeEditor; import org.apache.commons.io.FilenameUtils; -import java.io.File; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -public class TabEditorFragment extends Fragment { - private static final Logger LOG = new Logger("ESM/ProjectTabEditorFragment"); +public class TabEditorFragment extends PathedTabFragment { + private static final Logger LOG = new Logger("ESM/TabEditorFragment"); - private boolean isModified; - - private Tab tab; + private EditorState editorState = EditorState.SAVED; private CodeEditor editor; - private File editorFile; private String fileExtension; - public TabEditorFragment() { } public TabEditorFragment(final String pathToFile) { + super(pathToFile); this.fileExtension = FilenameUtils.getExtension(pathToFile); - this.editorFile = new File(pathToFile); } @Override @@ -53,21 +47,63 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa return editor; } + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + LanguageServerModel lsModel = LSPManager.getInstance().getLanguageServer(fileExtension); + if (lsModel != null) { + lsModel.addListener(new ILanguageServiceCallback() { + @Override + public void onReady() { + LSPUtils.connectToLsp( + LSPUtils.createNewLspEditor( + URIUtilsKt.toFileUri(getCurrentFile().getAbsolutePath()), + editor + ) + ); + } + }); + } + } + @Override public void onResume() { super.onResume(); editor.requestFocus(); } + @Override + public void onDestroy() { + super.onDestroy(); + editor.release(); + } + @Override public String toString() { - return this.getClass().getName() + " for " + editorFile.getAbsolutePath(); + return this.getClass().getName() + " for " + getCurrentFilePath(); } - @RestrictTo(RestrictTo.Scope.LIBRARY) - public void setCurrentTabObject(Tab currentTab) { - if (tab != null) return; - this.tab = currentTab; + public void save() { + if (getEditorState() == EditorState.SAVED) return; + EditorUtils.saveFileFromEditor(editor, getCurrentFilePath()).thenApply(success -> { + if (!success.booleanValue()) return success; + updateState(EditorState.SAVED); + return success; + }); + } + + private void updateState(EditorState state) { + switch (state.getState()) { + case 0: + if (editorState == EditorState.SAVED) return; + ActivityUtils.getInstance().runOnUIThread(() -> getCurrentTab().setText(FilenameUtils.getName(getCurrentFilePath()))); + editorState = EditorState.SAVED; + break; + case 1: + if (editorState == EditorState.MODIFIED) return; + ActivityUtils.getInstance().runOnUIThread(() -> getCurrentTab().setText("*" + FilenameUtils.getName(getCurrentFilePath()))); + editorState = EditorState.MODIFIED; + break; + } } private void initEditor() { @@ -82,59 +118,42 @@ private void initEditor() { switch (event.getAction()) { case ContentChangeEvent.ACTION_INSERT: case ContentChangeEvent.ACTION_DELETE: - tab.setText("*" + FilenameUtils.getName(editorFile.getAbsolutePath())); - isModified = true; + updateState(EditorState.MODIFIED); break; } }); - - ThemeRegistry.getInstance().setTheme(SharedPreference.getInstance().getStringFallback("editor_code_theme", "darcula")); - EditorUtils.ensureTMTheme(editor); - - Language editorLang = EditorUtils.createTMLanguage(fileExtension); - if (editorLang != null) editor.setEditorLanguage(editorLang); - - LanguageServerModel lsModel = LSPManager.getInstance().getLanguageServer(fileExtension); - if (lsModel != null) { - lsModel.addListener(new ILanguageServiceCallback() { - @Override - public void onReady() { - LSPUtils.connectToLsp( - LSPUtils.createNewLspEditor( - URIUtils.fileToURI(editorFile).toString(), - LSPManager.getInstance().getLanguageServer(fileExtension).getServerDefinition(), - editor - ) - ); - } - }); - } + editor.setHardwareAcceleratedDrawAllowed(true); + editor.getProps().deleteEmptyLineFast = false; + editor.getProps().stickyScroll = true; EditorUtils.loadFileToEditor(editor, getCurrentFilePath()); - } - - public void save() { - if (!isFileModified()) return; - CompletableFuture success = EditorUtils.saveFileFromEditor(editor, editorFile.getAbsolutePath()); - try { - isModified = !success.get(); - tab.setText(FilenameUtils.getName(editorFile.getAbsolutePath())); - } catch (ExecutionException | InterruptedException ignore) { } - } - - public Tab getCurrentTab() { - return this.tab; + + System.out.println(fileExtension); + Language editorLang = LanguageManager.getInstance().getLanguage(fileExtension); + EditorUtils.checkAndCorrectColorScheme(editor, editorLang); + editor.setEditorLanguage(editorLang); } public CodeEditor getEditor() { return this.editor; } - public String getCurrentFilePath() { - return this.editorFile.getAbsolutePath(); - } - - public boolean isFileModified() { - return this.isModified; + public EditorState getEditorState() { + return this.editorState; } + + public enum EditorState { + MODIFIED(1), + SAVED(0); + + private final int state; + + private EditorState(int state) { + this.state = state; + } + + public final int getState() { + return this.state; + } + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/BaseTabFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/BaseTabFragment.java new file mode 100644 index 0000000..37b46bc --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/BaseTabFragment.java @@ -0,0 +1,18 @@ +package com.thatmg393.esmanager.fragments.project.base; + +import androidx.fragment.app.Fragment; +import com.google.android.material.tabs.TabLayout.Tab; + +public class BaseTabFragment extends Fragment { + private Tab currentTab; + + /* internal */ + public void setCurrentTabObject(Tab currentTab) { + if (this.currentTab != null) return; + this.currentTab = currentTab; + } + + public Tab getCurrentTab() { + return this.currentTab; + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/PathedTabFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/PathedTabFragment.java new file mode 100644 index 0000000..c404070 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/project/base/PathedTabFragment.java @@ -0,0 +1,38 @@ +package com.thatmg393.esmanager.fragments.project.base; + +import android.os.Bundle; +import java.io.File; + +public class PathedTabFragment extends BaseTabFragment { + private File filePath; + + public PathedTabFragment(final String pathToFile) { + this.filePath = new File(pathToFile); + } + + public File getCurrentFile() { + return this.filePath; + } + + public String getCurrentFilePath() { + return this.filePath.getAbsolutePath(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + String path = savedInstanceState.getString("filePath"); + if (path != null) { + filePath = new File(path); + } + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + + savedInstanceState.putString("filePath", getCurrentFilePath()); + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/fragments/project/editor/TabPictureFragment.java b/app/src/main/java/com/thatmg393/esmanager/fragments/project/editor/TabPictureFragment.java new file mode 100644 index 0000000..2ea64b3 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/fragments/project/editor/TabPictureFragment.java @@ -0,0 +1,60 @@ +package com.thatmg393.esmanager.fragments.project.editor; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import android.widget.ImageView; +import com.davemorrissey.labs.subscaleview.ImageSource; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; +import com.thatmg393.esmanager.R; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; + +public class TabPictureFragment extends PathedTabFragment { + private SubsamplingScaleImageView rootView; + + public TabPictureFragment(final String pathToFile) { + super(pathToFile); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + rootView = new SubsamplingScaleImageView(requireActivity()); + return rootView; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + Picasso.get() + .load(getCurrentFile()) + .into(new Target() { + @Override + public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { + rootView.setImage(ImageSource.bitmap(bitmap)); + } + + @Override + public void onPrepareLoad(Drawable placeHolderDrawable) { } + @Override + public void onBitmapFailed(Exception error, Drawable errorDrawable) { } + }); + } + + + @Override + public String toString() { + return this.getClass().getName() + " for " + getCurrentFilePath(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + rootView = null; + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/interfaces/ILanguageServiceCallback.java b/app/src/main/java/com/thatmg393/esmanager/interfaces/ILanguageServiceCallback.java index 5339d93..1a14207 100644 --- a/app/src/main/java/com/thatmg393/esmanager/interfaces/ILanguageServiceCallback.java +++ b/app/src/main/java/com/thatmg393/esmanager/interfaces/ILanguageServiceCallback.java @@ -1,15 +1,15 @@ package com.thatmg393.esmanager.interfaces; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.utils.logging.Logger; public interface ILanguageServiceCallback { - public static final Logger LOG = new Logger("ESM/LanguageServiceCallbackImpl"); + public static final Logger LOG = new Logger("ESM/ILanguageServiceCallback"); public default void onReady() { - LOG.i("Language service started!"); + LOG.i("A language service has been started!"); } public default void onShutdown() { - LOG.e("Language service SHUTDOWN!"); + LOG.e("A language service is shutting down"); } } diff --git a/app/src/main/java/com/thatmg393/esmanager/interfaces/IOnTabUpdateListener.java b/app/src/main/java/com/thatmg393/esmanager/interfaces/IOnTabUpdateListener.java new file mode 100644 index 0000000..91994d4 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/interfaces/IOnTabUpdateListener.java @@ -0,0 +1,9 @@ +package com.thatmg393.esmanager.interfaces; + +import com.google.android.material.tabs.TabLayout; +import com.thatmg393.esmanager.fragments.project.base.BaseTabFragment; + +public interface IOnTabUpdateListener { + public default void onNewTab(TabLayout.Tab tab, BaseTabFragment fragment) { } + public default void onRemoveTab(int position) { } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/interfaces/IUIThreadTask.java b/app/src/main/java/com/thatmg393/esmanager/interfaces/IUIThreadTask.java new file mode 100644 index 0000000..44ec84e --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/interfaces/IUIThreadTask.java @@ -0,0 +1,7 @@ +package com.thatmg393.esmanager.interfaces; + +import android.content.Context; + +public interface IUIThreadTask { + public void run(Context context); +} diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/EditorManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/EditorManager.java index 8c89864..a48e99c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/EditorManager.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/EditorManager.java @@ -1,6 +1,7 @@ package com.thatmg393.esmanager.managers.editor; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; +import com.thatmg393.esmanager.utils.logging.Logger; import com.thatmg393.esmanager.fragments.project.TabEditorFragment; public class EditorManager { @@ -16,13 +17,13 @@ private EditorManager() { if (INSTANCE != null) throw new RuntimeException("Please use 'EditorManager#getInstance()'!"); } - private TabEditorFragment focusedTabEditor = null; + private PathedTabFragment focusedTabEditor = null; - public void setFocusedTabEditor(TabEditorFragment editor) { + public void setFocusedTabEditor(PathedTabFragment editor) { this.focusedTabEditor = editor; } - public TabEditorFragment getFocusedTabEditor() { + public PathedTabFragment getFocusedTabEditor() { return this.focusedTabEditor; } } diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/language/LanguageManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/language/LanguageManager.java index 1675f11..48e2da2 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/language/LanguageManager.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/language/LanguageManager.java @@ -1,13 +1,25 @@ package com.thatmg393.esmanager.managers.editor.language; +import android.content.res.AssetManager; + import androidx.annotation.Nullable; +import androidx.collection.ArrayMap; + +import com.itsaky.androidide.treesitter.TreeSitter; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.treesitter.lua.sora.LuaLanguageSpec; +import com.thatmg393.treesitter.lua.sora.TsLanguageLua; +import io.github.rosemoe.sora.lang.EmptyLanguage; import io.github.rosemoe.sora.lang.Language; import io.github.rosemoe.sora.langs.textmate.TextMateLanguage; -import java.util.HashMap; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; public class LanguageManager { + private static final Logger LOG = new Logger("ESM/LanguageManager"); private static volatile LanguageManager INSTANCE; public static synchronized LanguageManager getInstance() { @@ -15,17 +27,51 @@ public static synchronized LanguageManager getInstance() { return INSTANCE; } - private HashMap languageRegistry = new HashMap<>(); + private ArrayMap> languageRegistry = new ArrayMap<>(); private LanguageManager() { } @Nullable public Language getLanguage(String name) { - return languageRegistry.get(name); + Supplier lambda = languageRegistry.get(name); + if (lambda == null) { + System.out.println("Lambda null IMPASSABAL!!! does it relly exists?" + languageRegistry.containsKey(name)); + return new EmptyLanguage(); + } + + return lambda.get(); + } + + public void initTreeSitter() { + TreeSitter.loadLibrary(); + LOG.d("TreeSitter v" + TreeSitter.getLanguageVersion() + " loaded!"); + } + + public void registerTreeSitterLanguages(AssetManager assets) { + initTreeSitter(); + + languageRegistry.put("lua", () -> { + return new TsLanguageLua( + new LuaLanguageSpec( + loadScheme(assets, "lua", "highlights"), + loadScheme(assets, "lua", "locals"), + loadScheme(assets, "lua", "indents") + ), true + ); + }); + } + + public void registerTextMateLanguages() { + // languageRegistry.put("lua", () -> TextMateLanguage.create("source.lua", true) ); + languageRegistry.put("json", () -> TextMateLanguage.create("source.json", true) ); } - public void registerLanguages() { - languageRegistry.put("lua", TextMateLanguage.create("source.lua", true)); - languageRegistry.put("json", TextMateLanguage.create("source.json", true)); + private String loadScheme(AssetManager assets, String language, String type) { + try { + return new String(assets.open(language + "/" + type + ".scm").readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + LOG.w("Scheme " + type + " for " + language + " not found."); + return ""; + } } } diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/LSPManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/LSPManager.java index 43c7e4d..8f87ec6 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/LSPManager.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/LSPManager.java @@ -4,11 +4,14 @@ import com.thatmg393.esmanager.fragments.project.TabEditorFragment; import com.thatmg393.esmanager.managers.editor.lsp.lua.LuaLSPService; +import com.thatmg393.esmanager.managers.editor.project.ProjectManager; import com.thatmg393.esmanager.models.LanguageServerModel; import com.thatmg393.esmanager.models.ProjectModel; -import com.thatmg393.esmanager.utils.Logger; -import com.thatmg393.esmanager.utils.NetworkUtils; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.io.NetworkUtils; +import io.github.rosemoe.sora.lsp.editor.LspEditor; +import io.github.rosemoe.sora.lsp.editor.LspProject; import java.util.HashMap; public class LSPManager { @@ -56,8 +59,9 @@ public LanguageServerModel getLanguageServer(String language) { return languageServerRegistry.get(language); } - public void registerNewLSPServer(String language, LanguageServerModel lspModel) { + public void registerNewLSPServer(LspProject project, String language, LanguageServerModel lspModel) { languageServerRegistry.put(language, lspModel); + project.addServerDefinition(lspModel.getServerDefinition()); } public void registerLangServers() { @@ -68,7 +72,10 @@ public void registerLangServers() { ); */ - registerNewLSPServer("lua", + LspProject project = ProjectManager.getInstance().getCurrentLspProject(); + + registerNewLSPServer( + project, "lua", new LanguageServerModel( "lua", LuaLSPService.class, diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/base/BaseLSPService.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/base/BaseLSPService.java index d3caefe..d14b930 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/base/BaseLSPService.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/base/BaseLSPService.java @@ -2,6 +2,7 @@ import android.app.Service; import com.thatmg393.esmanager.interfaces.ILanguageServerCallback; +import java.util.ArrayList; public abstract class BaseLSPService extends Service { public abstract boolean isServerRunning(); @@ -11,5 +12,6 @@ public abstract class BaseLSPService extends Service { protected abstract void startServer() throws Exception; + public final ArrayList listeners = new ArrayList<>(); public abstract void addServerListener(ILanguageServerCallback ilsc); } diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/lua/LuaLSPService.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/lua/LuaLSPService.java index 163968f..8ffbb3c 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/lua/LuaLSPService.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/lsp/lua/LuaLSPService.java @@ -12,8 +12,8 @@ import com.thatmg393.esmanager.managers.editor.lsp.base.BaseLSPBinder; import com.thatmg393.esmanager.managers.editor.lsp.base.BaseLSPService; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.Logger; -import com.thatmg393.esmanager.utils.ThreadPlus; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.threading.ThreadPlus; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; public class LuaLSPService extends BaseLSPService { private final Logger LOG = new Logger("ESM/LSPManager.LSPService"); @@ -35,13 +34,14 @@ public class LuaLSPService extends BaseLSPService { private volatile boolean isServerRunning; +// Client variables { private ThreadPlus serverThread; + private ExecutorService jsonRPCThreadPool = Executors.newCachedThreadPool(); + private volatile AsynchronousSocketChannel serverClientSocket; +// } // Server variables { - private ExecutorService jsonrpcThreadPool = Executors.newCachedThreadPool(); - private volatile AsynchronousServerSocketChannel serverSocket; - private volatile AsynchronousSocketChannel serverClientSocket; private volatile InputStream serverIS; private volatile OutputStream serverOS; @@ -96,6 +96,7 @@ public void stopLSPServer() { } @Override + @SuppressWarnings("unchecked") protected void startServer() throws Exception { callbackOnStart(); serverClientSocket = serverSocket.accept().get(); @@ -110,7 +111,7 @@ protected void startServer() throws Exception { .setRemoteInterface(LuaLanguageClient.class) .setInput(serverIS) .setOutput(serverOS) - .setExecutorService(jsonrpcThreadPool) + .setExecutorService(jsonRPCThreadPool) .create(); luaServer.connect((LuaLanguageClient) serverLauncher.getRemoteProxy()); @@ -128,7 +129,7 @@ private synchronized void fullyCloseServer() { if (serverIS != null) serverIS.close(); if (serverOS != null) serverOS.close(); - jsonrpcThreadPool.shutdown(); + jsonRPCThreadPool.shutdown(); if (serverSocket != null) serverSocket.close(); if (serverClientSocket != null) serverClientSocket.close(); @@ -149,28 +150,16 @@ private synchronized void initializeServer() throws Exception { } } - private class LuaLSPBinder extends BaseLSPBinder { - @Override - public LuaLSPService getInstance() { - return LuaLSPService.this; - } - } - @Override public boolean isServerRunning() { return isServerRunning && serverThread.isRunning(); } - private ArrayList listeners = new ArrayList<>(); @Override public void addServerListener(ILanguageServerCallback ilsc) { listeners.add(ilsc); - if (isServerRunning) { - ilsc.onStart(); - } else { - ilsc.onShutdown(); - } + if (isServerRunning) ilsc.onStart(); } @MainThread @@ -182,4 +171,11 @@ private void callbackOnStart() { private void callbackOnShutdown() { listeners.forEach(ILanguageServerCallback::onShutdown); } + + private class LuaLSPBinder extends BaseLSPBinder { + @Override + public LuaLSPService getInstance() { + return LuaLSPService.this; + } + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/project/ProjectManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/project/ProjectManager.java index d55aab4..9577973 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/editor/project/ProjectManager.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/project/ProjectManager.java @@ -1,8 +1,11 @@ package com.thatmg393.esmanager.managers.editor.project; -import com.thatmg393.esmanager.utils.Logger; +import androidx.annotation.NonNull; +import com.thatmg393.esmanager.utils.logging.Logger; import com.thatmg393.esmanager.fragments.project.TabEditorFragment; import com.thatmg393.esmanager.models.ProjectModel; +import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.LanguageServerDefinition; +import io.github.rosemoe.sora.lsp.editor.LspProject; public class ProjectManager { private static final Logger LOG = new Logger("ESM/ProjectManager"); @@ -18,12 +21,22 @@ private ProjectManager() { } private ProjectModel currentProject = null; + private LspProject currentProjectAsLsp = null; - public void setCurrentProject(ProjectModel newProject) { + public void addServerDefinition(@NonNull LanguageServerDefinition serverDefinition) { + currentProjectAsLsp.addServerDefinition(serverDefinition); + } + + public void setCurrentProject(@NonNull ProjectModel newProject) { this.currentProject = newProject; + this.currentProjectAsLsp = new LspProject(newProject.projectPath); } public ProjectModel getCurrentProject() { return this.currentProject; } + + public LspProject getCurrentLspProject() { + return this.currentProjectAsLsp; + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/editor/themes/ThemeManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/editor/themes/ThemeManager.java new file mode 100644 index 0000000..9ae48a4 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/managers/editor/themes/ThemeManager.java @@ -0,0 +1,59 @@ +package com.thatmg393.esmanager.managers.editor.themes; + +import androidx.collection.ArrayMap; + +import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme; +import io.github.rosemoe.sora.langs.textmate.registry.FileProviderRegistry; +import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry; +import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel; +import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; +import io.github.rosemoe.sora.widget.schemes.SchemeDarcula; + +import org.eclipse.tm4e.core.registry.IThemeSource; + +import java.util.ArrayList; +import java.util.function.Supplier; + +public class ThemeManager { + private static volatile ThemeManager INSTANCE; + + public static synchronized ThemeManager getInstance() { + if (INSTANCE == null) INSTANCE = new ThemeManager(); + return INSTANCE; + } + + private ArrayMap> themes = new ArrayMap<>(); + + private ThemeManager() { } + + public EditorColorScheme getTheme(String name) { + Supplier theme = themes.get(name); + + if (theme == null) { + return new EditorColorScheme(); + } + + return theme.get(); + } + + public void registerThemes() { + themes.put("darcula", () -> new SchemeDarcula()); + // themes.put("darcula", () -> createTM("darcula")); + themes.put("quitelight", () -> createTM("quitelight")); + } + + public EditorColorScheme createTM(String name) { + String themePath = "tm/themes/" + name + ".json"; + try { + return TextMateColorScheme.create(new ThemeModel( + IThemeSource.fromInputStream( + FileProviderRegistry.getInstance().tryGetInputStream(themePath), + themePath, null + ), name + )); + } catch (Exception e) { + e.printStackTrace(System.err); + return new EditorColorScheme(); + } + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/DRPCManager.java b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/DRPCManager.java index 495265a..f26d45b 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/DRPCManager.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/DRPCManager.java @@ -10,7 +10,7 @@ import com.thatmg393.esmanager.managers.rpc.impl.RPCService; import com.thatmg393.esmanager.models.DiscordProfileModel; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.utils.logging.Logger; public class DRPCManager implements ServiceConnection { private static final Logger LOG = new Logger("ESM/DRPCManager"); diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCService.java b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCService.java index 56e32b9..1380ef4 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCService.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCService.java @@ -4,7 +4,6 @@ import android.app.NotificationManager; import android.app.Service; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.os.Binder; @@ -21,7 +20,7 @@ import com.thatmg393.esmanager.managers.rpc.DRPCManager; import com.thatmg393.esmanager.utils.ActivityUtils; import com.thatmg393.esmanager.utils.SharedPreference; -import com.thatmg393.esmanager.utils.ThreadPlus; +import com.thatmg393.esmanager.utils.threading.ThreadPlus; import im.delight.android.webview.AdvancedWebView; @@ -66,19 +65,19 @@ public void onCreate() { super.onCreate(); ActivityUtils.getInstance().createNotificationChannel(CHANNEL_ID, "Discord Rich Presence Service", NotificationManager.IMPORTANCE_DEFAULT); - try { - if (rpcWebsocketClient == null) rpcWebsocketClient = new RPCSocketClient(this); - } catch (URISyntaxException ignore) { } - + this.websocketThread = null; this.websocketThread = new ThreadPlus(() -> { try { + if (rpcWebsocketClient != null) rpcWebsocketClient.close(0); + rpcWebsocketClient = new RPCSocketClient(RPCService.this); + if (!rpcWebsocketClient.isOpen()) rpcWebsocketClient.connectBlocking(); - } catch (InterruptedException ignore) { } + } catch (InterruptedException | URISyntaxException ignore) { } }) { @Override public void stop() { super.stop(); - rpcWebsocketClient.close(); + if (rpcWebsocketClient != null) rpcWebsocketClient.close(); } }; diff --git a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCSocketClient.java b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCSocketClient.java index 9388757..e53e11b 100644 --- a/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCSocketClient.java +++ b/app/src/main/java/com/thatmg393/esmanager/managers/rpc/impl/RPCSocketClient.java @@ -5,14 +5,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import com.thatmg393.esmanager.GlobalConstants; -import com.thatmg393.esmanager.managers.rpc.DRPCManager; import com.thatmg393.esmanager.models.DiscordProfileModel; -import com.thatmg393.esmanager.utils.FileUtils; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.utils.logging.Logger; import com.thatmg393.esmanager.utils.SharedPreference; +import com.thatmg393.esmanager.utils.threading.ThreadPlus; -import com.thatmg393.esmanager.utils.ThreadPlus; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; @@ -33,15 +30,14 @@ public class RPCSocketClient extends WebSocketClient { public volatile DiscordProfileModel discordProfile; - public boolean isConnecting; - public boolean isConnected; + public ConnectionState connectionState; public RPCSocketClient(RPCService serviceInstance) throws URISyntaxException { super(new URI("wss://gateway.discord.gg/?encoding=json&v=10")); this.serviceInstance = serviceInstance; this.heartbeatThread = new ThreadPlus(() -> { - if (isConnected) { + if (connectionState == ConnectionState.CONNECTED) { try { if (heartbeatInterval < 10000) throw new RuntimeException("Invalid Heartbeat Interval!"); Thread.sleep(heartbeatInterval); @@ -111,8 +107,7 @@ private void onMessageDispatch(ArrayMap dataMap) { serviceInstance.updateNotificationTitle("Connected to " + discordProfile.getFullUsername()); serviceInstance.callbackOnConnected(); - isConnecting = false; - isConnected = true; + connectionState = ConnectionState.CONNECTED; break; case "SESSIONS_REPLACE": // Status change like, dnd -> idle String currentStatus = (String) ((Map) ((List) dataMap.get("d")).get(0)).get("status"); @@ -123,6 +118,8 @@ private void onMessageDispatch(ArrayMap dataMap) { serviceInstance.updateNotificationContent("Changed status to " + currentStatus); } break; + default: + serviceInstance.updateNotificationContent("Unknown state recieved -> " + state); } } @@ -144,20 +141,21 @@ public void close() { @Override public boolean isOpen() { - return super.isOpen() && (isConnected || isConnecting); + return super.isOpen() && connectionState == ConnectionState.CONNECTED; } @Override public boolean connectBlocking() throws InterruptedException { - isConnecting = true; + connectionState = ConnectionState.CONNECTING; return super.connectBlocking(); } private void _close() { - if (!isConnected) return; + if (connectionState == ConnectionState.DISCONNECTED + || connectionState == ConnectionState.CONNECTING) return; LOG.d("Closing RPCSocketClient"); - isConnected = false; + connectionState = ConnectionState.DISCONNECTED; heartbeatThread.kill(); serviceInstance.callbackShutdown(); } @@ -246,4 +244,10 @@ else if (link.startsWith("cdn.discordapp.com")) { return link; } + + public enum ConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/models/LanguageServerModel.java b/app/src/main/java/com/thatmg393/esmanager/models/LanguageServerModel.java index 4ba3035..315a9fe 100644 --- a/app/src/main/java/com/thatmg393/esmanager/models/LanguageServerModel.java +++ b/app/src/main/java/com/thatmg393/esmanager/models/LanguageServerModel.java @@ -9,12 +9,11 @@ import androidx.annotation.Nullable; import com.thatmg393.esmanager.interfaces.ILanguageServiceCallback; -import com.thatmg393.esmanager.managers.editor.project.ProjectManager; import com.thatmg393.esmanager.managers.editor.lsp.base.BaseLSPBinder; import com.thatmg393.esmanager.managers.editor.lsp.base.BaseLSPService; import com.thatmg393.esmanager.utils.ActivityUtils; -import com.thatmg393.esmanager.utils.LSPUtils; -import com.thatmg393.esmanager.utils.Logger; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.sora.LSPUtils; import io.github.rosemoe.sora.lsp.client.connection.SocketStreamConnectionProvider; import io.github.rosemoe.sora.lsp.client.connection.StreamConnectionProvider; @@ -44,8 +43,7 @@ public LanguageServerModel( this.serverPort = serverPort; this.serverWrapper = LSPUtils.createNewServerWrapper( serverName, - new SocketStreamConnectionProvider(() -> serverPort), - ProjectManager.getInstance().getCurrentProject().projectPath + new SocketStreamConnectionProvider(serverPort, null) ); } @@ -56,8 +54,7 @@ public LanguageServerModel( this.serverName = serverName; this.serverWrapper = LSPUtils.createNewServerWrapper( serverName, - serverConnectionProvider, - ProjectManager.getInstance().getCurrentProject().projectPath + serverConnectionProvider ); } diff --git a/app/src/main/java/com/thatmg393/esmanager/models/TabModel.java b/app/src/main/java/com/thatmg393/esmanager/models/TabModel.java new file mode 100644 index 0000000..994ed85 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/models/TabModel.java @@ -0,0 +1,24 @@ +package com.thatmg393.esmanager.models; + +import androidx.annotation.NonNull; + +import com.google.android.material.tabs.TabLayout; +import com.thatmg393.esmanager.fragments.project.base.PathedTabFragment; + +public class TabModel { + public final TabLayout.Tab tab; + public final PathedTabFragment fragment; + + public final String fullPath; + public final long itemId; + + public TabModel( + @NonNull TabLayout.Tab tab, + @NonNull PathedTabFragment fragment + ) { + this.tab = tab; + this.fragment = fragment; + this.fullPath = fragment.getCurrentFilePath(); + this.itemId = new Long(fragment.hashCode()); + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/ActivityUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/ActivityUtils.java index e675490..2e4263b 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/ActivityUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/ActivityUtils.java @@ -1,8 +1,5 @@ package com.thatmg393.esmanager.utils; -import static android.os.Build.VERSION.SDK_INT; -import static android.os.Build.VERSION_CODES; - import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; @@ -32,7 +29,10 @@ import androidx.core.util.Pair; import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import com.thatmg393.esmanager.interfaces.IUIThreadTask; import java.util.Locale; +import java.util.function.Supplier; public class ActivityUtils { private static volatile ActivityUtils INSTANCE; @@ -77,6 +77,10 @@ public void runOnUIThread(Runnable toBeRun) { mainThread.post(toBeRun); } + public void runOnUIThread(IUIThreadTask toBeRun) { + mainThread.post(() -> toBeRun.run(getRegisteredActivity().getApplicationContext())); + } + public void createNotificationChannel( @NonNull String channelID, @NonNull String channelName, diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/PermissionUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/PermissionUtils.java index 2104163..7481060 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/PermissionUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/PermissionUtils.java @@ -1,6 +1,5 @@ package com.thatmg393.esmanager.utils; -import android.net.Uri; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES; @@ -13,13 +12,13 @@ import android.content.pm.PackageManager; import android.os.Environment; import android.provider.Settings; - import android.util.ArrayMap; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultCallback; + import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; +import com.thatmg393.esmanager.utils.logging.Logger; + public class PermissionUtils { private static final Logger LOG = new Logger("ESM/Permissiontils"); @@ -71,57 +70,68 @@ public static void askForUsageStatsPermission(Context context) { } } - public static boolean checkDrawOverlayPermission(Context context) { - return Settings.canDrawOverlays(context); - } - - public static void requestDrawOverlayPermission(Context context, PermissionResult pmr) { - if (!checkDrawOverlayPermission(context)) { - Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); - - ActivityUtils.getInstance().registerForActivityResult(new ActivityResultCallback() { - @Override - public void onActivityResult(ActivityResult result) { - if (checkDrawOverlayPermission(context)) { - pmr.onReturn(Status.GRANTED); - } else { - pmr.onReturn(Status.DENIED); - } + public static void checkForPermission(Context context, String permission, PermissionAskListener listener) { + if (permission == null) return; + if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE && SDK_INT >= VERSION_CODES.R) listener.onPermissionGranted(); + + if (shoudAskForPermission(context, permission)) { + if (((Activity) context).shouldShowRequestPermissionRationale(permission)) { + listener.onPermissionPreviouslyDenied(); + } else { + if (isFirstTimeAskingForPermission(permission)) { + ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, 69); + SharedPreference.getInstance().putBool(permission, true); + } else { + listener.onPermissionNeverAskAgain(); } - }).launch(intent); + } } else { - pmr.onReturn(Status.GRANTED); + listener.onPermissionGranted(); } } - public static boolean checkForPermission(Activity activity, String permission, int requestCode) { - if (permission == null) return false; - if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE || - SDK_INT >= VERSION_CODES.R) { return false; } - - if (activity.getApplicationContext().checkSelfPermission(permission) == - PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(activity, new String[] { permission }, requestCode); - return true; - } - - return false; - } - - public static ArrayMap checkForPermissions(Activity activity, String[] permissions, int requestCode) { + public static ArrayMap checkForPermissions(Activity activity, String[] permissions, int requestCode) { if (permissions == null) return null; - ArrayMap isPermissionAllowed = new ArrayMap<>(); + ArrayMap isPermissionAllowed = new ArrayMap<>(); + if (permissions.length > 1) { for (int idx = 0; idx < permissions.length; idx++) { - isPermissionAllowed.put(permissions[idx], checkForPermission(activity, permissions[idx], requestCode)); + final int i = idx; + checkForPermission(activity, permissions[idx], new PermissionAskListener() { + @Override + public void onPermissionPreviouslyDenied() { + isPermissionAllowed.put(permissions[i], Status.DENIED); + } + + @Override + public void onPermissionNeverAskAgain() { + isPermissionAllowed.put(permissions[i], Status.CANNOT_ASK); + } + + @Override + public void onPermissionGranted() { + isPermissionAllowed.put(permissions[i], Status.GRANTED); + } + }); } } return isPermissionAllowed; } - public static interface PermissionResult { - public void onReturn(Status returnValue); + public static boolean shoudAskForPermission(Context context, String permission) { + int permissionResult = ActivityCompat.checkSelfPermission(context, permission); + return permissionResult != PackageManager.PERMISSION_GRANTED; + } + + public static boolean isFirstTimeAskingForPermission(String permission) { + return SharedPreference.getInstance().getBoolFallback(permission, true); + } + + public static interface PermissionAskListener { + public default void onPermissionGranted() { } + public default void onPermissionPreviouslyDenied() { } + public default void onPermissionNeverAskAgain() { } } } diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/ProcessListener.java b/app/src/main/java/com/thatmg393/esmanager/utils/ProcessListener.java index b368083..b73819d 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/ProcessListener.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/ProcessListener.java @@ -1,19 +1,25 @@ package com.thatmg393.esmanager.utils; -import android.content.ComponentName; -import android.content.ServiceConnection; -import android.os.Binder; +import android.app.usage.UsageStatsManager; +import android.content.Context; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES; import android.app.Service; import android.app.usage.UsageEvents; +import android.content.ComponentName; import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; import android.os.IBinder; import com.thatmg393.esmanager.interfaces.IOnProcessListener; import java.util.HashMap; +import java.util.List; +import android.app.usage.UsageStats; +import java.util.SortedMap; +import java.util.TreeMap; public class ProcessListener implements ServiceConnection { private static volatile ProcessListener INSTANCE; @@ -65,22 +71,79 @@ public void onServiceDisconnected(ComponentName cn) { } private final class ProcessListenerThread extends Thread { + private final UsageStatsManager usageStatsManager; private final String processName; private final IOnProcessListener callback; private final boolean stopOnAppStop; - private ProcessListenerThread(String processName, IOnProcessListener callback, boolean stopOnAppStop) { + private ProcessListenerThread(Context context, String processName, IOnProcessListener callback, boolean stopOnAppStop) { this.processName = processName; this.callback = callback; this.stopOnAppStop = stopOnAppStop; + this.usageStatsManager = context.getSystemService(UsageStatsManager.class); } + private boolean opf; + private boolean opb; + @Override public void run() { while (!Thread.interrupted()) { - // TODO: Implement functionality + usageStatsManager.queryEvents(10, 10); + + if (isAppInForeground()) { + if (opb) { + callback.onProcessForeground(); + opb = false; + opf = true; + } + } else { + if (opf) { + callback.onProcessBackground(); + opf = false; + opb = true; + } + } + + try { Thread.sleep(1000); } + catch (InterruptedException e) { } } } + + private final boolean isAppInForeground() { + long time = System.currentTimeMillis(); + List stats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 1000, time); + + String topPackageName = null; + + if (stats != null) { + SortedMap sortedStats = new TreeMap<>(); + + for (UsageStats usageStats : stats) { + sortedStats.put(usageStats.getLastTimeUsed(), usageStats); + } + + if (sortedStats != null && !sortedStats.isEmpty()) { + topPackageName = sortedStats.get(sortedStats.lastKey()).getPackageName(); + } + } + return topPackageName.equals(processName); + } + + private final boolean isCurrentEventEqualTo(int currentEvent, int activityCode) { + if (SDK_INT < VERSION_CODES.R) { + switch (currentEvent) { + case UsageEvents.Event.ACTIVITY_PAUSED: + return currentEvent == UsageEvents.Event.MOVE_TO_BACKGROUND; + case UsageEvents.Event.ACTIVITY_RESUMED: + return currentEvent == UsageEvents.Event.MOVE_TO_FOREGROUND; + default: + break; + } + } + + return currentEvent == activityCode; + } } private class ProcessListenerService extends Service { @@ -88,7 +151,7 @@ private class ProcessListenerService extends Service { private final ProcessListenerBinder plBinder = new ProcessListenerBinder(); public void startListeningThread(String packageToListenTo, IOnProcessListener processListenerCallback, boolean stopOnAppStop) { - ProcessListenerThread plThread = new ProcessListenerThread(packageToListenTo, processListenerCallback, stopOnAppStop); + ProcessListenerThread plThread = new ProcessListenerThread(getApplicationContext(), packageToListenTo, processListenerCallback, stopOnAppStop); threads.put(packageToListenTo, plThread); plThread.start(); @@ -112,21 +175,6 @@ public boolean onUnbind(Intent intent) { return true; } - private final boolean isCurrentEventEqualTo(int currentEvent, int activityCode) { - if (SDK_INT < VERSION_CODES.R) { - switch (currentEvent) { - case UsageEvents.Event.ACTIVITY_PAUSED: - return currentEvent == UsageEvents.Event.MOVE_TO_BACKGROUND; - case UsageEvents.Event.ACTIVITY_RESUMED: - return currentEvent == UsageEvents.Event.MOVE_TO_FOREGROUND; - default: - break; - } - } - - return currentEvent == activityCode; - } - private class ProcessListenerBinder extends Binder { public ProcessListenerService getInstance() { return ProcessListenerService.this; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/SharedPreference.java b/app/src/main/java/com/thatmg393/esmanager/utils/SharedPreference.java index 53d0e77..d40d23f 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/SharedPreference.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/SharedPreference.java @@ -5,14 +5,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceDataStore; -import java.util.Set; import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; import com.thatmg393.esmanager.GlobalConstants; +import com.thatmg393.esmanager.utils.logging.Logger; import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.Set; public class SharedPreference { private static final Logger LOG = new Logger("ESM/SharedPreference"); diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/io/AndroidFolderUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/AndroidFolderUtils.java new file mode 100644 index 0000000..d330005 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/AndroidFolderUtils.java @@ -0,0 +1,103 @@ +package com.thatmg393.esmanager.utils.io; + +import android.content.Context; +import android.net.Uri; +import com.lazygeniouz.dfc.file.DocumentFileCompat; +import java.io.File; +import java.util.ArrayList; + +public class AndroidFolderUtils { + public static class AndroidFSNode { + protected final DocumentFileCompat file; + protected final Context context; + + protected AndroidFSNode(Context context, DocumentFileCompat file) { + this.context = context; + this.file = file; + } + + public AndroidFile getFile(String filename) { + return new AndroidFile(context, appendToUri(file.getUri(), filename)); + } + + public AndroidDirectory getDirectory(String foldername) { + return new AndroidDirectory(context, appendToUri(file.getUri(), foldername)); + } + + public AndroidDirectory getParent() { + return new AndroidDirectory(context, file.getParentFile().getUri()); + } + + public ArrayList listFSNodes() { + ArrayList alafsn = new ArrayList<>(); + + file.listFiles().stream().parallel().forEach(e -> alafsn.add( + e.isFile() ? new AndroidFile(context, e.getUri()) : new AndroidDirectory(context, e.getUri()) + )); + + return alafsn; + } + + public void rename(String newName) { + file.renameTo(newName); + } + + public boolean delete(String name) { + if (file.findFile(name).isDirectory()) return getDirectory(name).deleteMe(); + return getFile(name).deleteMe(); + } + + public boolean deleteMe() { + return file.delete(); + } + + protected final Uri appendToUri(Uri old, String f) { + return old.buildUpon().appendPath(f).build(); + } + } + + public static class AndroidDirectory extends AndroidFSNode { + private String folderName; + + public AndroidDirectory(Context context, Uri uri) { + super(context, DocumentFileCompat.fromTreeUri(context, uri)); + this.folderName = file.getName(); + } + + @Override + public void rename(String newName) { + super.rename(newName); + this.folderName = newName; + } + + public String getFolderName() { + return this.folderName; + } + } + + public static class AndroidFile extends AndroidFSNode { + private String fileName; + + public AndroidFile(Context context, Uri uri) { + super(context, DocumentFileCompat.fromSingleUri(context, uri)); + this.fileName = file.getName(); + } + + @Override + public void rename(String newName) { + super.rename(newName); + this.fileName = newName; + } + + public String getFileName() { + return this.fileName; + } + } + + public static AndroidDirectory getDataFolder(Context context) { + return new AndroidDirectory( + context, + DocumentFileCompat.fromFile(context, new File("/storage/emulated/0/Android/data")).getUri() + ); + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/BitmapUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/BitmapUtils.java similarity index 96% rename from app/src/main/java/com/thatmg393/esmanager/utils/BitmapUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/io/BitmapUtils.java index c39ed05..09106e7 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/BitmapUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/BitmapUtils.java @@ -1,4 +1,4 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.io; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -10,6 +10,8 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import com.thatmg393.esmanager.utils.ActivityUtils; + import java.io.IOException; import java.io.InputStream; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/FileUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/FileUtils.java similarity index 93% rename from app/src/main/java/com/thatmg393/esmanager/utils/FileUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/io/FileUtils.java index 60d43fa..b8570a1 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/FileUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/FileUtils.java @@ -1,13 +1,11 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.io; import io.github.rosemoe.sora.text.Content; import io.github.rosemoe.sora.text.ContentIO; -import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/NetworkUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/NetworkUtils.java similarity index 90% rename from app/src/main/java/com/thatmg393/esmanager/utils/NetworkUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/io/NetworkUtils.java index 055a097..3f7da8a 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/NetworkUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/NetworkUtils.java @@ -1,4 +1,4 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.io; import java.io.IOException; import java.net.ServerSocket; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/StorageUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/StorageUtils.java similarity index 78% rename from app/src/main/java/com/thatmg393/esmanager/utils/StorageUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/io/StorageUtils.java index eff63f8..1cb2470 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/StorageUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/StorageUtils.java @@ -1,5 +1,10 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.io; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES; @@ -18,6 +23,10 @@ import com.thatmg393.esmanager.GlobalConstants; import com.thatmg393.esmanager.interfaces.IOnAllowFolderAccess; import com.thatmg393.esmanager.interfaces.IOnFilePick; +import com.thatmg393.esmanager.utils.ActivityUtils; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.esmanager.utils.PermissionUtils; +import com.thatmg393.esmanager.utils.SharedPreference; import java.io.File; @@ -79,10 +88,18 @@ public static FileFullPath getFileFullPath(@NonNull String path) { return fullPath; } - public static boolean isStoragePermissionGranted() { - return PermissionUtils.checkForPermission(ActivityUtils.getInstance().getRegisteredActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE, GlobalConstants.RequestCodes.REQUEST_WRITE_ACCESS); + public static void isStoragePermissionGranted(PermissionUtils.PermissionAskListener listener) { + PermissionUtils.checkForPermission(ActivityUtils.getInstance().getRegisteredActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE, listener); } + /* + public static boolean isFilesVersionSupported(Context context) { + PackageManager pm = context.getSystemService(PackageManager.class); + PackageInfo filesPackageInfo = pm.getPackageInfo("com.google.android.documentsui", 0); + + } + */ + public static void initStorageHelper() { SSH = new SimpleStorageHelper(ActivityUtils.getInstance().getRegisteredActivity()); } @@ -95,8 +112,4 @@ public static SimpleStorageHelper getStorageHelper() { private static void checkIfStorageHelperNonNull() { if (SSH == null) throw new UnsupportedOperationException("Call 'StorageUtils#initStorageHelper()' first"); } - - public static enum Status { - GRANTED, DENIED, CANNOT_ASK, FAILURE - } } \ No newline at end of file diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/URIUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/io/URIUtils.java similarity index 98% rename from app/src/main/java/com/thatmg393/esmanager/utils/URIUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/io/URIUtils.java index 4b26171..e6dfcb4 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/URIUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/io/URIUtils.java @@ -1,4 +1,4 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.io; import android.content.Context; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/logging/ErrorHandler.java b/app/src/main/java/com/thatmg393/esmanager/utils/logging/ErrorHandler.java new file mode 100644 index 0000000..e5633c4 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/utils/logging/ErrorHandler.java @@ -0,0 +1,36 @@ +package com.thatmg393.esmanager.utils.logging; + +import com.thatmg393.esmanager.GlobalConstants; +import com.thatmg393.esmanager.utils.io.FileUtils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +public class ErrorHandler { + public static final void writeError(Throwable err) { + FileUtils.appendToFile(GlobalConstants.getInstance().getESMRootFolder() + "/errors.txt", getFullStacktrace(err)); + } + + public static final String getFullStacktrace(Throwable err) { + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + + try { + int loopTimes = 0; + + Throwable cause = err; + while (cause != null && loopTimes <= 45) { + cause.printStackTrace(printWriter); + cause = cause.getCause(); + loopTimes++; + } + + printWriter.close(); + result.close(); + } catch (IOException e) { } + + return result.toString(); + } +} diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/Logger.java b/app/src/main/java/com/thatmg393/esmanager/utils/logging/Logger.java similarity index 89% rename from app/src/main/java/com/thatmg393/esmanager/utils/Logger.java rename to app/src/main/java/com/thatmg393/esmanager/utils/logging/Logger.java index d6e5455..6fcc7d3 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/Logger.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/logging/Logger.java @@ -1,4 +1,4 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.logging; import android.util.Log; diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/EditorUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/sora/EditorUtils.java similarity index 67% rename from app/src/main/java/com/thatmg393/esmanager/utils/EditorUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/sora/EditorUtils.java index 806aeb9..f0ecf5e 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/EditorUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/sora/EditorUtils.java @@ -1,69 +1,49 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.sora; import android.widget.Toast; import androidx.core.util.Pair; -import com.thatmg393.esmanager.managers.editor.language.LanguageManager; +import com.thatmg393.esmanager.managers.editor.themes.ThemeManager; +import com.thatmg393.esmanager.utils.ActivityUtils; +import com.thatmg393.esmanager.utils.io.FileUtils; +import com.thatmg393.esmanager.utils.logging.Logger; +import com.thatmg393.treesitter.base.BaseTSLanguage; import io.github.rosemoe.sora.lang.Language; import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme; import io.github.rosemoe.sora.langs.textmate.TextMateLanguage; -import io.github.rosemoe.sora.langs.textmate.registry.FileProviderRegistry; -import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry; -import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel; import io.github.rosemoe.sora.text.Content; import io.github.rosemoe.sora.widget.CodeEditor; import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; -import org.eclipse.tm4e.core.registry.IThemeSource; - import java.io.IOException; import java.util.concurrent.CompletableFuture; public class EditorUtils { private static final Logger LOG = new Logger("ESM/EditorUtils"); - public static final String[] tmThemes = { - "darcula", - "quietlight" - }; - + public static void checkAndCorrectColorScheme(CodeEditor editor, Language lang) { + if (lang instanceof BaseTSLanguage) { + ensureTSTheme(editor); + } else if (lang instanceof TextMateLanguage) { + ensureTMTheme(editor); + } else { + editor.setColorScheme(new EditorColorScheme()); + } + } + public static void ensureTMTheme(CodeEditor editor) { EditorColorScheme colorScheme = editor.getColorScheme(); if (!(colorScheme instanceof TextMateColorScheme)) { - try { - colorScheme = TextMateColorScheme.create(ThemeRegistry.getInstance()); - } catch (Exception e) { - e.printStackTrace(); - } + colorScheme = ThemeManager.getInstance().getTheme("quitelight"); editor.setColorScheme(colorScheme); } } - public static TextMateLanguage createTMLanguage(String language) { - Language l = LanguageManager.getInstance().getLanguage(language); - if (l != null && l instanceof TextMateLanguage) return (TextMateLanguage) l; - return null; - } - - public static void loadTMThemes() { - ThemeRegistry themeRegistry = ThemeRegistry.getInstance(); - for(String theme : tmThemes) { - try { - String themePath = "tm/themes/" + theme + ".json"; - themeRegistry.loadTheme( - new ThemeModel( - IThemeSource.fromInputStream( - FileProviderRegistry.getInstance().tryGetInputStream(themePath), - themePath, null - ), theme - ) - ); - } catch (Exception e) { - e.printStackTrace(); - } - } + public static void ensureTSTheme(CodeEditor editor) { + EditorColorScheme tsTheme = ThemeManager.getInstance().getTheme("darcula"); + editor.setColorScheme(tsTheme); } public static void loadFileToEditor(CodeEditor editor, String path) { @@ -128,4 +108,8 @@ public static CompletableFuture saveFileFromEditor(Pair class2) { + return class1.getClass().equals(class2); + } } diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/LSPUtils.java b/app/src/main/java/com/thatmg393/esmanager/utils/sora/LSPUtils.java similarity index 56% rename from app/src/main/java/com/thatmg393/esmanager/utils/LSPUtils.java rename to app/src/main/java/com/thatmg393/esmanager/utils/sora/LSPUtils.java index 3c80471..4d93292 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/LSPUtils.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/sora/LSPUtils.java @@ -1,66 +1,75 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.sora; import android.widget.Toast; import androidx.annotation.NonNull; import com.thatmg393.esmanager.managers.editor.project.ProjectManager; +import com.thatmg393.esmanager.utils.ActivityUtils; +import com.thatmg393.esmanager.utils.logging.ErrorHandler; +import com.thatmg393.esmanager.utils.logging.Logger; -import io.github.rosemoe.sora.langs.textmate.TextMateLanguage; import io.github.rosemoe.sora.lsp.client.connection.StreamConnectionProvider; import io.github.rosemoe.sora.lsp.client.languageserver.requestmanager.RequestManager; import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.CustomLanguageServerDefinition; -import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.LanguageServerDefinition; import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.EventHandler; import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.LanguageServerWrapper; import io.github.rosemoe.sora.lsp.editor.LspEditor; -import io.github.rosemoe.sora.lsp.editor.LspEditorManager; import io.github.rosemoe.sora.widget.CodeEditor; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.services.LanguageServer; + import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; public class LSPUtils { private static final Logger LOG = new Logger("ESM/LSPUtils"); + private static final class NoImplEventListener implements EventHandler.EventListener { + public static final NoImplEventListener INSTANCE = new NoImplEventListener(); + + @Override + public void initialize(LanguageServer arg0, InitializeResult arg1) { } + + @Override + public void onHandlerException(Exception arg0) { } + + @Override + public void onLogMessage(MessageParams arg0) { } + + @Override + public void onShowMessage(MessageParams arg0) { } + } public static LanguageServerWrapper createNewServerWrapper( @NonNull String language, - @NonNull StreamConnectionProvider connectionProvider, - @NonNull String projectPath + @NonNull StreamConnectionProvider connectionProvider ) { return new LanguageServerWrapper( new CustomLanguageServerDefinition( - "." + language, + language, (workingDir) -> { return connectionProvider; } ) { @Override public EventHandler.EventListener getEventListener() { - return EventHandler.EventListener.DEFAULT; + return NoImplEventListener.INSTANCE; } - }, projectPath + }, ProjectManager.getInstance().getCurrentLspProject() ); } public static LspEditor createNewLspEditor( @NonNull String fileUri, - @NonNull LanguageServerDefinition serverDefinition, @NonNull CodeEditor editor ) { - LspEditor lspEditor = LspEditorManager.getOrCreateEditorManager(ProjectManager.getInstance().getCurrentProject().projectPath) - .createEditor( - fileUri, - serverDefinition - ); + LspEditor lspEditor = ProjectManager.getInstance().getCurrentLspProject() + .getOrCreateEditor(fileUri); if (lspEditor == null) { - LOG.w("LSPEditor for " + fileUri.toString() + " is somehow null..."); - lspEditor = LspEditorManager.getOrCreateEditorManager(ProjectManager.getInstance().getCurrentProject().projectPath) - .createEditor( - fileUri, - serverDefinition - ); + lspEditor = ProjectManager.getInstance().getCurrentLspProject() + .getOrCreateEditor(fileUri); } lspEditor.setWrapperLanguage(editor.getEditorLanguage()); @@ -74,16 +83,31 @@ public static void connectToLsp( ) { CompletableFuture.runAsync(() -> { try { - lspEditor.connectWithTimeout(); + lspEditor.connectWithTimeoutBlocking(); onLspConnected(lspEditor); - } catch(TimeoutException | InterruptedException e) { + } catch(Exception e) { e.printStackTrace(System.err); ActivityUtils.getInstance().runOnUIThread( - () -> Toast.makeText(lspEditor.getEditor().getContext(), "Failed to connect to LSP!\nNo completions will be provided.", Toast.LENGTH_SHORT).show() + (context) -> { + Toast.makeText(context, "Failed to connect to LSP!\nNo completions will be provided.", Toast.LENGTH_SHORT).show(); + } ); + ErrorHandler.writeError(e); } }); - } + } + + public static void disconnectToLsp( + @NonNull LspEditor lspEditor + ) { + CompletableFuture.runAsync(() -> { + try { + lspEditor.disconnect(); + } catch (Exception e) { + ErrorHandler.writeError(e); + } + }); + } public static void onLspConnected(LspEditor lspEditor) { RequestManager lspRequestManager = lspEditor.getRequestManager(); diff --git a/app/src/main/java/com/thatmg393/esmanager/utils/ThreadPlus.java b/app/src/main/java/com/thatmg393/esmanager/utils/threading/ThreadPlus.java similarity index 89% rename from app/src/main/java/com/thatmg393/esmanager/utils/ThreadPlus.java rename to app/src/main/java/com/thatmg393/esmanager/utils/threading/ThreadPlus.java index 36121fa..85442b5 100644 --- a/app/src/main/java/com/thatmg393/esmanager/utils/ThreadPlus.java +++ b/app/src/main/java/com/thatmg393/esmanager/utils/threading/ThreadPlus.java @@ -1,7 +1,9 @@ -package com.thatmg393.esmanager.utils; +package com.thatmg393.esmanager.utils.threading; import androidx.annotation.NonNull; +import com.thatmg393.esmanager.utils.logging.Logger; + /** ThreadPlus is a layer on top of {@link java.lang.Thread} * for easy use. * @@ -19,11 +21,7 @@ public class ThreadPlus implements Runnable { private final Runnable runnable; public ThreadPlus(@NonNull Runnable runnable) { - this.runnable = runnable; - this.thread = new Thread(this); - - thread.setDaemon(false); - thread.setPriority(Thread.MAX_PRIORITY); + this(runnable, true); } public ThreadPlus(@NonNull Runnable runnable, boolean loopThread) { @@ -88,7 +86,7 @@ public synchronized boolean isRunning() { return this.isRunning && !this.isDead; } - /** Gets the currently created {@link java.lang.Thread} + /** Gets the real created {@link java.lang.Thread} * @return the created thread */ public Thread getThread() { diff --git a/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/FolderViewHolder.java b/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/FolderViewHolder.java index 1109e4c..25b2bce 100644 --- a/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/FolderViewHolder.java +++ b/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/FolderViewHolder.java @@ -3,6 +3,7 @@ import android.view.View; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -10,6 +11,7 @@ import com.amrdeveloper.treeview.TreeViewHolder; import com.thatmg393.esmanager.R; +import com.thatmg393.esmanager.utils.ActivityUtils; import java.io.File; public class FolderViewHolder extends TreeViewHolder { diff --git a/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/NoFileViewHolder.java b/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/NoFileViewHolder.java new file mode 100644 index 0000000..11c9dc1 --- /dev/null +++ b/app/src/main/java/com/thatmg393/esmanager/viewholders/tree/NoFileViewHolder.java @@ -0,0 +1,19 @@ +package com.thatmg393.esmanager.viewholders.tree; + +import android.view.View; + +import androidx.annotation.NonNull; + +import com.amrdeveloper.treeview.TreeNode; +import com.amrdeveloper.treeview.TreeViewHolder; + +public class NoFileViewHolder extends TreeViewHolder { + public NoFileViewHolder(@NonNull View itemView) { + super(itemView); + } + + @Override + public void bindTreeNode(TreeNode node) { + super.bindTreeNode(node); + } +} diff --git a/app/src/main/res/layout/activity_project.xml b/app/src/main/res/layout/activity_project.xml index a156d36..c1f92cf 100644 --- a/app/src/main/res/layout/activity_project.xml +++ b/app/src/main/res/layout/activity_project.xml @@ -88,6 +88,8 @@ diff --git a/app/src/main/res/layout/project_file_tree_view.xml b/app/src/main/res/layout/project_file_tree_view.xml index ba97518..117a679 100644 --- a/app/src/main/res/layout/project_file_tree_view.xml +++ b/app/src/main/res/layout/project_file_tree_view.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:drawablePadding="@dimen/_6sdp" android:padding="@dimen/_12sdp" android:id="@+id/project_treeview_file_name" /> \ No newline at end of file diff --git a/app/src/main/res/layout/project_folder_tree_view.xml b/app/src/main/res/layout/project_folder_tree_view.xml index e243dc9..9daa28e 100644 --- a/app/src/main/res/layout/project_folder_tree_view.xml +++ b/app/src/main/res/layout/project_folder_tree_view.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:drawablePadding="@dimen/_6sdp" android:padding="@dimen/_12sdp" android:id="@+id/project_treeview_folder_name" /> \ No newline at end of file diff --git a/app/src/main/res/layout/project_nofile_tree_view.xml b/app/src/main/res/layout/project_nofile_tree_view.xml new file mode 100644 index 0000000..96b3d1f --- /dev/null +++ b/app/src/main/res/layout/project_nofile_tree_view.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8b87272..0523352 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.0.2' apply false - id 'de.undercouch.download' version '5.4.0' + id 'com.android.application' version '8.2.2' apply false + id 'com.android.library' version '8.2.2' apply false + id 'org.jetbrains.kotlin.android' version '2.0.0-Beta4' apply false + // id 'de.undercouch.download' version '5.4.0' } task clean(type: Delete) { diff --git a/gradle.properties b/gradle.properties index c346aad..cbc7c47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,26 +6,28 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs= -Xms750m -Xmx750m -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 +org.gradle.jvmargs= -Xms650m -Xmx650m -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:MaxTenuringThreshold=1 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -org.gradle.parallel=true - -# org.gradle.cache=true +# org.gradle.parallel=true + +org.gradle.cache=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true +android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official +# kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true +android.nonTransitiveRClass=true + +android.enableJetifier=true -android.enableJetifier=true \ No newline at end of file +android.experimental.dependency.excludeLibraryComponentsFromConstraints=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83..a363877 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 4899b2b..428e71f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,5 @@ dependencyResolutionManagement { } rootProject.name = "ESManager" include ':app' +include ':tree-sitter' +include ':sora-editor-treesitter' diff --git a/sora-editor-treesitter/.gitignore b/sora-editor-treesitter/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/sora-editor-treesitter/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sora-editor-treesitter/build.gradle.kts b/sora-editor-treesitter/build.gradle.kts new file mode 100644 index 0000000..d20d764 --- /dev/null +++ b/sora-editor-treesitter/build.gradle.kts @@ -0,0 +1,53 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +plugins { + id("com.android.library") + id("kotlin-android") +} + +group = "io.github.Rosemoe.sora-editor" +version = "0.23.4-f620608-SNAPSHOT" + +android { + namespace = "io.github.rosemoe.sora.ts" + compileSdk = 33 + + defaultConfig { + minSdk = 24 + + consumerProguardFiles("consumer-rules.pro") + } +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + implementation("com.itsaky.androidide.treesitter:android-tree-sitter:4.1.0") + + implementation(platform("io.github.Rosemoe.sora-editor:bom:0.23.4-f620608-SNAPSHOT")) + implementation("io.github.Rosemoe.sora-editor:editor") +} diff --git a/sora-editor-treesitter/consumer-rules.pro b/sora-editor-treesitter/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/sora-editor-treesitter/proguard-rules.pro b/sora-editor-treesitter/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/sora-editor-treesitter/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/AndroidManifest.xml b/sora-editor-treesitter/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a81b319 --- /dev/null +++ b/sora-editor-treesitter/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt new file mode 100644 index 0000000..37ae516 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -0,0 +1,249 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.treesitter.TSQueryCursor +import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory +import io.github.rosemoe.sora.lang.styling.Span +import io.github.rosemoe.sora.lang.styling.SpanFactory +import io.github.rosemoe.sora.lang.styling.Spans +import io.github.rosemoe.sora.lang.styling.TextStyle +import io.github.rosemoe.sora.text.CharPosition +import io.github.rosemoe.sora.text.Content +import io.github.rosemoe.sora.widget.schemes.EditorColorScheme + +/** + * Spans generator for tree-sitter. Results are cached. + * + * Note that this implementation does not support external modifications. + * + * @author Rosemoe + */ +class LineSpansGenerator( + internal var safeTree: SafeTsTree, internal var lineCount: Int, + private val content: Content, internal var theme: TsTheme, + private val languageSpec: TsLanguageSpec, var scopedVariables: TsScopedVariables, + private val spanFactory: TsSpanFactory +) : Spans { + + companion object { + const val CACHE_THRESHOLD = 60 + } + + private val caches = mutableListOf() + + fun queryCache(line: Int): MutableList? { + for (i in 0 until caches.size) { + val cache = caches[i] + if (cache.line == line) { + caches.removeAt(i) + caches.add(0, cache) + return cache.spans + } + } + return null + } + + fun pushCache(line: Int, spans: MutableList) { + while (caches.size >= CACHE_THRESHOLD) { + caches.removeAt(caches.size - 1) + } + caches.add(0, SpanCache(spans, line)) + } + + fun captureRegion(startIndex: Int, endIndex: Int): MutableList { + val list = mutableListOf() + val captures = mutableListOf() + + TSQueryCursor.create().use { cursor -> + cursor.setByteRange(startIndex * 2, endIndex * 2) + + safeTree.accessTree { tree -> + if (languageSpec.closed || tree.closed) { + return@accessTree + } + + cursor.exec(languageSpec.tsQuery, tree.rootNode) + var match = cursor.nextMatch() + while (match != null) { + if (languageSpec.queryPredicator.doPredicate( + languageSpec.predicates, + content, + match + ) + ) { + captures.addAll(match.captures) + } + match = cursor.nextMatch() + } + captures.sortBy { it.node.startByte } + var lastIndex = 0 + captures.forEach { capture -> + val startByte = capture.node.startByte + val endByte = capture.node.endByte + val start = (startByte / 2 - startIndex).coerceAtLeast(0) + val pattern = capture.index + // Do not add span for overlapping regions and out-of-bounds regions + if (start >= lastIndex && endByte / 2 >= startIndex && startByte / 2 < endIndex + && (pattern !in languageSpec.localsScopeIndices && pattern !in languageSpec.localsDefinitionIndices + && pattern !in languageSpec.localsDefinitionValueIndices && pattern !in languageSpec.localsMembersScopeIndices) + ) { + if (start != lastIndex) { + list.addAll( + createSpans( + capture, + lastIndex, + start - 1, + theme.normalTextStyle + ) + ) + } + var style = 0L + if (capture.index in languageSpec.localsReferenceIndices) { + val def = scopedVariables.findDefinition( + startByte / 2, + endByte / 2, + content.substring(startByte / 2, endByte / 2) + ) + if (def != null && def.matchedHighlightPattern != -1) { + style = theme.resolveStyleForPattern(def.matchedHighlightPattern) + } + // This reference can not be resolved to its definition + // but it can have its own fallback color by other captures + // so continue to next capture + if (style == 0L) { + return@forEach + } + } + if (style == 0L) { + style = theme.resolveStyleForPattern(capture.index) + } + if (style == 0L) { + style = theme.normalTextStyle + } + val end = (endByte / 2 - startIndex).coerceAtMost(endIndex) + list.addAll(createSpans(capture, start, end, style)) + lastIndex = end + } + } + if (lastIndex != endIndex) { + list.add(emptySpan(lastIndex)) + } + } + } + if (list.isEmpty()) { + list.add(emptySpan(0)) + } + return list + } + + private fun createSpans( + capture: TSQueryCapture, + startColumn: Int, + endColumn: Int, + style: Long + ): List { + val spans = spanFactory.createSpans(capture, startColumn, style) + if (spans.size > 1) { + var prevCol = spans[0].column + if (prevCol > endColumn) { + throw IndexOutOfBoundsException("Span's column is out of bounds! column=$prevCol, endColumn=$endColumn") + } + for (i in 1..spans.lastIndex) { + val col = spans[i].column + if (col <= prevCol) { + throw IllegalStateException("Spans must not overlap! prevCol=$prevCol, col=$col") + } + if (col > endColumn) { + throw IndexOutOfBoundsException("Span's column is out of bounds! column=$col, endColumn=$endColumn") + } + prevCol = col + } + } + return spans + } + + private fun emptySpan(column: Int): Span { + return SpanFactory.obtain( + column, + TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL) + ) + } + + override fun adjustOnInsert(start: CharPosition, end: CharPosition) { + + } + + override fun adjustOnDelete(start: CharPosition, end: CharPosition) { + + } + + override fun read() = object : Spans.Reader { + + private var spans = mutableListOf() + + override fun moveToLine(line: Int) { + if (line < 0 || line >= lineCount) { + spans = mutableListOf() + return + } + val cached = queryCache(line) + if (cached != null) { + spans = cached + return + } + val start = content.indexer.getCharPosition(line, 0).index + val end = start + content.getColumnCount(line) + spans = captureRegion(start, end) + pushCache(line, spans) + } + + override fun getSpanCount() = spans.size + + override fun getSpanAt(index: Int) = spans[index] + + override fun getSpansOnLine(line: Int): MutableList { + val cached = queryCache(line) + if (cached != null) { + return ArrayList(cached) + } + val start = content.indexer.getCharPosition(line, 0).index + val end = start + content.getColumnCount(line) + return captureRegion(start, end) + } + + } + + override fun supportsModify() = false + + override fun modify(): Spans.Modifier { + throw UnsupportedOperationException() + } + + override fun getLineCount() = lineCount +} + +data class SpanCache(val spans: MutableList, val line: Int) diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt new file mode 100644 index 0000000..49bc7f3 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt @@ -0,0 +1,55 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +/** + * [LocalsCaptureSpec] helps our parser to recognize local variables and generate different spans + * to highlight them. + * + * @author Rosemoe + */ +open class LocalsCaptureSpec { + + companion object { + val DEFAULT = LocalsCaptureSpec() + } + + open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value" + + open fun isDefinitionCapture(captureName: String) = captureName == "local.definition" + + open fun isReferenceCapture(captureName: String) = captureName == "local.reference" + + open fun isScopeCapture(captureName: String) = captureName == "local.scope" + + /** + * Usually, variables in a scope take effect after their declarations. This special scope + * indicates that, all variables in this scope (but not in its sub-scope), take effect in this + * scope, no matter where they are. + * For example, class member fields. + */ + open fun isMembersScopeCapture(captureName: String) = captureName == "local.scope.members" + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/SafeTsTree.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/SafeTsTree.kt new file mode 100644 index 0000000..03e30c3 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/SafeTsTree.kt @@ -0,0 +1,149 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSTree +import java.util.concurrent.locks.ReentrantLock + +/** + * Safe accessor for [TSTree] instances. single [TSTree] is not thread-safe. This class adds lock + * to make thread-safe access to the tree. + * + * @author Rosemoe + */ +class SafeTsTree(private val tree: TSTree) : AutoCloseable { + + private val lock = ReentrantLock() + + /** + * Close the original [TSTree] + */ + override fun close() { + accessTree { tree.close() } + } + + /** + * Make access to the tree. All access to the tree must be in the param [block]. + * The [TreeAccessor] object will be invalid immediately after the given [block] is executed. + * + * @return the result for executing the given [block] + */ + fun accessTree(block: (tree: TreeAccessor) -> R): R { + lock.lock() + try { + val accessor = TreeAccessor() + val result = block(accessor) + accessor.accessible = false + return result + } finally { + lock.unlock() + } + } + + /** + * Make access to the tree if the tree is not currently closed. + * + * @see accessTree + * @return whether the given [block] is executed + */ + fun accessTreeIfAvailable(block: (tree: TreeAccessor) -> Unit): Boolean { + if (!tree.canAccess()) { + return false + } + return accessTree { + if (!it.closed) { + block(it) + true + } else { + false + } + } + } + + /** + * Accessor for [TSTree] + */ + inner class TreeAccessor(internal var accessible: Boolean = true) { + + /** + * Check if the [TreeAccessor] can be used and the original tree is not closed. + */ + private fun checkAccess() { + if (closed) + throw IllegalStateException("executing operation on dead accessor") + } + + /** + * Root node of the tree. This should not be stored outside the accessing block. + * + * @see TSTree.getRootNode + */ + val rootNode: TSNode + get() { + checkAccess() + return tree.rootNode + } + + /** + * Language of the tree + * + * @see TSTree.getLanguage + */ + val language: TSLanguage + get() { + checkAccess() + return tree.language + } + + /** + * If the [TreeAccessor] can be used and the original tree is not closed. + */ + val closed: Boolean + get() { + return !accessible || !tree.canAccess() + } + + /** + * Copy a new [TSTree] + */ + fun copy(): TSTree { + checkAccess() + return tree.copy() + } + + /** + * Edit the [TSTree] + */ + fun edit(edit: TSInputEdit) { + checkAccess() + tree.edit(edit) + } + + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt new file mode 100644 index 0000000..fc9cf24 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt @@ -0,0 +1,329 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import android.os.Bundle +import android.os.Message +import android.util.Log +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSParser +import com.itsaky.androidide.treesitter.TSQueryCursor +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.string.UTF16String +import com.itsaky.androidide.treesitter.string.UTF16StringFactory +import io.github.rosemoe.sora.data.ObjectAllocator +import io.github.rosemoe.sora.editor.ts.spans.DefaultSpanFactory +import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory +import io.github.rosemoe.sora.lang.analysis.AnalyzeManager +import io.github.rosemoe.sora.lang.analysis.StyleReceiver +import io.github.rosemoe.sora.lang.styling.CodeBlock +import io.github.rosemoe.sora.lang.styling.Styles +import io.github.rosemoe.sora.lang.util.BaseAnalyzeManager +import io.github.rosemoe.sora.text.CharPosition +import io.github.rosemoe.sora.text.ContentReference +import java.util.concurrent.LinkedBlockingQueue + +open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme) : + BaseAnalyzeManager() { + + val currentReceiver: StyleReceiver? + get() = receiver + val reference: ContentReference? + get() = contentRef + var thread: TsLooperThread? = null + var spanFactory : TsSpanFactory = DefaultSpanFactory() + + open var styles = Styles() + + fun updateTheme(theme: TsTheme) { + this.theme = theme + val spans = styles.spans + spans?.let { + if (it is LineSpansGenerator) + it.theme = theme + } + } + + override fun insert(start: CharPosition, end: CharPosition, insertedContent: CharSequence) { + thread?.offerMessage( + MSG_MOD, + TextModification( + start.index, + end.index, + newTSInputEdit(start, start, end), + insertedContent.toString() + ) + ) + (styles.spans as LineSpansGenerator?)?.apply { + lineCount = reference!!.lineCount + safeTree.accessTreeIfAvailable { + it.edit(newTSInputEdit(start, start, end)) + } + } + } + + override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) { + thread?.offerMessage( + MSG_MOD, + TextModification( + start.index, + end.index, + newTSInputEdit(start, end, start), + null + ) + ) + (styles.spans as LineSpansGenerator?)?.apply { + lineCount = reference!!.lineCount + safeTree.accessTreeIfAvailable { + it.edit(newTSInputEdit(start, end, start)) + } + } + } + + override fun rerun() { + destroyPreviousRes() + styles = Styles() + val initText = reference?.reference?.toString() ?: "" + thread = TsLooperThread().also { + it.name = "TsDaemon-${nextThreadId()}" + it.offerMessage(MSG_INIT, initText) + it.start() + } + } + + override fun destroy() { + destroyPreviousRes() + spanFactory.close() + super.destroy() + } + + /** + * Destroy resources related to previous worker thread, and reset spans. + */ + protected fun destroyPreviousRes() { + thread?.let { + if (it.isAlive) { + it.interrupt() + it.abort = true + } + } + val spans = styles.spans + // IMPORTANT avoid access to the tree after destruction + styles.spans = null + if (spans is LineSpansGenerator) { + spans.safeTree.close() + } + } + + companion object { + private const val MSG_BASE = 11451400 + private const val MSG_INIT = MSG_BASE + 1 + private const val MSG_MOD = MSG_BASE + 2 + + @Volatile + private var threadId = 0 + + @Synchronized + fun nextThreadId() = ++threadId + } + + inner class TsLooperThread : Thread() { + + private val messageQueue = LinkedBlockingQueue() + + @Volatile + var abort: Boolean = false + val localText: UTF16String = UTF16StringFactory.newString() + private val parser = TSParser.create().also { + it.language = languageSpec.language + } + var tree: TSTree? = null + + fun offerMessage(what: Int, obj: Any?) { + val msg = Message.obtain() + msg.what = what + msg.obj = obj + offerMessage(msg) + } + + fun offerMessage(msg: Message) { + // Result ignored: capacity is enough as it is INT_MAX + messageQueue.offer(msg) + } + + fun updateStyles() { + val scopedVariables = TsScopedVariables(tree!!, localText, languageSpec) + if (thread == this && messageQueue.isEmpty()) { + val oldTree = (styles.spans as LineSpansGenerator?)?.safeTree + val newTree = SafeTsTree(tree!!.copy()) + styles.spans = LineSpansGenerator( + newTree, + reference!!.lineCount, + reference!!.reference, + theme, + languageSpec, + scopedVariables, + spanFactory + ) + val oldBlocks = styles.blocks + updateCodeBlocks() + currentReceiver?.setStyles(this@TsAnalyzeManager, styles) { + oldTree?.close() + if (oldBlocks != null) { + ObjectAllocator.recycleBlockLines(oldBlocks) + } + } + currentReceiver?.updateBracketProvider( + this@TsAnalyzeManager, + TsBracketPairs(newTree, languageSpec) + ) + } + } + + fun updateCodeBlocks() { + if (languageSpec.blocksQuery.patternCount == 0 || !languageSpec.blocksQuery.canAccess()) { + return + } + val blocks = mutableListOf() + TSQueryCursor.create().use { + it.exec(languageSpec.blocksQuery, tree!!.rootNode) + var match = it.nextMatch() + while (match != null) { + if (languageSpec.blocksPredicator.doPredicate( + languageSpec.predicates, + localText, + match + ) + ) { + match.captures.forEach { + val block = ObjectAllocator.obtainBlockLine().also { block -> + var node = it.node + val start = node.startPoint + block.startLine = start.row + block.startColumn = start.column / 2 + val end = if (languageSpec.blocksQuery.getCaptureNameForId(it.index) + .endsWith(".marked") + ) { + // Goto last terminal element + while (node.childCount > 0) { + node = node.getChild(node.childCount - 1) + } + node.startPoint + } else { + node.endPoint + } + block.endLine = end.row + block.endColumn = end.column / 2 + } + if (block.endLine - block.startLine > 1) { + blocks.add(block) + } + } + } + match = it.nextMatch() + } + } + // sequence should be preferred here in order to avoid allocating multiple lists and sets + val distinct = blocks.asSequence().distinct().toMutableList() + styles.blocks = distinct + styles.finishBuilding() + } + + override fun run() { + try { + while (!abort && !isInterrupted) { + val msg = messageQueue.take() + if (!handleMessage(msg)) { + break + } + msg.recycle() + } + } catch (e: InterruptedException) { + // ignored + } + releaseThreadResources() + } + + fun handleMessage(msg: Message): Boolean { + try { + when (msg.what) { + MSG_INIT -> { + localText.append(msg.obj!! as String) + if (!abort && !isInterrupted) { + tree = parser.parseString(localText) + updateStyles() + } + } + + MSG_MOD -> { + if (!abort && !isInterrupted) { + val modification = msg.obj!! as TextModification + val newText = modification.changedText + val t = tree!! + t.edit(modification.tsEdition) + if (newText == null) { + localText.delete(modification.start, modification.end) + } else { + if (modification.start == localText.length) { + localText.append(newText) + } else { + localText.insert(modification.start, newText) + } + } + tree = parser.parseString(t, localText) + t.close() + updateStyles() + } + } + } + return true + } catch (e: Exception) { + Log.w( + "TsAnalyzeManager", + "Thread $name exited with an error", + e + ) + } + return false + } + + fun releaseThreadResources() { + parser.close() + tree?.close() + localText.close() + } + + } + + data class TextModification( + val start: Int, + val end: Int, + val tsEdition: TSInputEdit, + /** + * null for deletion + */ + val changedText: String? + ) +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt new file mode 100644 index 0000000..e276496 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt @@ -0,0 +1,103 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSQueryCursor +import io.github.rosemoe.sora.lang.brackets.BracketsProvider +import io.github.rosemoe.sora.lang.brackets.PairedBracket +import io.github.rosemoe.sora.text.Content +import java.lang.Math.max + +class TsBracketPairs( + private val safeTree: SafeTsTree, + private val languageSpec: TsLanguageSpec +) : BracketsProvider { + + companion object { + + val OPEN_NAME = "editor.brackets.open" + val CLOSE_NAME = "editor.brackets.close" + + } + + override fun getPairedBracketAt(text: Content, index: Int): PairedBracket? { + if (languageSpec.bracketsQuery.canAccess() && languageSpec.bracketsQuery.patternCount > 0) { + TSQueryCursor.create().use { cursor -> + cursor.setByteRange(max(0, index - 1) * 2, index * 2 + 1) + + return safeTree.accessTree { tree -> + if (tree.closed) return@accessTree null + val rootNode = tree.rootNode + if (!rootNode.canAccess() || rootNode.hasChanges()) { + return@accessTree null + } + cursor.exec(languageSpec.bracketsQuery, rootNode) + var match = cursor.nextMatch() + var matched = false + val pos = IntArray(4) + while (match != null && !matched) { + if (languageSpec.bracketsPredicator.doPredicate( + languageSpec.predicates, + text, + match + ) + ) { + pos.fill(-1) + for (capture in match.captures) { + val captureName = + languageSpec.bracketsQuery.getCaptureNameForId(capture.index) + if (captureName == OPEN_NAME || captureName == CLOSE_NAME) { + val node = capture.node + if (index >= node.startByte / 2 && index <= node.endByte / 2) { + matched = true + } + if (captureName == OPEN_NAME) { + pos[0] = node.startByte + pos[1] = node.endByte + } else { + pos[2] = node.startByte + pos[3] = node.endByte + } + } + } + if (matched && pos[0] != -1 && pos[2] != -1) { + return@accessTree PairedBracket( + pos[0] / 2, + (pos[1] - pos[0]) / 2, + pos[2] / 2, + (pos[3] - pos[2]) / 2 + ) + } + } + match = cursor.nextMatch() + } + null + } + } + } + return null + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt new file mode 100644 index 0000000..cab44ee --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt @@ -0,0 +1,112 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import android.os.Bundle +import io.github.rosemoe.sora.lang.EmptyLanguage +import io.github.rosemoe.sora.lang.Language +import io.github.rosemoe.sora.lang.completion.CompletionPublisher +import io.github.rosemoe.sora.lang.format.Formatter +import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler +import io.github.rosemoe.sora.text.CharPosition +import io.github.rosemoe.sora.text.ContentReference +import io.github.rosemoe.sora.widget.SymbolPairMatch + +/** + * Tree-sitter based language. + * + * @param languageSpec The language specification for parsing and highlighting + * @param themeDescription Theme for colorizing nodes + * @param tab whether tab should be used + * + * @see TsTheme + * @see TsLanguageSpec + * + * @author Rosemoe + */ +open class TsLanguage( + val languageSpec: TsLanguageSpec, + val tab: Boolean = false, + themeDescription: TsThemeBuilder.() -> Unit +) : Language { + + init { + if (languageSpec.closed) { + throw IllegalStateException("spec is closed") + } + } + + protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme + + open val analyzer by lazy { + TsAnalyzeManager(languageSpec, tsTheme) + } + + /** + * Update tree-sitter colorizing theme with the given description + */ + fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = languageSpec.let { + if (it.closed) { + throw IllegalStateException("spec is closed") + } + updateTheme(TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme) + } + + /** + * Update tree-sitter colorizing theme + */ + fun updateTheme(theme: TsTheme) { + this.tsTheme = theme + analyzer.updateTheme(theme) + } + + override fun getAnalyzeManager() = analyzer + + override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG + + override fun requireAutoComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + extraArguments: Bundle + ) { + // Nothing + } + + override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0 + + override fun useTab() = tab + + override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE + + override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS + + override fun getNewlineHandlers() = emptyArray() + + override fun destroy() { + languageSpec.close() + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt new file mode 100644 index 0000000..48b330d --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt @@ -0,0 +1,184 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryError +import io.github.rosemoe.sora.editor.ts.predicate.Predicator +import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate +import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate +import java.io.Closeable + +/** + * Language specification for tree-sitter highlighter. This specification covers language code + * parsing, highlighting captures and local variable tracking descriptions. + * + * Note that you must use ASCII characters in your scm sources. Otherwise, an [IllegalArgumentException] is + * thrown. + * Be careful that this should be closed to avoid native memory leaks. + * + * @author Rosemoe + * @param language The tree-sitter language instance to be used for parsing + * @param highlightScmSource The scm source code for highlighting tree nodes + * @param codeBlocksScmSource The scm source for capturing code blocks. + * All captured nodes are considered to be a code block. + * Capture named with '.marked' suffix will have its last terminal node's start position as its scope end + * @param bracketsScmSource The scm source for capturing brackets. Capture named 'editor.brackets.open' and 'editor.brackets.close' are used to compute bracket pairs + * @param localsScmSource The scm source code for tracking local variables + * @param localsCaptureSpec Custom specification for locals scm file + * @param predicates Client custom predicate implementations + */ +open class TsLanguageSpec( + val language: TSLanguage, + highlightScmSource: String, + codeBlocksScmSource: String = "", + bracketsScmSource: String = "", + localsScmSource: String = "", + localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, + val predicates: List = listOf(MatchPredicate) +) : Closeable { + + /** + * The generated scm source code for querying + */ + val querySource = localsScmSource + "\n" + highlightScmSource + + /** + * Offset of highlighting scm source code in [querySource] + */ + val highlightScmOffset = localsScmSource.encodeToByteArray().size + 1 + + /** + * The actual [TSQuery] object + */ + val tsQuery = TSQuery.create(language, querySource) + + /** + * The first index of highlighting pattern + */ + val highlightPatternOffset: Int + + /** + * Indices of variable definition patterns + */ + val localsDefinitionIndices = mutableListOf() + + /** + * Indices of variable reference patterns + */ + val localsReferenceIndices = mutableListOf() + + /** + * Indices of variable scope patterns + */ + val localsScopeIndices = mutableListOf() + + /** + * Indices of weak variable scope patterns + * @see [LocalsCaptureSpec.isMembersScopeCapture] for more information + */ + val localsMembersScopeIndices = mutableListOf() + + /** + * Indices of variable definition-value patterns. Currently unused in analysis. + */ + val localsDefinitionValueIndices = mutableListOf() + + val blocksQuery = if (codeBlocksScmSource.isBlank()) { + TSQuery.EMPTY + } else TSQuery.create(language, codeBlocksScmSource) + + val bracketsQuery = if (bracketsScmSource.isBlank()) { + TSQuery.EMPTY + } else TSQuery.create(language, bracketsScmSource) + + init { + // Check the queries before access + try { + blocksQuery.validateOrThrow("code-blocks") + bracketsQuery.validateOrThrow("brackets") + querySource.forEach { + if (it > 0xFF.toChar()) { + throw IllegalArgumentException("use non-ASCII characters in scm source is unexpected") + } + } + if (!tsQuery.canAccess()) { + throw IllegalArgumentException("Syntax highlights query is invalid") + } + if (tsQuery.errorType != TSQueryError.None) { + val region = if (tsQuery.errorOffset < highlightScmOffset) "locals" else "highlight" + val offset = + if (tsQuery.errorOffset < highlightScmOffset) tsQuery.errorOffset else tsQuery.errorOffset - highlightScmOffset + throw IllegalArgumentException("bad scm sources: error ${tsQuery.errorType.name} occurs in $region range at offset $offset") + } + } catch (e: IllegalArgumentException) { + close() + throw e + } + } + + val queryPredicator = Predicator(tsQuery) + val blocksPredicator = Predicator(blocksQuery) + val bracketsPredicator = Predicator(bracketsQuery) + + /** + * Close flag + */ + var closed = false + private set + + init { + var highlightOffset = 0 + for (i in 0 until tsQuery.captureCount) { + // Only locals in localsScm are taken down + val name = tsQuery.getCaptureNameForId(i) + if (localsCaptureSpec.isDefinitionCapture(name)) { + localsDefinitionIndices.add(i) + } else if (localsCaptureSpec.isReferenceCapture(name)) { + localsReferenceIndices.add(i) + } else if (localsCaptureSpec.isScopeCapture(name)) { + localsScopeIndices.add(i) + } else if (localsCaptureSpec.isDefinitionValueCapture(name)) { + localsDefinitionValueIndices.add(i) + } else if (localsCaptureSpec.isMembersScopeCapture(name)) { + localsMembersScopeIndices.add(i) + } + } + for (i in 0 until tsQuery.patternCount) { + if (tsQuery.getStartByteForPattern(i) < highlightScmOffset) { + highlightOffset++ + } + } + highlightPatternOffset = highlightOffset + } + + override fun close() { + tsQuery.close() + blocksQuery.close() + bracketsQuery.close() + closed = true + } +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt new file mode 100644 index 0000000..863ab8c --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt @@ -0,0 +1,138 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.treesitter.TSQueryCursor +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.string.UTF16String +import java.util.Stack + +/** + * Class for storing tree-sitter variables. This class tracks the positions and scopes + * of variables and find definitions. + * + * @author Rosemoe + * @param tree The parsed tree + * @param text The current text for tree + * @param spec Language specification, which should the same as highlighter's + */ +class TsScopedVariables(tree: TSTree, text: UTF16String, val spec: TsLanguageSpec) { + + private val rootScope: Scope = Scope(0, tree.rootNode.endByte / 2) + + init { + if (spec.localsDefinitionIndices.isNotEmpty()) { + TSQueryCursor.create().use { cursor -> + cursor.exec(spec.tsQuery, tree.rootNode) + var match = cursor.nextMatch() + val captures = mutableListOf() + while (match != null) { + if (spec.queryPredicator.doPredicate(spec.predicates, text, match)) { + captures.addAll(match.captures) + } + match = cursor.nextMatch() + } + captures.sortBy { it.node.startByte } + val scopeStack = Stack() + var lastAddedVariableNode: TSNode? = null + scopeStack.push(rootScope) + for (capture in captures) { + val startIndex = capture.node.startByte / 2 + val endIndex = capture.node.endByte / 2 + while (startIndex >= scopeStack.peek().endIndex) { + scopeStack.pop() + } + val pattern = capture.index + if (pattern in spec.localsScopeIndices) { + val newScope = Scope(startIndex, endIndex) + scopeStack.peek().childScopes.add(newScope) + scopeStack.push(newScope) + } else if (pattern in spec.localsMembersScopeIndices) { + val newScope = Scope(startIndex, endIndex, true) + scopeStack.peek().childScopes.add(newScope) + scopeStack.push(newScope) + } else if (pattern in spec.localsDefinitionIndices) { + val scope = scopeStack.peek() + val utf16Name = text.subseqChars(startIndex, endIndex) + val name = utf16Name.toString() + utf16Name.close() + val scopedVar = ScopedVariable( + name, + if (scope.forMembers) scope.startIndex else startIndex, + scope.endIndex + ) + scope.variables.add(scopedVar) + lastAddedVariableNode = capture.node + } else if (pattern !in spec.localsDefinitionValueIndices && pattern !in spec.localsReferenceIndices && lastAddedVariableNode != null) { + val topVariables = scopeStack.peek().variables + if (topVariables.isNotEmpty()) { + val topVariable = topVariables.last() + if (lastAddedVariableNode.startByte / 2 == startIndex && lastAddedVariableNode.endByte / 2 == endIndex && topVariable.matchedHighlightPattern == -1) { + topVariable.matchedHighlightPattern = pattern + } + } + } + } + } + } + } + + data class Scope( + val startIndex: Int, + val endIndex: Int, + val forMembers: Boolean = false, + val variables: MutableList = mutableListOf(), + val childScopes: MutableList = mutableListOf() + ) + + data class ScopedVariable( + var name: String, + var scopeStartIndex: Int, + var scopeEndIndex: Int, + var matchedHighlightPattern: Int = -1 + ) + + fun findDefinition(startIndex: Int, endIndex: Int, name: String): ScopedVariable? { + var definition: ScopedVariable? = null + var currentScope: Scope? = rootScope + while (currentScope != null) { + for (variable in currentScope.variables) { + if (variable.scopeStartIndex > startIndex) { + break + } + if (variable.scopeStartIndex <= startIndex && variable.scopeEndIndex >= endIndex && variable.name == name) { + definition = variable + // Do not break here: name can be shadowed in some languages + } + } + currentScope = + currentScope.childScopes.firstOrNull { scope -> scope.startIndex <= startIndex && scope.endIndex >= endIndex } + } + return definition + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt new file mode 100644 index 0000000..a9e3ae0 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt @@ -0,0 +1,114 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import android.util.SparseLongArray +import com.itsaky.androidide.treesitter.TSQuery +import io.github.rosemoe.sora.lang.styling.TextStyle +import io.github.rosemoe.sora.widget.schemes.EditorColorScheme + +/** + * Theme for tree-sitter. This is different from [io.github.rosemoe.sora.widget.schemes.EditorColorScheme]. + * It is only used for colorizing spans in tree-sitter module. The real colors are still stored in editor + * color schemes. + * As what tree-sitter do, we try to match the longest scope. + * For example, if 'variable' and 'variable.builtin' rule are both defined, Query 'variable.builtin' + * and 'variable.builtin.this' will get 'variable.builtin' rule. + * The theme also provide a fallback. You may call [TsTheme.putStyleRule] with a rule of length 0 to + * set fallback color scheme. + * Note that colors of 'locals.definition', 'locals.reference', etc. can not be set by this theme object. + * + * @author Rosemoe + */ +class TsTheme(private val tsQuery: TSQuery) { + + private val styles = mutableMapOf() + private val mapping = SparseLongArray() + + /** + * The text style for normal texts + */ + var normalTextStyle = TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL) + + /** + * Set text style for the given rule string. + * + * @param rule The rule for locating nodes + * @param style The style value for those nodes + * @see io.github.rosemoe.sora.lang.styling.TextStyle + */ + fun putStyleRule(rule: String, style: Long) { + styles[rule] = style + mapping.clear() + } + + /** + * Remove rule + * @param rule The rule for locating nodes + */ + fun eraseStyleRule(rule: String) = putStyleRule(rule, 0L) + + fun resolveStyleForPattern(pattern: Int): Long { + val index = mapping.indexOfKey(pattern) + return if (index >= 0) { + mapping.valueAt(index) + } else { + var mappedName = tsQuery.getCaptureNameForId(pattern) + var style = styles[mappedName] ?: 0L + while (style == 0L && mappedName.isNotEmpty()) { + mappedName = mappedName.substringBeforeLast('.', "") + style = styles[mappedName] ?: 0L + } + mapping.put(pattern, style) + style + } + } + +} + +/** + * Builder class for tree-sitter themes + */ +class TsThemeBuilder(tsQuery: TSQuery) { + + internal val theme = TsTheme(tsQuery) + + infix fun Long.applyTo(targetRule: String) { + theme.putStyleRule(targetRule, this) + } + + infix fun Long.applyTo(targetRules: Array) { + targetRules.forEach { + applyTo(it) + } + } + +} + +/** + * Build tree-sitter theme + */ +fun tsTheme(tsQuery: TSQuery, description: TsThemeBuilder.() -> Unit) = + TsThemeBuilder(tsQuery).also { it.description() }.theme \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt new file mode 100644 index 0000000..913ead5 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt @@ -0,0 +1,55 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryError +import io.github.rosemoe.sora.text.CharPosition + +/** + * Convert a [CharPosition] object to a [TSPoint] object + */ +fun CharPosition.toTSPoint(): TSPoint = TSPoint.create(line, column * 2) + +fun TSQuery.validateOrThrow(name: String = "unknown") { + if (errorType != TSQueryError.None) { + throw IllegalArgumentException("query(name:$name) parsing failed: ${errorType.name} at text offset $errorOffset") + } +} + +/** + * Create a new [TSInputEdit] object for the given positions + */ +fun newTSInputEdit(start: CharPosition, oldEnd: CharPosition, newEnd: CharPosition) = + TSInputEdit.create( + start.index * 2, + oldEnd.index * 2, + newEnd.index * 2, + start.toTSPoint(), + oldEnd.toTSPoint(), + newEnd.toTSPoint() + ) \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt new file mode 100644 index 0000000..de7150c --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt @@ -0,0 +1,37 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.multilang + +interface LanguagePriorityCheck { + + companion object { + val PRIORITY_NEVER = 0 + val PRIORITY_AS_FALLBACK = 1 + val PRIORITY_ALWAYS = 1000 + } + + fun getPriorityByName(name: String) : Int + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt new file mode 100644 index 0000000..4bab025 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt @@ -0,0 +1,28 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.multilang + +interface TsIndentHelper { +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt new file mode 100644 index 0000000..7a21a5b --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt @@ -0,0 +1,56 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.multilang + +import com.itsaky.androidide.treesitter.TSLanguage +import io.github.rosemoe.sora.editor.ts.LocalsCaptureSpec +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec +import io.github.rosemoe.sora.editor.ts.TsThemeBuilder +import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate +import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate + +class TsInjectableLanguageSpec( + language: TSLanguage, + highlightScmSource: String, + themeDescription: TsThemeBuilder.() -> Unit, + val languageName: LanguagePriorityCheck, + val indentHelper: TsIndentHelper? = null, + codeBlocksScmSource: String = "", + bracketsScmSource: String = "", + localsScmSource: String = "", + localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, + predicates: List = listOf(MatchPredicate) +) : TsLanguageSpec(language, highlightScmSource, codeBlocksScmSource, bracketsScmSource, localsScmSource, localsCaptureSpec, predicates) { + + var theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme + + fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = run { + if (closed) { + throw IllegalStateException("spec is closed") + } + theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt new file mode 100644 index 0000000..81628db --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt @@ -0,0 +1,46 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +/** + * Predicate result for [TsPredicate] + */ +enum class PredicateResult { + /** + * The given predicate is not handled by this [TsPredicate]. + * The [com.itsaky.androidide.treesitter.TSQueryMatch] object will be passed to other [TsPredicate]. + */ + UNHANDLED, + + /** + * The given predicate is accepted by the [TsPredicate] + */ + ACCEPT, + + /** + * The given predicate is not accepted by the [TsPredicate] + */ + REJECT +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt new file mode 100644 index 0000000..841bebe --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt @@ -0,0 +1,94 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.treesitter.TSQueryPredicateStep + +/** + * Predicate runner for tree-sitter + * + * @author Rosemoe + */ +class Predicator(private val query: TSQuery) { + + /** + * Predicates for patterns + */ + private val patternPredicates = mutableListOf>() + + init { + for (i in 0 until query.patternCount) { + patternPredicates.add(query.getPredicatesForPattern(i).map { + when (it.type) { + TSQueryPredicateStep.Type.String -> TsClientPredicateStep( + it.type, + query.getStringValueForId(it.valueId) + ) + + TSQueryPredicateStep.Type.Capture -> TsClientPredicateStep( + it.type, + query.getCaptureNameForId(it.valueId) + ) + + else -> TsClientPredicateStep(it.type, "") + } + }) + } + } + + fun doPredicate( + predicates: List, + text: CharSequence, + match: TSQueryMatch, + syntheticCaptureContainer: TsSyntheticCaptureContainer = TsSyntheticCaptureContainer.EMPTY_IMMUTABLE_CONTAINER + ): Boolean { + val description = patternPredicates[match.patternIndex] + var tail = 0 + for (i in description.indices) { + if (description[i].predicateType == TSQueryPredicateStep.Type.Done) { + // Avoid allocating sublist if possible + val subPredicateStep = + if (tail == 0 && i + 1 == description.size) description else description.subList( + tail, + i + 1 + ) + for (j in predicates.indices) { + val predicate = predicates[j] + when (predicate.doPredicate(query, text, match, subPredicateStep, syntheticCaptureContainer)) { + PredicateResult.ACCEPT -> break + PredicateResult.REJECT -> return false + else -> {} + } + } + tail = i + 1 + } + } + + return true + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt new file mode 100644 index 0000000..549b9c3 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt @@ -0,0 +1,32 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +import com.itsaky.androidide.treesitter.TSQueryPredicateStep + +data class TsClientPredicateStep( + val predicateType: TSQueryPredicateStep.Type, + val content: String +) diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt new file mode 100644 index 0000000..4ff45d5 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt @@ -0,0 +1,48 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryMatch + +/** + * Predicate client-side implementation + */ +interface TsPredicate { + + /** + * Run the predicate on the given [TSQueryMatch] + * @see TSQueryMatch + * @see PredicateResult + */ + fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt new file mode 100644 index 0000000..9d9ef5a --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt @@ -0,0 +1,33 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +import com.itsaky.androidide.treesitter.TSNode + +data class TsSyntheticCapture( + val captureName: String, + val captureText: String? = null, + val captureNode: TSNode? = null +) \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt new file mode 100644 index 0000000..5ce097c --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt @@ -0,0 +1,57 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate + +class TsSyntheticCaptureContainer(private val noAddOperation: Boolean = false) { + + companion object { + val EMPTY_IMMUTABLE_CONTAINER = TsSyntheticCaptureContainer(true) + } + + private val syntheticCaptures = mutableListOf() + + val indices + get() = syntheticCaptures.indices + + val size + get() = syntheticCaptures.size + + operator fun get(index: Int) = syntheticCaptures[index] + + fun addSyntheticCapture(syntheticCapture: TsSyntheticCapture) { + if (syntheticCapture.captureNode == null && syntheticCapture.captureText == null) { + throw IllegalArgumentException("at least one field between 'captureText' and 'captureNode' should be non-null") + } + if (noAddOperation) { + return + } + syntheticCaptures.add(syntheticCapture) + } + + fun clear() { + syntheticCaptures.clear() + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt new file mode 100644 index 0000000..92d6d0d --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt @@ -0,0 +1,72 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate.builtin + +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.treesitter.TSQueryPredicateStep.Type +import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult +import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep +import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate +import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer +import java.util.concurrent.ConcurrentHashMap +import java.util.regex.PatternSyntaxException + +object MatchPredicate : TsPredicate { + + private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done) + + private val cache = ConcurrentHashMap() + + override fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult { + if (!parametersMatch(predicateSteps, PARAMETERS) || predicateSteps[0].content != "match?") { + return PredicateResult.UNHANDLED + } + val captured = getCaptureContent(tsQuery, match, predicateSteps[1].content, text) + try { + var regex = cache[predicateSteps[2].content] + if (regex == null) { + regex = Regex(predicateSteps[2].content) + cache[predicateSteps[2].content] = regex + } + for (str in captured) { + if (regex.find(str) == null) { + return PredicateResult.REJECT + } + } + return PredicateResult.ACCEPT + } catch (e: PatternSyntaxException) { + e.printStackTrace() + return PredicateResult.UNHANDLED + } + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt new file mode 100644 index 0000000..43db539 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt @@ -0,0 +1,82 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate.builtin + +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.treesitter.TSQueryPredicateStep +import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult +import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep +import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate +import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCapture +import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer + +object SetCapturePredicate : TsPredicate { + + private val PARAMETERS_1 = arrayOf( + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Done + ) + private val PARAMETERS_2 = arrayOf( + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.Done + ) + + override fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult { + if (predicateSteps[0].content == "set!") { + if (parametersMatch(predicateSteps, PARAMETERS_1)) { + syntheticCaptures.addSyntheticCapture( + TsSyntheticCapture( + predicateSteps[1].content, + predicateSteps[2].content + ) + ) + } else if (parametersMatch(predicateSteps, PARAMETERS_2)) { + val captureTexts = getCaptureContent(tsQuery, match, predicateSteps[2].content, text) + if (captureTexts.size == 1) { + syntheticCaptures.addSyntheticCapture( + TsSyntheticCapture( + predicateSteps[1].content, + captureTexts[0] + ) + ) + } + } + } + // As this does not affect whether the match is actually valid, we always return UNHANDLED + return PredicateResult.UNHANDLED + } + +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt new file mode 100644 index 0000000..09e76b7 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt @@ -0,0 +1,68 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.predicate.builtin + +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.treesitter.TSQueryPredicateStep.Type +import com.itsaky.androidide.treesitter.string.UTF16String +import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep +import io.github.rosemoe.sora.text.Content + +fun parametersMatch( + predicate: List, + types: Array +): Boolean { + if (predicate.size == types.size) { + for (i in types.indices) { + if (predicate[i].predicateType != types[i]) { + return false + } + } + return true + } + return false +} + +fun getCaptureContent( + tsQuery: TSQuery, + match: TSQueryMatch, + captureName: String, + text: CharSequence +) = match.captures.filter { tsQuery.getCaptureNameForId(it.index) == captureName } + .map { + val start = it.node.startByte / 2 + val end = it.node.endByte / 2 + when (text) { + is UTF16String -> { + text.subseqChars(start, end).use { + it.toString() + } + } + + is Content -> text.substring(start, end) + else -> text.substring(start, end) + } + } \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt new file mode 100644 index 0000000..938d9cd --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt @@ -0,0 +1,49 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.spans + +import com.itsaky.androidide.treesitter.TSQueryCapture +import io.github.rosemoe.sora.lang.styling.Span +import io.github.rosemoe.sora.lang.styling.SpanFactory + +/** + * Default implementation of the [TsSpanFactory]. + * + * @author Akash Yadav + */ +open class DefaultSpanFactory : TsSpanFactory { + + override fun createSpans(capture: TSQueryCapture, column: Int, spanStyle: Long): List { + return listOf( + SpanFactory.obtain( + column, + spanStyle + ) + ) + } + + override fun close() { + } +} \ No newline at end of file diff --git a/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt new file mode 100644 index 0000000..59eac89 --- /dev/null +++ b/sora-editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt @@ -0,0 +1,46 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2024 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.editor.ts.spans + +import com.itsaky.androidide.treesitter.TSQueryCapture +import io.github.rosemoe.sora.lang.styling.Span + +/** + * Factory for creating spans for the tree sitter analyze manager. + * + * @author Akash Yadav + */ +interface TsSpanFactory : AutoCloseable { + + /** + * Creates the spans using the provided data. + * + * @param capture The query capture. More information about the node can be found using the [TSQueryCapture.node] object. + * @param column The start column index for the span. + * @param spanStyle The style for the spans. + * @return The [Span] objects. + */ + fun createSpans(capture: TSQueryCapture, column: Int, spanStyle: Long): List +} \ No newline at end of file diff --git a/tree-sitter/build.gradle b/tree-sitter/build.gradle new file mode 100644 index 0000000..922be5e --- /dev/null +++ b/tree-sitter/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.android.library' +} + +android { + namespace "com.thatmg393.treesitter" + compileSdk 33 + + defaultConfig { + minSdk 24 + targetSdk 33 + versionCode 1 + versionName "v1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + // implementation 'androidx.appcompat:appcompat:1.7.0-alpha02' + + implementation platform('io.github.Rosemoe.sora-editor:bom:0.23.4-f620608-SNAPSHOT') + implementation 'io.github.Rosemoe.sora-editor:editor' + implementation project(path: ':sora-editor-treesitter') + + implementation 'com.itsaky.androidide.treesitter:android-tree-sitter:4.1.0' +} diff --git a/tree-sitter/src/main/AndroidManifest.xml b/tree-sitter/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0746c7d --- /dev/null +++ b/tree-sitter/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tree-sitter/src/main/assets/lua/highlights.scm b/tree-sitter/src/main/assets/lua/highlights.scm new file mode 100644 index 0000000..76014db --- /dev/null +++ b/tree-sitter/src/main/assets/lua/highlights.scm @@ -0,0 +1,273 @@ +; Keywords +"return" @keyword.return + +[ + "goto" + "in" + "local" +] @keyword + +(break_statement) @keyword + +(do_statement + [ + "do" + "end" + ] @keyword) + +(while_statement + [ + "while" + "do" + "end" + ] @keyword.repeat) + +(repeat_statement + [ + "repeat" + "until" + ] @keyword.repeat) + +(if_statement + [ + "if" + "elseif" + "else" + "then" + "end" + ] @keyword.conditional) + +(elseif_statement + [ + "elseif" + "then" + "end" + ] @keyword.conditional) + +(else_statement + [ + "else" + "end" + ] @keyword.conditional) + +(for_statement + [ + "for" + "do" + "end" + ] @keyword.repeat) + +(function_declaration + [ + "function" + "end" + ] @keyword.function) + +(function_definition + [ + "function" + "end" + ] @keyword.function) + +; Operators +[ + "and" + "not" + "or" +] @keyword.operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "<<" + ">>" + "//" + ".." +] @operator + +; Punctuations +[ + ";" + ":" + "::" + "," + "." +] @punctuation.delimiter + +; Brackets +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +; Variables +(identifier) @variable + +((identifier) @constant.builtin + (#eq? @constant.builtin "_VERSION")) + +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) + +((identifier) @module.builtin + (#any-of? @module.builtin "_G" "debug" "io" "jit" "math" "os" "package" "string" "table" "utf8")) + +((identifier) @keyword.coroutine + (#eq? @keyword.coroutine "coroutine")) + +(variable_list + (attribute + "<" @punctuation.bracket + (identifier) @attribute + ">" @punctuation.bracket)) + +; Labels +(label_statement + (identifier) @label) + +(goto_statement + (identifier) @label) + +; Constants +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +(vararg_expression) @constant + +(nil) @constant.builtin + +[ + (false) + (true) +] @boolean + +; Tables +(field + name: (identifier) @variable.member) + +(dot_index_expression + field: (identifier) @variable.member) + +(table_constructor + [ + "{" + "}" + ] @constructor) + +; Functions +(parameters + (identifier) @variable.parameter) + +(vararg_expression) @variable.parameter.builtin + +(function_declaration + name: + [ + (identifier) @function + (dot_index_expression + field: (identifier) @function) + ]) + +(function_declaration + name: + (method_index_expression + method: (identifier) @function.method)) + +(assignment_statement + (variable_list + . + name: + [ + (identifier) @function + (dot_index_expression + field: (identifier) @function) + ]) + (expression_list + . + value: (function_definition))) + +(table_constructor + (field + name: (identifier) @function + value: (function_definition))) + +(function_call + name: + [ + (identifier) @function.call + (dot_index_expression + field: (identifier) @function.call) + (method_index_expression + method: (identifier) @function.method.call) + ]) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" "load" "loadfile" + "loadstring" "module" "next" "pairs" "pcall" "print" "rawequal" "rawget" "rawlen" "rawset" + "require" "select" "setfenv" "setmetatable" "tonumber" "tostring" "type" "unpack" "xpcall" + "__add" "__band" "__bnot" "__bor" "__bxor" "__call" "__concat" "__div" "__eq" "__gc" "__idiv" + "__index" "__le" "__len" "__lt" "__metatable" "__mod" "__mul" "__name" "__newindex" "__pairs" + "__pow" "__shl" "__shr" "__sub" "__tostring" "__unm")) + +; Others +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-][-]")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-](%s?)@")) + +(hash_bang_line) @keyword.directive + +(number) @number + +(string) @string + +(escape_sequence) @string.escape + +; string.match("123", "%d+") +(function_call + (dot_index_expression + field: (identifier) @_method + (#any-of? @_method "find" "match" "gmatch" "gsub")) + arguments: + (arguments + . + (_) + . + (string + content: (string_content) @string.regexp))) + +;("123"):match("%d+") +(function_call + (method_index_expression + method: (identifier) @_method + (#any-of? @_method "find" "match" "gmatch" "gsub")) + arguments: + (arguments + . + (string + content: (string_content) @string.regexp))) \ No newline at end of file diff --git a/tree-sitter/src/main/assets/lua/indents.scm b/tree-sitter/src/main/assets/lua/indents.scm new file mode 100644 index 0000000..d58bf84 --- /dev/null +++ b/tree-sitter/src/main/assets/lua/indents.scm @@ -0,0 +1,42 @@ +[ + (function_definition) + (function_declaration) + (field) + (do_statement) + (method_index_expression) + (while_statement) + (repeat_statement) + (if_statement) + "then" + (for_statement) + (return_statement) + (table_constructor) + (arguments) + (return_statement) +] @indent.begin + +[ + "end" + ")" + "}" +] @indent.end + +(return_statement + (expression_list + (function_call))) @indent.dedent + +[ + "end" + "then" + "until" + "}" + ")" + "elseif" + (elseif_statement) + "else" + (else_statement) +] @indent.branch + +(comment) @indent.auto + +(string) @indent.auto \ No newline at end of file diff --git a/tree-sitter/src/main/assets/lua/locals.scm b/tree-sitter/src/main/assets/lua/locals.scm new file mode 100644 index 0000000..a38fa57 --- /dev/null +++ b/tree-sitter/src/main/assets/lua/locals.scm @@ -0,0 +1,56 @@ +; Scopes +[ + (chunk) + (do_statement) + (while_statement) + (repeat_statement) + (if_statement) + (for_statement) + (function_declaration) + (function_definition) +] @local.scope + +; Definitions +(assignment_statement + (variable_list + (identifier) @local.definition.var)) + +(assignment_statement + (variable_list + (dot_index_expression + . + (_) @local.definition.associated + (identifier) @local.definition.var))) + +((function_declaration + name: (identifier) @local.definition.function) + (#set! definition.function.scope "parent")) + +((function_declaration + name: + (dot_index_expression + . + (_) @local.definition.associated + (identifier) @local.definition.function)) + (#set! definition.method.scope "parent")) + +((function_declaration + name: + (method_index_expression + . + (_) @local.definition.associated + (identifier) @local.definition.method)) + (#set! definition.method.scope "parent")) + +(for_generic_clause + (variable_list + (identifier) @local.definition.var)) + +(for_numeric_clause + name: (identifier) @local.definition.var) + +(parameters + (identifier) @local.definition.parameter) + +; References +(identifier) @local.reference \ No newline at end of file diff --git a/tree-sitter/src/main/assets/lua/tags.scm b/tree-sitter/src/main/assets/lua/tags.scm new file mode 100644 index 0000000..0b47d8c --- /dev/null +++ b/tree-sitter/src/main/assets/lua/tags.scm @@ -0,0 +1,34 @@ +(function_declaration + name: [ + (identifier) @name + (dot_index_expression + field: (identifier) @name) + ]) @definition.function + +(function_declaration + name: (method_index_expression + method: (identifier) @name)) @definition.method + +(assignment_statement + (variable_list . + name: [ + (identifier) @name + (dot_index_expression + field: (identifier) @name) + ]) + (expression_list . + value: (function_definition))) @definition.function + +(table_constructor + (field + name: (identifier) @name + value: (function_definition))) @definition.function + +(function_call + name: [ + (identifier) @name + (dot_index_expression + field: (identifier) @name) + (method_index_expression + method: (identifier) @name) + ]) @reference.call \ No newline at end of file diff --git a/tree-sitter/src/main/java/com/itsaky/androidide/treesitter/lua/TSLanguageLua.java b/tree-sitter/src/main/java/com/itsaky/androidide/treesitter/lua/TSLanguageLua.java new file mode 100644 index 0000000..e53d36f --- /dev/null +++ b/tree-sitter/src/main/java/com/itsaky/androidide/treesitter/lua/TSLanguageLua.java @@ -0,0 +1,41 @@ +package com.itsaky.androidide.treesitter.lua; + +import android.util.Log; + +import dalvik.annotation.optimization.FastNative; + +import com.itsaky.androidide.treesitter.TSLanguage; +import com.itsaky.androidide.treesitter.TSLanguageCache; + +public class TSLanguageLua { + public static class Native { + @FastNative + public static native long getInstance(); + } + + public static TSLanguage getInstance() { + TSLanguage tSLanguage = TSLanguageCache.get("lua"); + if (tSLanguage != null) return tSLanguage; + + long libHandle = Native.getInstance(); + Log.d("TSJNI", "Got TSLuaLib handle! Handle: " + libHandle); + tSLanguage = TSLanguage.create("lua", libHandle); + TSLanguageCache.cache("lua", tSLanguage); + + return tSLanguage; + } + + private TSLanguageLua() { + throw new UnsupportedOperationException(); + } + + static { + Log.i("TSLanguageLua", "Loading tree-sitter-lua, my native library!"); + System.loadLibrary("tree-sitter-lua"); + } + + @Deprecated + public static TSLanguage newInstance() { + return getInstance(); + } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/TSLanguageSpecExt.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/TSLanguageSpecExt.java new file mode 100644 index 0000000..c9e3771 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/TSLanguageSpecExt.java @@ -0,0 +1,18 @@ +package com.thatmg393.treesitter; + +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec; + +import java.io.Closeable; +import java.io.IOException; + +public class TSLanguageSpecExt implements Closeable { + public TSLanguageSpecExt( + TsLanguageSpec langSpec, + String indentsQueryScm + ) { + System.out.println("no-op :)"); + } + + @Override + public void close() throws IOException { } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/base/BaseTSLanguage.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/base/BaseTSLanguage.java new file mode 100644 index 0000000..a913817 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/base/BaseTSLanguage.java @@ -0,0 +1,22 @@ +package com.thatmg393.treesitter.base; + +import com.itsaky.androidide.treesitter.util.Consumer; + +import io.github.rosemoe.sora.editor.ts.TsLanguage; +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec; +import io.github.rosemoe.sora.editor.ts.TsThemeBuilder; + +import kotlin.Unit; + +public abstract class BaseTSLanguage extends TsLanguage { + public BaseTSLanguage( + TsLanguageSpec langSpec, + boolean tab, + Consumer theme + ) { + super(langSpec, tab, (builder) -> { + theme.accept(builder); + return Unit.INSTANCE; + }); + } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/LuaLanguageSpec.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/LuaLanguageSpec.java new file mode 100644 index 0000000..c811ae1 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/LuaLanguageSpec.java @@ -0,0 +1,61 @@ +package com.thatmg393.treesitter.lua.sora; + +import com.itsaky.androidide.treesitter.TSQuery; +import com.itsaky.androidide.treesitter.TSQueryError; +import com.itsaky.androidide.treesitter.lua.TSLanguageLua; + +import io.github.rosemoe.sora.editor.ts.LocalsCaptureSpec; +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec; +import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate; + +import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class LuaLanguageSpec extends TsLanguageSpec { + private TSQuery indentsQuery; + + public LuaLanguageSpec( + String highlightsScmSrc, + String localsScmSrc, + String indentsScmSrc + // String tagsScmSrc + ) { + super( + TSLanguageLua.getInstance(), + highlightsScmSrc, + "", + "", + localsScmSrc, + LocalsCaptureSpec.Companion.getDEFAULT(), + new ArrayList() /* Arrays.asList(new TsPredicate[] { + new MatchPredicate() + }) */ + ); + + indentsQuery = createTSQuery("indents", indentsScmSrc); + } + + @Override + public void close() { + super.close(); + if (indentsQuery != null) indentsQuery.close(); + } + + private TSQuery createTSQuery(String queryName, String querySrc) { + TSQuery q = (querySrc.isBlank()) ? TSQuery.EMPTY : TSQuery.create(getLanguage(), querySrc); + if (!q.canAccess()) { + throw new RuntimeException( + "java.lang.IllegalArgumentException : Query source is invalid!" + ); + } + if (q != null && q.getErrorType() != TSQueryError.None) { + throw new RuntimeException( + "java.lang.IllegalArgumentException : query(name:" + queryName + ") parsing failed with error " + q.getErrorType().name() + " at text offset " + q.getErrorOffset() + ); + } + + return q; + } +} \ No newline at end of file diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLanguageLua.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLanguageLua.java new file mode 100644 index 0000000..b9b9b8d --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLanguageLua.java @@ -0,0 +1,17 @@ +package com.thatmg393.treesitter.lua.sora; + +import com.thatmg393.treesitter.base.BaseTSLanguage; +import com.thatmg393.treesitter.sora.TSThemeBuilder; + +import io.github.rosemoe.sora.editor.ts.TsAnalyzeManager; + +public class TsLanguageLua extends BaseTSLanguage { + public TsLanguageLua(LuaLanguageSpec luaSpec, boolean tab) { + super(luaSpec, tab, builder -> TSThemeBuilder.buildThemeLua(builder)); + } + + @Override + public TsAnalyzeManager getAnalyzeManager() { + return new TsLuaAnalysisManager(getLanguageSpec(), getTsTheme()); + } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaAnalysisManager.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaAnalysisManager.java new file mode 100644 index 0000000..2acb6b5 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaAnalysisManager.java @@ -0,0 +1,19 @@ +package com.thatmg393.treesitter.lua.sora; + +import io.github.rosemoe.sora.editor.ts.TsAnalyzeManager; +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec; +import io.github.rosemoe.sora.editor.ts.TsTheme; +import io.github.rosemoe.sora.lang.styling.Styles; + +public class TsLuaAnalysisManager extends TsAnalyzeManager { + public TsLuaAnalysisManager(TsLanguageSpec langSpec, TsTheme tsTheme) { + super(langSpec, tsTheme); + + setStyles(new Styles()); + setSpanFactory(new TsLuaSpanFactory( + getReference(), + langSpec.getTsQuery(), + getStyles() + )); + } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaSpanFactory.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaSpanFactory.java new file mode 100644 index 0000000..692c233 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/lua/sora/TsLuaSpanFactory.java @@ -0,0 +1,49 @@ +package com.thatmg393.treesitter.lua.sora; + +import com.itsaky.androidide.treesitter.TSQuery; +import com.itsaky.androidide.treesitter.TSQueryCapture; + +import io.github.rosemoe.sora.editor.ts.spans.DefaultSpanFactory; +import io.github.rosemoe.sora.lang.styling.Span; +import io.github.rosemoe.sora.lang.styling.Styles; +import io.github.rosemoe.sora.text.Content; +import io.github.rosemoe.sora.text.ContentReference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class TsLuaSpanFactory extends DefaultSpanFactory { + public static final String HEX_REGEX = "null"; + + private ContentReference contentRef; + private TSQuery query; + private Styles styles; + + public TsLuaSpanFactory(ContentReference contentRef, TSQuery query, Styles styles) { + this.contentRef = contentRef; + this.query = query; + this.styles = styles; + } + + @Override + public List createSpans(TSQueryCapture capture, int column, long spanStyle) { + List spans = new ArrayList<>(); + + Content content = Objects.requireNonNull(contentRef.getReference()); + + String captureName = query.getCaptureNameForId(capture.getIndex()); + System.out.println(captureName); + + return super.createSpans(capture, column, spanStyle); + } + + @Override + public void close() { + super.close(); + + contentRef = null; + query = null; + styles = null; + } +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/predicates/TSMatchPredicate.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/predicates/TSMatchPredicate.java new file mode 100644 index 0000000..4481f8c --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/predicates/TSMatchPredicate.java @@ -0,0 +1,4 @@ +package com.thatmg393.treesitter.predicates; + +public class TSMatchPredicate { +} diff --git a/tree-sitter/src/main/java/com/thatmg393/treesitter/sora/TSThemeBuilder.java b/tree-sitter/src/main/java/com/thatmg393/treesitter/sora/TSThemeBuilder.java new file mode 100644 index 0000000..38a1e00 --- /dev/null +++ b/tree-sitter/src/main/java/com/thatmg393/treesitter/sora/TSThemeBuilder.java @@ -0,0 +1,70 @@ +package com.thatmg393.treesitter.sora; + +import com.itsaky.androidide.treesitter.TSQuery; + +import io.github.rosemoe.sora.editor.ts.TsThemeBuilder; +import io.github.rosemoe.sora.lang.styling.TextStyle; +import io.github.rosemoe.sora.lang.styling.TextStyleKt; +import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; + +public class TSThemeBuilder { + /* makeStyle( + type, + bg, bold, italic, strkthr, comp + ) */ + + public static TsThemeBuilder buildThemeLua(TsThemeBuilder themeBuilder) { + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.COMMENT, + 0, false, true, false, false + ), "comment" + ); + + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.KEYWORD, + 0, true, false, false, false + ), "keyword" + ); + + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.LITERAL, + 0, false, false, false, false + ), new String[] { + "constant.builtin", "string", + "number" + } + ); + + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.IDENTIFIER_VAR, + 0, false, false, false, false + ), new String[] { + "variable.builtin", "variable", + "constant" + } + ); + + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.IDENTIFIER_VAR, + 0, false, false, false, false + ), new String[] { + "function.method", + "function.builtin", "variable.field" + } + ); + + themeBuilder.applyTo( + TextStyle.makeStyle( + EditorColorScheme.OPERATOR, + 0, false, false, false, false + ), "operator" + ); + + return themeBuilder; + } +} diff --git a/tree-sitter/src/main/java/dalvik/annotation/optimization/FastNative.java b/tree-sitter/src/main/java/dalvik/annotation/optimization/FastNative.java new file mode 100644 index 0000000..92beee5 --- /dev/null +++ b/tree-sitter/src/main/java/dalvik/annotation/optimization/FastNative.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 dalvik.annotation.optimization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An ART runtime built-in optimization for {@code native} methods to speed up JNI transitions: + * Compared to normal {@code native} methods, {@code native} methods that are annotated with + * {@literal @}{@code FastNative} use faster JNI transitions from managed code to the native code + * and back. Calls from a {@literal @}{@code FastNative} method implementation to JNI functions + * that access the managed heap or call managed code also have faster internal transitions. + * + *

+ * While executing a {@literal @}{@code FastNative} method, the garbage collection cannot + * suspend the thread for essential work and may become blocked. Use with caution. Do not use + * this annotation for long-running methods, including usually-fast, but generally unbounded, + * methods. In particular, the code should not perform significant I/O operations or acquire + * native locks that can be held for a long time. (Some logging or native allocations, which + * internally acquire native locks for a short time, are generally OK. However, as the cost + * of several such operations adds up, the {@literal @}{@code FastNative} performance gain + * can become insignificant and overshadowed by potential GC delays.) + * Acquiring managed locks is OK as it internally allows thread suspension. + *

+ * + *

+ * For performance critical methods that need this annotation, it is strongly recommended + * to explicitly register the method(s) with JNI {@code RegisterNatives} instead of relying + * on the built-in dynamic JNI linking. + *

+ * + *

+ * The {@literal @}{@code FastNative} optimization was implemented for system use since + * Android 8 and became CTS-tested public API in Android 14. Developers aiming for maximum + * compatibility should avoid calling {@literal @}{@code FastNative} methods on Android 13-. + * The optimization is likely to work also on Android 8-13 devices (after all, it was used + * in the system, albeit without the strong CTS guarantees), especially those that use + * unmodified versions of ART, such as Android 12+ devices with the official ART Module. + * The built-in dynamic JNI linking is working only in Android 12+, the explicit registration + * with JNI {@code RegisterNatives} is strictly required for running on Android versions 8-11. + * The annotation is ignored on Android 7-. + *

+ * + *

+ * Deadlock Warning: As a rule of thumb, any native locks acquired in a + * {@literal @}{@link FastNative} call (despite the above warning that this is an unbounded + * operation that can block GC for a long time) must be released before returning to managed code. + *

+ * + *

+ * Say some code does: + * + * + * fast_jni_call_to_grab_a_lock(); + * does_some_java_work(); + * fast_jni_call_to_release_a_lock(); + * + * + *

+ * This code can lead to deadlocks. Say thread 1 just finishes + * {@code fast_jni_call_to_grab_a_lock()} and is in {@code does_some_java_work()}. + * GC kicks in and suspends thread 1. Thread 2 now is in {@code fast_jni_call_to_grab_a_lock()} + * but is blocked on grabbing the native lock since it's held by thread 1. + * Now thread suspension can't finish since thread 2 can't be suspended since it's doing + * FastNative JNI. + *

+ * + *

+ * Normal JNI doesn't have the issue since once it's in native code, + * it is considered suspended from java's point of view. + * FastNative JNI however doesn't do the state transition done by JNI. + *

+ * + *

+ * Note that even in FastNative methods you are allowed to + * allocate objects and make upcalls into Java code. A call from Java to + * a FastNative function and back to Java is equivalent to a call from one Java + * method to another. What's forbidden in a FastNative method is blocking + * the calling thread in some non-Java code and thereby preventing the thread + * from responding to requests from the garbage collector to enter the suspended + * state. + *

+ * + *

+ * Has no effect when used with non-native methods. + *

+ */ +@Retention(RetentionPolicy.CLASS) // Save memory, don't instantiate as an object at runtime. +@Target(ElementType.METHOD) +public @interface FastNative { } \ No newline at end of file diff --git a/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-json.so b/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-json.so new file mode 100644 index 0000000..5a23ed3 Binary files /dev/null and b/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-json.so differ diff --git a/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-lua.so b/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-lua.so new file mode 100644 index 0000000..f86b2f5 Binary files /dev/null and b/tree-sitter/src/main/jniLibs/arm64-v8a/libtree-sitter-lua.so differ diff --git a/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-json.so b/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-json.so new file mode 100644 index 0000000..832a2e7 Binary files /dev/null and b/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-json.so differ diff --git a/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-lua.so b/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-lua.so new file mode 100644 index 0000000..159b2a1 Binary files /dev/null and b/tree-sitter/src/main/jniLibs/armeabi-v7a/libtree-sitter-lua.so differ