diff --git a/.idea/misc.xml b/.idea/misc.xml
index 06b3731ae..ceb55db53 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -49,6 +49,7 @@
+
diff --git a/app/src/main/assets/data/common/gradle-plugin.zip b/app/src/main/assets/data/common/gradle-plugin.zip
index 2d3e7a159..d266fa744 100644
Binary files a/app/src/main/assets/data/common/gradle-plugin.zip and b/app/src/main/assets/data/common/gradle-plugin.zip differ
diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java
new file mode 100644
index 000000000..2a204705d
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.adapters;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.itsaky.androidide.databinding.LayoutTextActionItemBinding;
+import com.itsaky.androidide.views.editor.IDEEditor;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Adapter for text actions in editor.
+ * @author Akash Yadav
+ */
+public class TextActionItemAdapter extends RecyclerView.Adapter {
+
+ private final List actions;
+ private final Consumer onClick;
+
+ public TextActionItemAdapter (List actions, Consumer onClick) {
+ Objects.requireNonNull (actions);
+ this.actions = actions;
+ this.onClick = onClick;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder (@NonNull ViewGroup parent, int viewType) {
+ return new VH (LayoutTextActionItemBinding.inflate (LayoutInflater.from (parent.getContext ()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder (@NonNull VH holder, int position) {
+ final var binding = holder.binding;
+ final var action = actions.get (position);
+ final var button = binding.getRoot ();
+
+ button.setText (action.titleId);
+ button.setCompoundDrawablesRelative (action.icon, null, null, null);
+
+ button.setOnClickListener (v -> {
+ if (onClick != null) {
+ onClick.accept (action);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount () {
+ return actions.size ();
+ }
+
+ static class VH extends RecyclerView.ViewHolder {
+
+ private final LayoutTextActionItemBinding binding;
+
+ public VH (@NonNull LayoutTextActionItemBinding binding) {
+ super (binding.getRoot ());
+ this.binding = binding;
+ }
+ }
+}
diff --git a/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java b/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java
index 84365da4b..eff61452c 100755
--- a/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java
+++ b/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java
@@ -73,7 +73,7 @@ public class SchemeAndroidIDE extends EditorColorScheme {
setColor (WHOLE_BACKGROUND, 0xff212121);
setColor (LINE_NUMBER_BACKGROUND, 0xff212121);
setColor (LINE_DIVIDER, Color.TRANSPARENT);
- setColor (LINE_NUMBER, 0xffaaaaaa);
+ setColor (LINE_NUMBER, 0xffaaaaaa );
setColor (LINE_NUMBER_PANEL, 0xff000000);
setColor (LINE_NUMBER_PANEL_TEXT, 0xffffffff);
setColor (TEXT_NORMAL, 0xfff5f5f5);
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java
new file mode 100644
index 000000000..a2d297a1a
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java
@@ -0,0 +1,230 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.views.editor;
+
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.itsaky.androidide.R;
+import com.itsaky.androidide.adapters.TextActionItemAdapter;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import io.github.rosemoe.sora.event.HandleStateChangeEvent;
+import io.github.rosemoe.sora.event.ScrollEvent;
+import io.github.rosemoe.sora.event.SelectionChangeEvent;
+import io.github.rosemoe.sora.widget.CodeEditor;
+import io.github.rosemoe.sora.widget.EditorTouchEventHandler;
+import io.github.rosemoe.sora.widget.base.EditorPopupWindow;
+
+/**
+ * Presents text actions in a popup widow.
+ *
+ * @author Akash Yadav
+ * @see io.github.rosemoe.sora.widget.component.EditorTextActionWindow
+ */
+public class EditorTextActionWindow extends EditorPopupWindow implements IDEEditor.ITextActionPresenter {
+
+ private IDEEditor editor;
+ private RecyclerView actionsList;
+ private EditorTouchEventHandler touchHandler;
+
+ private final static long DELAY = 200;
+ private long mLastScroll;
+ private int mLastPosition;
+
+ private final Set registeredActions = new TreeSet<> ();
+
+ /**
+ * Create a popup window for editor
+ *
+ * @param editor The editor
+ * @see #FEATURE_SCROLL_AS_CONTENT
+ * @see #FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED
+ * @see #FEATURE_HIDE_WHEN_FAST_SCROLL
+ */
+ public EditorTextActionWindow (@NonNull CodeEditor editor) {
+ super (editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED);
+ }
+
+ @Override
+ public void bindEditor (@NonNull IDEEditor editor) {
+ Objects.requireNonNull (editor, "Cannot bind with null editor");
+
+ this.editor = editor;
+ this.touchHandler = editor.getEventHandler ();
+ this.actionsList = new RecyclerView (editor.getContext ());
+ this.actionsList.setLayoutParams (new ViewGroup.LayoutParams (ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ this.actionsList.setLayoutManager (new LinearLayoutManager (editor.getContext ()));
+ this.actionsList.setBackground (createBackground ());
+ this.actionsList.setVerticalFadingEdgeEnabled (true);
+ this.actionsList.setFadingEdgeLength ((int) (10 * editor.getDpUnit ()));
+ this.actionsList.setClipToOutline (true); // prevent items from being drawn outside window
+ this.editor.getComponent (io.github.rosemoe.sora.widget.component.EditorTextActionWindow.class).setEnabled (false);
+
+ setContentView (this.actionsList);
+ subscribeToEvents ();
+
+ this.registeredActions.clear ();
+ }
+
+ @NonNull
+ private Drawable createBackground () {
+ final var drawable = new GradientDrawable ();
+ drawable.setShape (GradientDrawable.RECTANGLE);
+ drawable.setColor (ContextCompat.getColor (editor.getContext (), R.color.primaryLightColor));
+ drawable.setCornerRadius (8 * editor.getDpUnit ());
+ return drawable;
+ }
+
+ @Override
+ public void registerAction (@NonNull IDEEditor.TextAction action) {
+ Objects.requireNonNull (editor, "No editor attached!");
+
+ this.registeredActions.add (action);
+ }
+
+ @Override
+ public void show () {
+ Objects.requireNonNull (editor, "No editor attached!");
+
+ final var actions = this.registeredActions.stream ()
+ .filter (action -> this.editor.shouldShowTextAction (action.id))
+ .collect (Collectors.toList ());
+ this.actionsList.setAdapter (new TextActionItemAdapter (actions, this::performTextAction));
+ this.actionsList.measure (
+ View.MeasureSpec.makeMeasureSpec (
+ editor.getWidth (),
+ View.MeasureSpec.AT_MOST
+ ),
+ View.MeasureSpec.makeMeasureSpec (
+ (int) (260 * editor.getDpUnit ()), // 260dp at most
+ View.MeasureSpec.AT_MOST
+ )
+ );
+ setSize (this.actionsList.getMeasuredWidth (), this.actionsList.getMeasuredHeight ());
+
+ super.show ();
+ }
+
+ private void subscribeToEvents () {
+ this.editor.subscribeEvent (SelectionChangeEvent.class, (event, unsubscribe) -> {
+ if (touchHandler.hasAnyHeldHandle ()) {
+ return;
+ }
+ if (event.isSelected ()) {
+ if (!isShowing ()) {
+ this.editor.post (this::displayWindow);
+ }
+ mLastPosition = -1;
+ } else {
+ var show = false;
+ if (event.getCause () == SelectionChangeEvent.CAUSE_TAP
+ && event.getLeft ().index == mLastPosition
+ && !isShowing () && !this.editor.getText ().isInBatchEdit ()) {
+ this.editor.post (this::displayWindow);
+ show = true;
+ } else {
+ dismiss ();
+ }
+ if (event.getCause () == SelectionChangeEvent.CAUSE_TAP && !show) {
+ mLastPosition = event.getLeft ().index;
+ } else {
+ mLastPosition = -1;
+ }
+ }
+ });
+ this.editor.subscribeEvent (ScrollEvent.class, ((event, unsubscribe) -> {
+ var last = mLastScroll;
+ mLastScroll = System.currentTimeMillis ();
+ if (mLastScroll - last < DELAY) {
+ postDisplay ();
+ }
+ }));
+ this.editor.subscribeEvent (HandleStateChangeEvent.class, ((event, unsubscribe) -> {
+ if (event.isHeld ()) {
+ postDisplay ();
+ }
+ }));
+ }
+
+ private void performTextAction (@NonNull IDEEditor.TextAction action) {
+ this.editor.performTextAction (action);
+ dismiss ();
+ }
+
+ private int selectTop (@NonNull RectF rect) {
+ var rowHeight = editor.getRowHeight ();
+ if (rect.top - rowHeight * 3 / 2F > getHeight ()) {
+ return (int) (rect.top - rowHeight * 3 / 2 - getHeight ());
+ } else {
+ return (int) (rect.bottom + rowHeight / 2);
+ }
+ }
+
+ public void displayWindow () {
+ int top;
+ var cursor = this.editor.getCursor ();
+ if (cursor.isSelected ()) {
+ var leftRect = this.editor.getLeftHandleDescriptor ().position;
+ var rightRect = this.editor.getRightHandleDescriptor ().position;
+ var top1 = selectTop (leftRect);
+ var top2 = selectTop (rightRect);
+ top = Math.min (top1, top2);
+ } else {
+ top = selectTop (this.editor.getInsertHandleDescriptor ().position);
+ }
+ top = Math.max (0, Math.min (top, this.editor.getHeight () - getHeight () - 5));
+ float handleLeftX = this.editor.getOffset (this.editor.getCursor ().getLeftLine (), this.editor.getCursor ().getLeftColumn ());
+ float handleRightX = this.editor.getOffset (this.editor.getCursor ().getRightLine (), this.editor.getCursor ().getRightColumn ());
+ int panelX = (int) ((handleLeftX + handleRightX) / 2f);
+ setLocationAbsolutely (panelX, top);
+ show ();
+ }
+
+ private void postDisplay () {
+ if (!isShowing ()) {
+ return;
+ }
+ dismiss ();
+ if (!this.editor.getCursor ().isSelected ()) {
+ return;
+ }
+ this.editor.postDelayed (new Runnable () {
+ @Override
+ public void run () {
+ if (!touchHandler.hasAnyHeldHandle () && System.currentTimeMillis () - mLastScroll > DELAY
+ && touchHandler.getScroller ().isFinished ()) {
+ displayWindow ();
+ } else {
+ editor.postDelayed (this, DELAY);
+ }
+ }
+ }, DELAY);
+ }
+}
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
index e40d666d2..40aeb0612 100644
--- a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java
@@ -77,7 +77,6 @@ public class IDEEditor extends CodeEditor {
private SignatureHelpWindow mSignatureHelpWindow;
private DiagnosticWindow mDiagnosticWindow;
- @SuppressWarnings("FieldCanBeLocal,unused")
private ITextActionPresenter mTextActionPresenter;
public static final String KEY_FILE = "editor_file";
@@ -99,7 +98,7 @@ public class IDEEditor extends CodeEditor {
super (context, attrs, defStyleAttr, defStyleRes);
setColorScheme (new SchemeAndroidIDE ());
- setTextActionPresenter (new EditorTextActionMode ());
+ setTextActionPresenter (new EditorTextActionWindow (this));
subscribeEvent (SelectionChangeEvent.class, (event, unsubscribe) -> handleSelectionChange (event));
subscribeEvent (ContentChangeEvent.class, (event, unsubscribe) -> handleContentChange (event));
diff --git a/app/src/main/res/layout/layout_text_action_item.xml b/app/src/main/res/layout/layout_text_action_item.xml
new file mode 100644
index 000000000..f84454d18
--- /dev/null
+++ b/app/src/main/res/layout/layout_text_action_item.xml
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file