From 900365c35332e1fc7f9f5a43204d2c3a0b23faec Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 20 Sep 2022 19:24:48 +0530 Subject: [PATCH] Extracted interface for IDEEditor --- app/build.gradle | 1 + .../assets/data/common/tooling-api-all.jar | Bin 12715459 -> 12715459 bytes .../com/itsaky/androidide/EditorActivity.java | 4 +- .../views/editor/CodeEditorView.java | 83 +-- .../androidide/views/editor/IDEEditor.java | 597 +++++++++--------- build.gradle | 4 +- .../java/com/itsaky/androidide/Versions.kt | 5 + editor/.gitignore | 1 + editor/build.gradle.kts | 39 ++ editor/consumer-rules.pro | 0 editor/proguard-rules.pro | 21 + .../editor/ExampleInstrumentedTest.java | 43 ++ editor/src/main/AndroidManifest.xml | 21 + .../com/itsaky/androidide/editor/IEditor.java | 179 ++++++ .../androidide/editor/ExampleUnitTest.java | 34 + settings.gradle | 1 + tests/test-home/.androidide/init/model.jar | Bin 59879 -> 59879 bytes 17 files changed, 666 insertions(+), 367 deletions(-) create mode 100644 editor/.gitignore create mode 100644 editor/build.gradle.kts create mode 100644 editor/consumer-rules.pro create mode 100644 editor/proguard-rules.pro create mode 100644 editor/src/androidTest/java/com/itsaky/androidide/editor/ExampleInstrumentedTest.java create mode 100644 editor/src/main/AndroidManifest.xml create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/IEditor.java create mode 100644 editor/src/test/java/com/itsaky/androidide/editor/ExampleUnitTest.java diff --git a/app/build.gradle b/app/build.gradle index 455d3fa17..46fa2e4bd 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') diff --git a/app/src/main/assets/data/common/tooling-api-all.jar b/app/src/main/assets/data/common/tooling-api-all.jar index ac59d1eaf4e54da4071fb3f3fb45ee3acad10489..a1b3ca092bafc474b9c25571fd755e864834da93 100644 GIT binary patch delta 21743 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%0AfT*MsEB^xF}f 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 RAg99=$e3AnIUNpA{U2OZHaGwP diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java index d0e6e7631..a03a21475 100755 --- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java +++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java @@ -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); } diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/CodeEditorView.java b/app/src/main/java/com/itsaky/androidide/views/editor/CodeEditorView.java index a9377433c..aa85e7ca1 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/CodeEditorView.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/CodeEditorView.java @@ -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 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 { *

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(); } 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 2d0e30ce5..30127141b 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 @@ -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 : - * - *

    - *
  • '(' (parentheses) - *
  • ',' (comma) - *
- * - * @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 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 true if valid, false 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 true if valid, false 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 true if valid, false 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 true if valid, false 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 null 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 { *

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 true if valid, false 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 true if valid, false 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 true if valid, false 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 true if valid, false 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 { *

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 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 : + * + *

    + *
  • '(' (parentheses) + *
  • ',' (comma) + *
+ * + * @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 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); } diff --git a/build.gradle b/build.gradle index fc745f978..3d7535e8f 100755 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/buildSrc/src/main/java/com/itsaky/androidide/Versions.kt b/buildSrc/src/main/java/com/itsaky/androidide/Versions.kt index 501156bfb..94c384758 100644 --- a/buildSrc/src/main/java/com/itsaky/androidide/Versions.kt +++ b/buildSrc/src/main/java/com/itsaky/androidide/Versions.kt @@ -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" } diff --git a/editor/.gitignore b/editor/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/editor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/editor/build.gradle.kts b/editor/build.gradle.kts new file mode 100644 index 000000000..103fcbc75 --- /dev/null +++ b/editor/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/editor/consumer-rules.pro b/editor/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/editor/proguard-rules.pro b/editor/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/editor/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/editor/src/androidTest/java/com/itsaky/androidide/editor/ExampleInstrumentedTest.java b/editor/src/androidTest/java/com/itsaky/androidide/editor/ExampleInstrumentedTest.java new file mode 100644 index 000000000..bd85173f6 --- /dev/null +++ b/editor/src/androidTest/java/com/itsaky/androidide/editor/ExampleInstrumentedTest.java @@ -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 . + */ + +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 Testing documentation + */ +@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()); + } +} \ No newline at end of file diff --git a/editor/src/main/AndroidManifest.xml b/editor/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e62e7b210 --- /dev/null +++ b/editor/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/IEditor.java b/editor/src/main/java/com/itsaky/androidide/editor/IEditor.java new file mode 100644 index 000000000..99533fa44 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/IEditor.java @@ -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 . + */ + +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 true if the content has been modified, false 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 true if valid, false otherwise. + */ + boolean isValidRange(Range range); + + /** + * Checks if the given position is valid for this editor's text. + * + * @param position The position to check. + * @return true if valid, false otherwise. + */ + boolean isValidPosition(Position position); + + /** + * Checks if the given line is valid for this editor's text. + * + * @param line The line to check. + * @return true if valid, false 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 true if valid, false 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 null 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. + * + *

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. + * + *

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(); +} diff --git a/editor/src/test/java/com/itsaky/androidide/editor/ExampleUnitTest.java b/editor/src/test/java/com/itsaky/androidide/editor/ExampleUnitTest.java new file mode 100644 index 000000000..fce4dc05d --- /dev/null +++ b/editor/src/test/java/com/itsaky/androidide/editor/ExampleUnitTest.java @@ -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 . + */ + +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 Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3e7505e20..dc623fcb4 100755 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 469111820ee65116012a513747e7e3d5f9d469e6..8bb6c7ead644bc11706cc2015b31850d7765fac0 100644 GIT binary patch delta 415 zcmaEUnfdu;W}X0VW)?061`ZB}S+h3sTw!OLIcxGG_C63*#Ni2|_HqOOsm=VHOdKFu zSa2T?h`uaq15&(MP;~_>ke<9-%MHY3*7jxr(E<9LAbN7Nu{Vgj!uSavi0*SL)d$h6 zMccuq?W>v!Rvucf%M224X%c4x(Q`X$z~a)qYned&Q~ipfAo}f+I$jW+yU7Burqrl__4r?%dn!^bwyqTYq zi33Cn3-03q(U)ayK*~1@s;*!Kt2?P>0pc@jd$WM(0DVptuz04iEr`Fu_z53~?sF>D z2hprW+rj4TtC|W{A6l==3=(f?5@!R^b31Cl;?liqnLzwg{feR>`t6cBUJ#wT$rmiY zXWM_UzP-Dmz_jGPZBiili#HZCGxbj1Tz01!q<`}7dm)qG-VvA_f8P^K@3NOS4?@43F7^K0alsu GvK|0oWvy}m