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 ac59d1eaf..a1b3ca092 100644 Binary files a/app/src/main/assets/data/common/tooling-api-all.jar and b/app/src/main/assets/data/common/tooling-api-all.jar differ 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 : - * - *

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

+ * + * @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 469111820..8bb6c7ead 100644 Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ