New feature: Expand selection

Expand the current selection in editor using the 'Expand selection' option
This commit is contained in:
Akash Yadav
2022-01-25 20:31:54 +05:30
parent 9fb75d302d
commit a9b19fa463
16 changed files with 406 additions and 87 deletions
+1
View File
@@ -3,6 +3,7 @@
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable-hdpi/ic_expand_selection.xml" value="0.20833333333333334" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/bg_design_container_background.xml" value="0.1" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/bg_ui_view_pressed.xml" value="0.1" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_github.xml" value="0.21837606837606838" />
Binary file not shown.
@@ -81,6 +81,7 @@ import com.itsaky.lsp.models.DocumentChangeEvent;
import com.itsaky.lsp.models.DocumentCloseEvent;
import com.itsaky.lsp.models.DocumentOpenEvent;
import com.itsaky.lsp.models.DocumentSaveEvent;
import com.itsaky.lsp.models.ExpandSelectionParams;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.models.ReferenceParams;
@@ -3528,6 +3529,17 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
setSelectionRegion (0, 0, getLineCount () - 1, getText ().getColumnCount (getLineCount () - 1));
}
/**
* Set selection to the given range.
* @param range The range to select.
*/
public void setSelection (Range range) {
setSelectionRegion (range.getStart ().getLine (),
range.getStart ().getColumn (),
range.getEnd ().getLine (),
range.getEnd ().getColumn ());
}
/**
* Set selection region with a call to {@link CodeEditor#ensureSelectionVisible()}
*
@@ -4634,6 +4646,36 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
)));
}
public void expandSelection () {
if (mLanguageServer == null || getFile () == null) {
LOG.error ("Cannot expand selection. Language server or file is null");
return;
}
final var pd = ProgressDialog.show (getContext (), null, getContext ().getString (R.string.please_wait), true, false);
final CompletableFuture<Range> future = CompletableFuture.supplyAsync (() ->
mLanguageServer.getSelectionProvider ()
.expandSelection (
new ExpandSelectionParams (
getFile ().toPath (),
getCursorRange ()
)
)
);
future.whenComplete (((range, throwable) -> {
pd.dismiss ();
if (throwable != null) {
LOG.error ("Error computing expanded selection range", throwable);
return;
}
setSelection (range);
}));
}
public Position getCursorAsLSPPosition () {
return new Position (getCursor ().getLeftLine (), getCursor ().getLeftColumn ());
}
@@ -36,6 +36,7 @@ import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.SizeUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.lsp.models.CodeActionItem;
@@ -84,23 +85,27 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
android.R.attr.actionModePasteDrawable,
});
menu.add (0, 8, 0, R.string.action_expand_selection)
.setShowAsActionFlags (1)
.setIcon (createDrawable (R.drawable.ic_expand_selection));
menu.add(0, 0, 0, mEditor.getContext().getString(android.R.string.selectAll))
.setShowAsActionFlags(2)
.setShowAsActionFlags(1)
.setIcon(array.getDrawable(0));
if (mEditor.isEditable()) {
menu.add(0, 1, 0, mEditor.getContext().getString(android.R.string.cut))
.setShowAsActionFlags(2)
.setShowAsActionFlags(1)
.setIcon(array.getDrawable(1));
}
menu.add(0, 2, 0, mEditor.getContext().getString(android.R.string.copy))
.setShowAsActionFlags(2)
.setShowAsActionFlags(1)
.setIcon(array.getDrawable(2));
if (mEditor.isEditable()) {
menu.add(0, 3, 0, mEditor.getContext().getString(android.R.string.paste))
.setShowAsActionFlags(2)
.setShowAsActionFlags(1)
.setIcon(array.getDrawable(3));
}
@@ -191,18 +196,16 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
mEditor.uncommentLine();
break;
case 8 :
if (fixes == null || fixes.isEmpty()) {
StudioApp.getInstance().toast(com.itsaky.androidide.R.string.msg_no_code_actions, Toaster.Type.INFO);
break;
}
showFixes();
mEditor.expandSelection ();
break;
}
if (menuItem.getItemId() != 0) {
onExit();
final var id = menuItem.getItemId ();
if (id == 0 || id == 8) {
return true;
}
return false;
return onExit ();
}
@Override
@@ -211,77 +214,6 @@ class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresente
mActionMode = null;
mEditor.setSelection(mEditor.getCursor().getLeftLine(), mEditor.getCursor().getLeftColumn());
}
void showFixes() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (mEditor.getContext ());
builder.setTitle(com.itsaky.androidide.R.string.msg_code_actions);
builder.setView(createFixesListView());
builder.show();
}
ListView createFixesListView() {
final ListView list = new ListView(mEditor.getContext());
final QuickFixAdapter adapter = new QuickFixAdapter(fixes);
list.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
list.setAdapter(adapter);
list.setOnItemClickListener((a, b, c, d) -> performCodeAction(adapter.getItem(c)));
return list;
}
class QuickFixAdapter extends BaseAdapter {
private final List<CodeActionItem> actions;
private final int dp16 = SizeUtils.dp2px(16);
private final int dp4 = SizeUtils.dp2px(4);
private final int dp48 = SizeUtils.dp2px(48);
public QuickFixAdapter(List<CodeActionItem> actions) {
this.actions = actions;
}
@Override
public int getCount() {
return actions.size();
}
@Override
public CodeActionItem getItem(int position) {
return actions.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = createItemTextView(position, parent);
}
return convertView;
}
private View createItemTextView(int position, ViewGroup parent) {
final var action = getItem(position);
final TextView text = new TextView(parent.getContext());
final String title = action.getTitle();
text.setText(title);
text.setPaddingRelative(dp16, dp4, dp16, dp4);
text.setMinHeight(dp48);
text.setGravity(Gravity.CENTER_VERTICAL);
return text;
}
}
void performCodeAction(CodeActionItem action) {
if (mEditor.getLanguageClient() != null) {
mEditor.getLanguageClient().performCodeAction(mEditor, action);
}
}
});
}
@@ -0,0 +1,8 @@
<!-- drawable/arrow_expand_all.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#ffffff" android:pathData="M9.5,13.09L10.91,14.5L6.41,19H10V21H3V14H5V17.59L9.5,13.09M10.91,9.5L9.5,10.91L5,6.41V10H3V3H10V5H6.41L10.91,9.5M14.5,13.09L19,17.59V14H21V21H14V19H17.59L13.09,14.5L14.5,13.09M13.09,9.5L17.59,5H14V3H21V10H19V6.41L14.5,10.91L13.09,9.5Z" />
</vector>
+3 -2
View File
@@ -156,7 +156,7 @@
<string name="template_navigation_drawer">Navigation drawer project</string>
<string name="template_kotlin_basic">Kotlin Basic Project</string>
<string name="template_description_kotlin_basic">Kotlin project with Appcompat Toolbar and a Floating Action Button</string>
<!-- Files -->
<string name="save">Save</string>
<string name="show_files">Files</string>
@@ -354,5 +354,6 @@
<string name="msg_compiler_module_install">Compiler module is being installed, please wait&#8230;</string>
<string name="msg_compiler_module_install_failed">Compiler module installation failed. The error was:\n%s</string>
<string name="msg_compiler_module_installed">Compiler module installed successfully!</string>
<string name="action_expand_selection">Expand selection</string>
</resources>
@@ -131,6 +131,14 @@ public interface ILanguageServer {
@NonNull
IDefinitionProvider getDefinitionProvider();
/**
* Get the selection provider associated with this language server.
*
* @return The selection provider instance.
*/
@NonNull
ISelectionProvider getSelectionProvider ();
/**
* Get the signature help provider associated with this language server.
* @return The signature help provider.
@@ -0,0 +1,44 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.api;
import androidx.annotation.NonNull;
import com.itsaky.lsp.models.ExpandSelectionParams;
import com.itsaky.lsp.models.Range;
/**
* A selection provider provides features such as
* expanding the current selection.
*
* @author Akash Yadav
*/
public interface ISelectionProvider {
/**
* Request sent by client for expanding the current selection.
* <p>
* Example: If a statement in a method body is selected,
* the provider returns the range of the method body as the expanded selection.
* </p>
*
* @param params The params for expanding the selection.
* @return The expanded selection range. Or same range as params if cannot be expanded.
*/
Range expandSelection (@NonNull ExpandSelectionParams params);
}
@@ -41,6 +41,14 @@ public interface IServerSettings {
*/
boolean codeActionsEnabled ();
/**
* Called by language server to check if
* smart selections are enabled or not.
*
* @return {@code true} if enabled, {@code false} otherwise.
*/
boolean smartSelectionsEnabled ();
/**
* Called by the language server to check if the signature
* help is enabled.
@@ -142,7 +142,44 @@ open class Range(var start: Position, var end: Position) : Comparable<Range> {
return start.line <= line && end.line >= line
}
fun containsColumn (column: Int): Boolean {
return start.column <= column && end.column >= column
}
fun containsRange (other: Range) : Boolean {
if (!containsLine(other.start.line) || !containsLine(other.end.line)) {
return false
}
return containsColumn(other.start.column) && containsColumn(other.end.column);
}
fun isSmallerThan (other: Range) : Boolean {
return other.isBiggerThan(this)
}
fun isBiggerThan (other: Range) : Boolean {
if (equals(other)) {
return false
}
if (start.line < other.start.line && end.line > other.end.line) {
return true
}
if (start.line == other.start.line && end.line == other.end.line) {
if (start.column <= other.start.column && end.column >= other.end.column) {
return true
}
}
return false
}
override fun toString(): String {
return "Range(start=$start, end=$end)"
}
}
}
data class ExpandSelectionParams (var file: Path, var selection: Range)
@@ -27,6 +27,7 @@ import com.itsaky.lsp.api.IServerSettings
class DefaultServerSettings : IServerSettings {
override fun completionsEnabled(): Boolean = true
override fun codeActionsEnabled(): Boolean = true
override fun smartSelectionsEnabled(): Boolean = true
override fun signatureHelpEnabled(): Boolean = true
override fun referencesEnabled(): Boolean = true
override fun definitionsEnabled(): Boolean = true
@@ -0,0 +1,29 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.util
import com.itsaky.lsp.api.ISelectionProvider
import com.itsaky.lsp.models.ExpandSelectionParams
import com.itsaky.lsp.models.Range
/**
* @author Akash Yadav
*/
class NoSelectionProvider : ISelectionProvider {
override fun expandSelection(params: ExpandSelectionParams): Range = params.selection;
}
@@ -29,6 +29,7 @@ import com.itsaky.lsp.api.IDocumentHandler;
import com.itsaky.lsp.api.ILanguageClient;
import com.itsaky.lsp.api.ILanguageServer;
import com.itsaky.lsp.api.IReferenceProvider;
import com.itsaky.lsp.api.ISelectionProvider;
import com.itsaky.lsp.api.IServerSettings;
import com.itsaky.lsp.api.ISignatureHelpProvider;
import com.itsaky.lsp.java.compiler.JavaCompilerService;
@@ -37,6 +38,7 @@ import com.itsaky.lsp.java.providers.CodeActionProvider;
import com.itsaky.lsp.java.providers.CompletionProvider;
import com.itsaky.lsp.java.providers.DefinitionProvider;
import com.itsaky.lsp.java.providers.JavaCodeAnalyzer;
import com.itsaky.lsp.java.providers.JavaSelectionProvider;
import com.itsaky.lsp.java.providers.ReferenceProvider;
import com.itsaky.lsp.java.providers.SignatureProvider;
import com.itsaky.lsp.models.DocumentChangeEvent;
@@ -51,6 +53,7 @@ import com.itsaky.lsp.util.NoCodeAnalyzer;
import com.itsaky.lsp.util.NoCompletionsProvider;
import com.itsaky.lsp.util.NoDefinitionProvider;
import com.itsaky.lsp.util.NoReferenceProvider;
import com.itsaky.lsp.util.NoSelectionProvider;
import com.itsaky.lsp.util.NoSignatureHelpProvider;
import java.util.Collections;
@@ -208,6 +211,16 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
return new DefinitionProvider (getCompiler ());
}
@NonNull
@Override
public ISelectionProvider getSelectionProvider () {
if (!settings.smartSelectionsEnabled ()) {
return new NoSelectionProvider ();
}
return new JavaSelectionProvider (getCompiler ());
}
@NonNull
@Override
public ISignatureHelpProvider getSignatureHelpProvider () {
@@ -0,0 +1,67 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.java.providers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.api.ISelectionProvider;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.java.visitors.FindBiggerRange;
import com.itsaky.lsp.models.ExpandSelectionParams;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.Tree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
/**
* Selection provider implementation for Java.
* @author Akash Yadav
*/
public class JavaSelectionProvider implements ISelectionProvider {
private final CompilerProvider compiler;
private static final Logger LOG = Logger.instance ("JavaSelectionProvider");
public JavaSelectionProvider (CompilerProvider compiler) {
this.compiler = compiler;
}
@Override
public Range expandSelection (@NonNull ExpandSelectionParams params) {
return compiler.compile (params.getFile ()).getWithTask (task -> {
final CompilationUnitTree root = task.root (params.getFile ());
final FindBiggerRange rangeFinder = new FindBiggerRange (task.task, root);
final Range range = rangeFinder.scan (root, params.getSelection ());
if (range != null) {
LOG.debug ("Expanding selection to range", range);
return range;
}
LOG.debug ("Unable to expand selection");
return params.getSelection ();
});
}
}
@@ -0,0 +1,127 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.java.visitors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
/**
* @author Akash Yadav
*/
public class FindBiggerRange extends TreeScanner<Range, Range> {
private final SourcePositions positions;
private final CompilationUnitTree root;
private final LineMap lineMap;
public FindBiggerRange (JavacTask task, @NonNull CompilationUnitTree root) {
this.positions = Trees.instance (task).getSourcePositions ();
this.root = root;
this.lineMap = root.getLineMap ();
}
@Override
public Range scan (Tree tree, Range range) {
final Range smallerThanThis = super.scan (tree, range);
if (smallerThanThis != null) {
return smallerThanThis;
}
final Range treeRange = getRange (tree);
if (treeRange != null && range.isSmallerThan (treeRange)) {
Logger.instance ("FindBiggerRange").debug ("Selecting tree", tree, tree.getClass (), tree.getKind ());
return treeRange;
}
return null;
}
@Override
public Range visitTry (TryTree node, Range range) {
// If this try's body or finally block is selected, then select the entire try
final Range methodRange = getRange (node);
final Range blockRange = getRange (node.getBlock ());
final Range finallyRange = getRange (node.getFinallyBlock ());
if ((range.equals (blockRange) || range.equals (finallyRange)) && methodRange != null) {
return methodRange;
}
return super.visitTry (node, range);
}
@Override
public Range visitMethod (MethodTree node, Range range) {
// If this methods body is selected, then select the entire method
final Range methodRange = getRange (node);
final Range blockRange = getRange (node.getBody ());
if (range.equals (blockRange) && methodRange != null) {
return methodRange;
}
return super.visitMethod (node, range);
}
@Override
public Range reduce (Range r1, Range r2) {
return r1 == null ? r2 : r1;
}
@Nullable
private Range getRange (Tree leaf) {
final Range range = new Range ();
final Position start = new Position (0,0);
final Position end = new Position (0,0);
final long startPos = positions.getStartPosition (root, leaf);
final long endPos = positions.getEndPosition (root, leaf);
if (startPos == -1 || endPos == -1) {
return null;
}
start.setLine ((int) lineMap.getLineNumber (startPos) - 1);
start.setColumn ((int) lineMap.getColumnNumber (startPos) - 1);
end.setLine ((int) lineMap.getLineNumber (endPos) - 1);
end.setColumn ((int) lineMap.getColumnNumber (endPos) - 1);
range.setStart (start);
range.setEnd (end);
return range;
}
}
@@ -25,6 +25,7 @@ import com.sun.source.util.*;
import javax.lang.model.element.Name;
public class FindNameAt extends TreePathScanner<TreePath, Long> {
private final JavacTask task;
private CompilationUnitTree root;
private ClassTree surroundingClass;