Extracted interface for IDEEditor

This commit is contained in:
Akash Yadav
2022-09-20 19:24:48 +05:30
parent 6250f28f0e
commit 900365c353
17 changed files with 666 additions and 367 deletions
+1
View File
@@ -133,6 +133,7 @@ dependencies {
// Local projects here
implementation project (':build-tools:aaptcompiler')
implementation project (':build-tools:xml-utils')
implementation project (':editor')
implementation project (':eventbus-android')
implementation project (':eventbus-events')
implementation project (':build-tools:projects')
Binary file not shown.
@@ -617,7 +617,7 @@ public class EditorActivity extends IDEActivity
}
if (editor != null && editor.getEditor() != null) {
editor.getEditor().close();
editor.getEditor().notifyClose();
editor.getEditor().release();
} else {
LOG.error("Cannot save file before close. Editor instance is null");
@@ -657,7 +657,7 @@ public class EditorActivity extends IDEActivity
for (int i = 0; i < count; i++) {
final var editor = getEditorAtIndex(i);
if (editor != null && editor.getEditor() != null) {
editor.getEditor().close();
editor.getEditor().notifyClose();
} else {
LOG.error("Unable to close file at index:", i);
}
@@ -58,7 +58,6 @@ import com.itsaky.androidide.language.groovy.GroovyLanguage;
import com.itsaky.androidide.language.java.JavaLanguage;
import com.itsaky.androidide.language.kotlin.KotlinLanguage;
import com.itsaky.androidide.language.xml.XMLLanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry;
import com.itsaky.androidide.lsp.java.JavaLanguageServer;
@@ -72,20 +71,15 @@ import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.inflater.values.ValuesTableFactory;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.lang.EmptyLanguage;
import io.github.rosemoe.sora.lang.Language;
import io.github.rosemoe.sora.text.LineSeparator;
@@ -102,13 +96,11 @@ public class CodeEditorView extends FrameLayout {
private static final ILogger LOG = ILogger.newInstance("CodeEditorView");
private final File file;
private final LayoutCodeEditorBinding binding;
private boolean isModified;
public CodeEditorView(
@NonNull Context context, @NonNull File file, final @NonNull Range selection) {
super(context);
this.file = file;
this.isModified = false;
this.binding = LayoutCodeEditorBinding.inflate(LayoutInflater.from(context));
this.binding.editor.setTypefaceText(TypefaceUtils.jetbrainsMono());
@@ -117,8 +109,6 @@ public class CodeEditorView extends FrameLayout {
this.binding.editor.setDividerWidth(SizeUtils.dp2px(1));
this.binding.editor.setColorScheme(new SchemeAndroidIDE());
this.binding.editor.setLineSeparator(LineSeparator.LF);
this.binding.editor.subscribeEvent(
ContentChangeEvent.class, ((event, unsubscribe) -> handleContentChange(event)));
this.binding.diagnosticTextContainer.setVisibility(GONE);
@@ -162,73 +152,6 @@ public class CodeEditorView extends FrameLayout {
return file;
}
private void handleContentChange(@NonNull ContentChangeEvent event) {
isModified = true;
if (event.getAction() == ContentChangeEvent.ACTION_INSERT) {
final var editor = event.getEditor();
final var content = event.getChangedText();
final var endLine = event.getChangeEnd().line;
final var endColumn = event.getChangeEnd().column;
if (file.getName().endsWith(".xml")) {
boolean isOpen = false;
try {
isOpen = editor.getText().charAt(editor.getCursor().getLeft() - 2) == '<';
} catch (Throwable th) {
LOG.error(th);
}
if (isOpen && "/".contentEquals(content)) {
closeCurrentTag(editor.getText().toString(), endLine, endColumn);
}
}
}
}
private void closeCurrentTag(String text, int line, int col) {
try {
XMLLexer lexer = new XMLLexer(CharStreams.fromReader(new StringReader(text)));
Token token;
boolean wasSlash = false, wasOpen = false;
ArrayList<String> currentNames = new ArrayList<>();
while (((token = lexer.nextToken()) != null && token.getType() != token.EOF)) {
final int type = token.getType();
if (type == XMLLexer.OPEN) {
wasOpen = true;
} else if (type == XMLLexer.Name) {
if (wasOpen && wasSlash && currentNames.size() > 0) {
currentNames.remove(0);
} else if (wasOpen) {
currentNames.add(0, token.getText());
wasOpen = false;
}
} else if (type == XMLLexer.OPEN_SLASH) {
int l = token.getLine() - 1;
int c = token.getCharPositionInLine();
if (l == line && c == col) {
break;
} else if (currentNames.size() > 0) {
currentNames.remove(0);
}
} else if (type == XMLLexer.SLASH_CLOSE || type == XMLLexer.SPECIAL_CLOSE) {
if (currentNames.size() > 0 && token.getText().trim().endsWith("/>")) {
currentNames.remove(0);
}
} else if (type == XMLLexer.SLASH) {
wasSlash = true;
} else {
wasOpen = wasSlash = false;
}
}
if (currentNames.size() > 0) {
binding.editor.getText().insert(line, col + 2, currentNames.get(0));
}
} catch (Throwable th) {
LOG.error("Unable to close current tag", th);
}
}
public IDEEditor getEditor() {
return binding.editor;
}
@@ -363,7 +286,7 @@ public class CodeEditorView extends FrameLayout {
* <p>Marks this editor as unmodified. Used only when the activity is being destroyed.
*/
public void markUnmodified() {
isModified = false;
binding.editor.markUnmodified();
}
@Subscribe(threadMode = ThreadMode.MAIN)
@@ -421,7 +344,6 @@ public class CodeEditorView extends FrameLayout {
try {
FileUtil.writeFile(file, text);
notifySaved();
isModified = false;
return true;
} catch (IOException io) {
LOG.error("Failed to save file", file, io);
@@ -430,7 +352,7 @@ public class CodeEditorView extends FrameLayout {
}
public boolean isModified() {
return isModified;
return binding.editor.isModified();
}
private void notifySaved() {
@@ -473,7 +395,6 @@ public class CodeEditorView extends FrameLayout {
/** Mark this files as saved. Even if it not saved. */
public void markAsSaved() {
isModified = false;
notifySaved();
}
@@ -16,7 +16,7 @@
*/
package com.itsaky.androidide.views.editor;
import static com.itsaky.androidide.R.*;
import static com.itsaky.androidide.R.string;
import static com.itsaky.androidide.models.prefs.EditorPreferencesKt.getVisiblePasswordFlag;
import android.app.Dialog;
@@ -29,7 +29,6 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.CompletionListAdapter;
import com.itsaky.androidide.eventbus.events.editor.ChangeType;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
@@ -69,7 +68,7 @@ import io.github.rosemoe.sora.widget.IDEEditorSearcher;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow;
public class IDEEditor extends CodeEditor {
public class IDEEditor extends CodeEditor implements com.itsaky.androidide.editor.IEditor {
public static final String KEY_FILE = "editor_file";
private static final ILogger LOG = ILogger.newInstance("IDEEditor");
@@ -77,6 +76,7 @@ public class IDEEditor extends CodeEditor {
private IDEEditorSearcher searcher;
private int fileVersion;
private File file;
private boolean isModified;
private ILanguageServer languageServer;
private SignatureHelpWindow signatureHelpWindow;
private DiagnosticWindow diagnosticWindow;
@@ -97,8 +97,9 @@ public class IDEEditor extends CodeEditor {
public IDEEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
actionsMenu = new EditorActionsMenu(this);
actionsMenu.init();
this.isModified = false;
this.actionsMenu = new EditorActionsMenu(this);
this.actionsMenu.init();
final var window = new EditorCompletionWindow(this);
window.setAdapter(new CompletionListAdapter());
@@ -140,6 +141,7 @@ public class IDEEditor extends CodeEditor {
*
* @return The file instance.
*/
@Override
public File getFile() {
return file;
}
@@ -173,50 +175,16 @@ public class IDEEditor extends CodeEditor {
EventBus.getDefault().post(openEvent);
}
/**
* Notify the language server that the content of this file has been changed.
*
* @param event The content change event.
*/
private void handleContentChange(ContentChangeEvent event, Unsubscribe unsubscribe) {
if (getFile() == null || languageServer == null) {
return;
}
CompletableFuture.runAsync(
() -> {
dispatchDocumentChangeEvent(event);
checkForSignatureHelp(event);
});
}
/**
* Checks if the content change event should trigger signature help. Signature help trigger
* characters are :
*
* <ul>
* <li><code>'('</code> (parentheses)
* <li><code>','</code> (comma)
* </ul>
*
* @param event The content change event.
*/
private void checkForSignatureHelp(@NonNull ContentChangeEvent event) {
if (event.getAction() != ContentChangeEvent.ACTION_INSERT
|| event.getChangedText().length() != 1) {
return;
}
final var ch = event.getChangedText().charAt(0);
if (ch == '(' || ch == ',') {
signatureHelp();
}
@Override
public boolean isModified() {
return isModified;
}
/**
* If any language server is set, requests signature help at the cursor's position. On a valid
* response, shows the signature help in a popup window.
*/
@Override
public void signatureHelp() {
if (languageServer != null && getFile() != null) {
final CompletableFuture<SignatureHelp> future =
@@ -248,6 +216,7 @@ public class IDEEditor extends CodeEditor {
*
* @param help The signature help data to show.
*/
@Override
public void showSignatureHelp(SignatureHelp help) {
getSignatureHelpWindow().setupAndDisplay(help);
}
@@ -260,70 +229,167 @@ public class IDEEditor extends CodeEditor {
return signatureHelpWindow;
}
protected void dispatchDocumentChangeEvent(final ContentChangeEvent event) {
if (getFile() == null) {
return;
}
final var file = getFile().toPath();
var type = ChangeType.INSERT;
if (event.getAction() == ContentChangeEvent.ACTION_DELETE) {
type = ChangeType.DELETE;
} else if (event.getAction() == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
type = ChangeType.NEW_TEXT;
}
var changeDelta = type == ChangeType.NEW_TEXT ? 0 : event.getChangedText().length();
if (type == ChangeType.DELETE) {
changeDelta = -changeDelta;
}
final var start = event.getChangeStart();
final var end = event.getChangeEnd();
final var changeRange =
new Range(
new Position(start.line, start.column, start.index),
new Position(end.line, end.column, end.index));
final var changeEvent =
new DocumentChangeEvent(
file, getText().toString(), fileVersion + 1, type, changeDelta, changeRange);
EventBus.getDefault().post(changeEvent);
}
public static int createInputFlags() {
var flags =
EditorInfo.TYPE_CLASS_TEXT
| EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
| EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
if (getVisiblePasswordFlag()) {
flags |= EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
}
return flags;
}
public void analyze() {
if (languageServer != null && getFile() != null && getEditorLanguage() instanceof IDELanguage) {
CompletableFuture.supplyAsync(() -> languageServer.analyze(getFile().toPath()))
.whenComplete(
(diagnostics, throwable) -> {
if (languageClient != null) {
languageClient.publishDiagnostics(diagnostics);
}
});
}
}
/**
* Set the selection of this editor to the given position.
*
* @param position The position to select.
*/
@Override
public void setSelection(@NonNull Position position) {
setSelection(position.getLine(), position.getColumn());
}
/**
* Set selection to the given range.
*
* @param range The range to select.
*/
@Override
public void setSelection(@NonNull Range range) {
if (isValidRange(range)) {
setSelectionRegion(
range.getStart().getLine(),
range.getStart().getColumn(),
range.getEnd().getLine(),
range.getEnd().getColumn());
}
}
/**
* Get the cursor's selection range in the form of {@link Range}.
*
* @return The {@link Range} of the cursor.
*/
@Override
public Range getCursorLSPRange() {
final var cursor = getCursor();
final var start = new Position(cursor.getLeftLine(), cursor.getLeftColumn());
final var end = new Position(cursor.getRightLine(), cursor.getRightColumn());
return new Range(start, end);
}
/**
* Get the cursor's position in the form of {@link Position}.
*
* @return The {@link Position} of the cursor.
*/
@Override
@SuppressWarnings("unused")
public Position getCursorLSPPosition() {
return new Position(getCursor().getLeftLine(), getCursor().getLeftColumn());
}
/**
* Validates the range if it is invalid and returns a valid range.
*
* @param range Th range to validate.
* @return A new, validated range.
*/
@Override
public Range validateRange(@NonNull final Range range) {
final var start = range.getStart();
final var end = range.getEnd();
if (start.getLine() < 0) {
start.setLine(0);
} else if (start.getLine() >= getText().getLineCount()) {
start.setLine(getText().getLineCount() - 1);
}
if (end.getLine() < 0) {
end.setLine(0);
} else if (end.getLine() >= getText().getLineCount()) {
end.setLine(getText().getLineCount() - 1);
}
if (end.getLine() < start.getLine()) {
var l = end.getLine();
var l2 = start.getLine();
start.setLine(l);
end.setLine(l2);
}
if (start.getColumn() < 0) {
start.setColumn(0);
} else if (start.getColumn() >= getText().getColumnCount(start.getLine())) {
start.setColumn(getText().getColumnCount(start.getLine()) - 1);
}
if (end.getColumn() < 0) {
end.setColumn(0);
} else if (end.getColumn() >= getText().getColumnCount(end.getLine())) {
end.setColumn(getText().getColumnCount(end.getLine()) - 1);
}
if (end.getColumn() < start.getColumn()) {
final var c = start.getColumn();
final var c2 = end.getColumn();
start.setColumn(c2);
end.setColumn(c);
}
return new Range(start, end);
}
/**
* Checks if the given range is valid for this editor's text.
*
* @param range The range to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
@Override
public boolean isValidRange(final Range range) {
if (range == null) {
return false;
}
final var start = range.getStart();
final var end = range.getEnd();
return isValidPosition(start)
&& isValidPosition(end)
&& start.compareTo(end) < 0; // make sure start position is before end position
}
/**
* Checks if the given position is valid for this editor's text.
*
* @param position The position to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
@Override
public boolean isValidPosition(final Position position) {
if (position == null) {
return false;
}
return isValidLine(position.getLine())
&& isValidColumn(position.getLine(), position.getColumn());
}
/**
* Checks if the given line is valid for this editor's text.
*
* @param line The line to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
@Override
public boolean isValidLine(int line) {
return line >= 0 && line < getText().getLineCount();
}
/**
* Checks if the given column is valid for this editor's text.
*
* @param line The line of the column to check.
* @param column The column to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
@Override
public boolean isValidColumn(int line, int column) {
return column >= 0 && column < getText().getColumnCount(line);
}
/**
* Set the language server that this editor will connect with. If the language client is not set,
* it'll be set to {@link IDELanguageClientImpl}.
@@ -331,6 +397,7 @@ public class IDEEditor extends CodeEditor {
* @param server The server to set. Provide <code>null</code> to disable all the language server
* features.
*/
@Override
public void setLanguageServer(ILanguageServer server) {
this.languageServer = server;
@@ -345,6 +412,7 @@ public class IDEEditor extends CodeEditor {
* @param text The text to append.
* @return The line at which the text was appended.
*/
@Override
public int append(CharSequence text) {
final var content = getText();
if (getLineCount() <= 0) {
@@ -360,6 +428,7 @@ public class IDEEditor extends CodeEditor {
}
/** Set the selection of the editor's cursor to the last line of the it's content. */
@Override
public void goToEnd() {
final var line = getText().getLineCount() - 1;
setSelection(line, 0);
@@ -372,6 +441,7 @@ public class IDEEditor extends CodeEditor {
* <p>If the server returns a valid response, and the file specified in the response is same the
* file in this editor, the range specified in the response will be selected.
*/
@Override
@SuppressWarnings({"deprecation", "unused"})
public void findDefinition() {
if (getFile() == null) {
@@ -452,76 +522,6 @@ public class IDEEditor extends CodeEditor {
});
}
/**
* Set selection to the given range.
*
* @param range The range to select.
*/
public void setSelection(@NonNull Range range) {
if (isValidRange(range)) {
setSelectionRegion(
range.getStart().getLine(),
range.getStart().getColumn(),
range.getEnd().getLine(),
range.getEnd().getColumn());
}
}
/**
* Checks if the given range is valid for this editor's text.
*
* @param range The range to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
public boolean isValidRange(final Range range) {
if (range == null) {
return false;
}
final var start = range.getStart();
final var end = range.getEnd();
return isValidPosition(start)
&& isValidPosition(end)
&& start.compareTo(end) < 0; // make sure start position is before end position
}
/**
* Checks if the given position is valid for this editor's text.
*
* @param position The position to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
public boolean isValidPosition(final Position position) {
if (position == null) {
return false;
}
return isValidLine(position.getLine())
&& isValidColumn(position.getLine(), position.getColumn());
}
/**
* Checks if the given line is valid for this editor's text.
*
* @param line The line to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
public boolean isValidLine(int line) {
return line >= 0 && line < getText().getLineCount();
}
/**
* Checks if the given column is valid for this editor's text.
*
* @param line The line of the column to check.
* @param column The column to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
public boolean isValidColumn(int line, int column) {
return column >= 0 && column < getText().getColumnCount(line);
}
/**
* Dismisses the given dialog on the UI thread.
*
@@ -539,6 +539,7 @@ public class IDEEditor extends CodeEditor {
* <p>If the server returns a valid response, that response is forwarded to the {@link
* IDELanguageClientImpl}.
*/
@Override
@SuppressWarnings("unused")
public void findReferences() {
if (getFile() == null) {
@@ -619,20 +620,43 @@ public class IDEEditor extends CodeEditor {
});
}
/** Notify the language server that the file in this editor is about to be closed. */
public void close() {
if (getFile() == null) {
LOG.info("No language server is available for this file");
/**
* Requests the language server to provided a semantically larger selection than the current
* selection. If a valid response is received, that range will be selected.
*/
@Override
public void expandSelection() {
if (languageServer == null || getFile() == null) {
LOG.error("Cannot expand selection. Language server or file is null");
return;
}
dispatchDocumentCloseEvent();
//noinspection deprecation
final var pd =
ProgressDialog.show(
getContext(), null, getContext().getString(string.please_wait), true, false);
final CompletableFuture<Range> future =
CompletableFuture.supplyAsync(
() ->
languageServer.expandSelection(
new ExpandSelectionParams(getFile().toPath(), getCursorLSPRange())));
actionsMenu.unsubscribeEvents();
ensureWindowsDismissed();
future.whenComplete(
((range, throwable) -> {
pd.dismiss();
if (throwable != null) {
LOG.error("Error computing expanded selection range", throwable);
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> setSelection(range));
}));
}
/** Ensures that all the windows are dismissed. */
@Override
public void ensureWindowsDismissed() {
if (getDiagnosticWindow().isShowing()) {
getDiagnosticWindow().dismiss();
@@ -649,6 +673,121 @@ public class IDEEditor extends CodeEditor {
}
}
/**
* Notify the language server that the content of this file has been changed.
*
* @param event The content change event.
*/
private void handleContentChange(ContentChangeEvent event, Unsubscribe unsubscribe) {
if (getFile() == null || languageServer == null) {
return;
}
if (event.getAction() != ContentChangeEvent.ACTION_SET_NEW_TEXT) {
isModified = true;
}
CompletableFuture.runAsync(
() -> {
dispatchDocumentChangeEvent(event);
checkForSignatureHelp(event);
});
}
/**
* Checks if the content change event should trigger signature help. Signature help trigger
* characters are :
*
* <ul>
* <li><code>'('</code> (parentheses)
* <li><code>','</code> (comma)
* </ul>
*
* @param event The content change event.
*/
private void checkForSignatureHelp(@NonNull ContentChangeEvent event) {
if (event.getAction() != ContentChangeEvent.ACTION_INSERT
|| event.getChangedText().length() != 1) {
return;
}
final var ch = event.getChangedText().charAt(0);
if (ch == '(' || ch == ',') {
signatureHelp();
}
}
protected void dispatchDocumentChangeEvent(final ContentChangeEvent event) {
if (getFile() == null) {
return;
}
final var file = getFile().toPath();
var type = ChangeType.INSERT;
if (event.getAction() == ContentChangeEvent.ACTION_DELETE) {
type = ChangeType.DELETE;
} else if (event.getAction() == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
type = ChangeType.NEW_TEXT;
}
var changeDelta = type == ChangeType.NEW_TEXT ? 0 : event.getChangedText().length();
if (type == ChangeType.DELETE) {
changeDelta = -changeDelta;
}
final var start = event.getChangeStart();
final var end = event.getChangeEnd();
final var changeRange =
new Range(
new Position(start.line, start.column, start.index),
new Position(end.line, end.column, end.index));
final var changeEvent =
new DocumentChangeEvent(
file, getText().toString(), fileVersion + 1, type, changeDelta, changeRange);
EventBus.getDefault().post(changeEvent);
}
public static int createInputFlags() {
var flags =
EditorInfo.TYPE_CLASS_TEXT
| EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
| EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
if (getVisiblePasswordFlag()) {
flags |= EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
}
return flags;
}
public void markUnmodified() {
this.isModified = false;
}
public void analyze() {
if (languageServer != null && getFile() != null && getEditorLanguage() instanceof IDELanguage) {
CompletableFuture.supplyAsync(() -> languageServer.analyze(getFile().toPath()))
.whenComplete(
(diagnostics, throwable) -> {
if (languageClient != null) {
languageClient.publishDiagnostics(diagnostics);
}
});
}
}
/** Notify the language server that the file in this editor is about to be closed. */
public void notifyClose() {
if (getFile() == null) {
LOG.info("No language server is available for this file");
return;
}
dispatchDocumentCloseEvent();
actionsMenu.unsubscribeEvents();
ensureWindowsDismissed();
}
protected void dispatchDocumentCloseEvent() {
if (getFile() == null) {
return;
@@ -697,113 +836,6 @@ public class IDEEditor extends CodeEditor {
}
}
/**
* Requests the language server to provided a semantically larger selection than the current
* selection. If a valid response is received, that range will be selected.
*/
public void expandSelection() {
if (languageServer == null || getFile() == null) {
LOG.error("Cannot expand selection. Language server or file is null");
return;
}
//noinspection deprecation
final var pd =
ProgressDialog.show(
getContext(), null, getContext().getString(string.please_wait), true, false);
final CompletableFuture<Range> future =
CompletableFuture.supplyAsync(
() ->
languageServer.expandSelection(
new ExpandSelectionParams(getFile().toPath(), getCursorLSPRange())));
future.whenComplete(
((range, throwable) -> {
pd.dismiss();
if (throwable != null) {
LOG.error("Error computing expanded selection range", throwable);
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> setSelection(range));
}));
}
/**
* Get the cursor's selection range in the form of {@link Range}.
*
* @return The {@link Range} of the cursor.
*/
public Range getCursorLSPRange() {
final var cursor = getCursor();
final var start = new Position(cursor.getLeftLine(), cursor.getLeftColumn());
final var end = new Position(cursor.getRightLine(), cursor.getRightColumn());
return new Range(start, end);
}
/**
* Get the cursor's position in the form of {@link Position}.
*
* @return The {@link Position} of the cursor.
*/
@SuppressWarnings("unused")
public Position getCursorLSPPosition() {
return new Position(getCursor().getLeftLine(), getCursor().getLeftColumn());
}
/**
* Validates the range if it is invalid and returns a valid range.
*
* @param range Th range to validate.
* @return A new, validated range.
*/
public Range validateRange(@NonNull final Range range) {
final var start = range.getStart();
final var end = range.getEnd();
if (start.getLine() < 0) {
start.setLine(0);
} else if (start.getLine() >= getText().getLineCount()) {
start.setLine(getText().getLineCount() - 1);
}
if (end.getLine() < 0) {
end.setLine(0);
} else if (end.getLine() >= getText().getLineCount()) {
end.setLine(getText().getLineCount() - 1);
}
if (end.getLine() < start.getLine()) {
var l = end.getLine();
var l2 = start.getLine();
start.setLine(l);
end.setLine(l2);
}
if (start.getColumn() < 0) {
start.setColumn(0);
} else if (start.getColumn() >= getText().getColumnCount(start.getLine())) {
start.setColumn(getText().getColumnCount(start.getLine()) - 1);
}
if (end.getColumn() < 0) {
end.setColumn(0);
} else if (end.getColumn() >= getText().getColumnCount(end.getLine())) {
end.setColumn(getText().getColumnCount(end.getLine()) - 1);
}
if (end.getColumn() < start.getColumn()) {
final var c = start.getColumn();
final var c2 = end.getColumn();
start.setColumn(c2);
end.setColumn(c);
}
return new Range(start, end);
}
@Override
public IDEEditorSearcher getSearcher() {
return searcher;
@@ -828,6 +860,7 @@ public class IDEEditor extends CodeEditor {
return;
}
isModified = false;
final var saveEvent = new DocumentSaveEvent(getFile().toPath());
EventBus.getDefault().post(saveEvent);
}
+2 -2
View File
@@ -38,8 +38,8 @@ buildscript {
versionName = Versions.INSTANCE.versionName
packageName = "com.itsaky.androidide"
javaSourceVersion = JavaVersion.VERSION_11
javaTargetVersion = JavaVersion.VERSION_11
javaSourceVersion = Versions.INSTANCE.javaVersion
javaTargetVersion = Versions.INSTANCE.javaVersion
}
repositories {
@@ -17,6 +17,8 @@
package com.itsaky.androidide
import org.gradle.api.JavaVersion
/**
* Version constants for the IDE.
*
@@ -52,6 +54,9 @@ object Versions {
version
}
/** The source and target Java compatibility. */
val javaVersion = JavaVersion.VERSION_11
/** The release version name. */
const val releaseVersionName = "2.1.2-beta"
}
+1
View File
@@ -0,0 +1 @@
/build
+39
View File
@@ -0,0 +1,39 @@
import com.itsaky.androidide.Versions
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
namespace = "com.itsaky.androidide.editor"
compileSdk = Versions.compileSdk
buildToolsVersion = Versions.buildTools
defaultConfig {
minSdk = Versions.minSdk
targetSdk = Versions.targetSdk
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Versions.javaVersion
targetCompatibility = Versions.javaVersion
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(project(path = ":common"))
implementation(project(path = ":shared"))
implementation(project(path = ":lsp:api"))
}
View File
+21
View File
@@ -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
@@ -0,0 +1,43 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.editor;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.itsaky.androidide.editor.test", appContext.getPackageName());
}
}
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,179 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.editor;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import java.io.File;
/**
* Interface for other modules to access the editor.
*
* @author Akash Yadav
*/
public interface IEditor {
/**
* Get the file that this editor is currently editing.
*
* @return The file instance.
*/
File getFile();
/**
* Whether the content of this editor differs from the content of the file on disk.
*
* @return <code>true</code> if the content has been modified, <code>false</code> otherwise.
*/
boolean isModified();
/**
* If any language server is set, requests signature help at the cursor's position. On a valid
* response, shows the signature help in a popup window.
*/
void signatureHelp();
/**
* Shows the given signature help in the editor.
*
* @param help The signature help data to show.
*/
void showSignatureHelp(SignatureHelp help);
/**
* Set the selection of this editor to the given position.
*
* @param position The position to select.
*/
void setSelection(@NonNull Position position);
/**
* Set selection to the given range.
*
* @param range The range to select.
*/
void setSelection(@NonNull Range range);
/**
* Get the cursor's selection range in the form of {@link Range}.
*
* @return The {@link Range} of the cursor.
*/
Range getCursorLSPRange();
/**
* Get the cursor's position in the form of {@link Position}.
*
* @return The {@link Position} of the cursor.
*/
Position getCursorLSPPosition();
/**
* Validates the range if it is invalid and returns a valid range.
*
* @param range Th range to validate.
* @return A new, validated range.
*/
Range validateRange(@NonNull Range range);
/**
* Checks if the given range is valid for this editor's text.
*
* @param range The range to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
boolean isValidRange(Range range);
/**
* Checks if the given position is valid for this editor's text.
*
* @param position The position to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
boolean isValidPosition(Position position);
/**
* Checks if the given line is valid for this editor's text.
*
* @param line The line to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
boolean isValidLine(int line);
/**
* Checks if the given column is valid for this editor's text.
*
* @param line The line of the column to check.
* @param column The column to check.
* @return <code>true</code> if valid, <code>false</code> otherwise.
*/
boolean isValidColumn(int line, int column);
/**
* Set the language server that this editor will connect with. If the language client is not set,
* it'll be set to {@link IDELanguageClientImpl}.
*
* @param server The server to set. Provide <code>null</code> to disable all the language server
* features.
*/
void setLanguageServer(ILanguageServer server);
/**
* Append the given text at the end of the editor's content.
*
* @param text The text to append.
* @return The line at which the text was appended.
*/
int append(CharSequence text);
/** Set the selection of the editor's cursor to the last line of the it's content. */
void goToEnd();
/**
* If any language server is set, asks the language server to find the definition of token at the
* cursor position.
*
* <p>If the server returns a valid response, and the file specified in the response is same the
* file in this editor, the range specified in the response will be selected.
*/
void findDefinition();
/**
* If any language server instance is set, finds the references to of the token at the current
* cursor position.
*
* <p>If the server returns a valid response, that response is forwarded to the {@link
* IDELanguageClientImpl}.
*/
@SuppressWarnings("unused")
void findReferences();
/**
* Requests the language server to provided a semantically larger selection than the current
* selection. If a valid response is received, that range will be selected.
*/
void expandSelection();
/** Ensures that all the windows are dismissed. */
void ensureWindowsDismissed();
}
@@ -0,0 +1,34 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.editor;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
+1
View File
@@ -43,3 +43,4 @@ include ':build-tools:tooling-api-testing'
include ':build-tools:xml-dom'
include ':build-tools:xml-formatter'
include ':build-tools:xml-utils'
include ':editor'
Binary file not shown.