From 230ccfa5369eade274e5dea0fbfa2f820dcbb7e7 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 30 Sep 2023 15:08:28 +0530 Subject: [PATCH] fix(editor): rewrite IDEEditor in Kotlin with Coroutines --- .../androidide/ui/editor/CodeEditorView.java | 4 +- .../androidide/utils/FlashbarActivityUtils.kt | 10 +- .../itsaky/androidide/editor/api/IEditor.java | 8 +- .../editor/ui/AbstractPopupWindow.kt | 1 + .../editor/ui/EditorSearchLayout.kt | 4 +- .../androidide/editor/ui/IDEEditor.java | 1022 ----------------- .../itsaky/androidide/editor/ui/IDEEditor.kt | 890 ++++++++++++++ .../api/DefaultLanguageServerRegistry.java | 10 +- ...LanguageServer.java => ILanguageServer.kt} | 102 +- .../lsp/java/JavaLanguageServer.java | 378 ------ .../androidide/lsp/java/JavaLanguageServer.kt | 322 ++++++ .../itsaky/androidide/lsp/java/JavaLSPTest.kt | 4 +- .../androidide/lsp/xml/XMLLanguageServer.java | 174 --- .../androidide/lsp/xml/XMLLanguageServer.kt | 146 +++ .../itsaky/androidide/lsp/xml/XMLLSPTest.kt | 2 +- 15 files changed, 1426 insertions(+), 1651 deletions(-) delete mode 100644 editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt rename lsp/api/src/main/java/com/itsaky/androidide/lsp/api/{ILanguageServer.java => ILanguageServer.kt} (58%) delete mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java create mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt delete mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.java create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.kt diff --git a/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java b/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java index 374c672eb..03dfafb4f 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java +++ b/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java @@ -224,7 +224,7 @@ public class CodeEditorView extends LinearLayout { } protected void onInputTypePrefChanged() { - binding.editor.setInputType(IDEEditor.createInputFlags()); + binding.editor.setInputType(IDEEditor.Companion.createInputTypeFlags()); } protected void onPrintingFlagsPrefChanged() { @@ -439,6 +439,6 @@ public class CodeEditorView extends LinearLayout { return; } - editor.updateFile(file); + editor.setFile(file); } } diff --git a/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt b/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt index 9c70b31d4..408b7ff20 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt @@ -104,15 +104,19 @@ fun Activity.flashInfo(@StringRes msg: Int) { } @JvmOverloads -fun Activity.flashProgress(configure: Consumer? = null, action: Consumer) { +fun Activity.flashProgress( + configure: (Flashbar.Builder.() -> Unit)? = null, + action: (Flashbar) -> R? +) : R? { val builder = flashbarBuilder(gravity = TOP, duration = DURATION_INDEFINITE) .showProgress(Flashbar.ProgressPosition.LEFT) - configure?.accept(builder) + configure?.invoke(builder) val flashbar = builder.build() flashbar.show() - action.accept(flashbar) + + return action(flashbar) } fun Flashbar.Builder.showOnUiThread() { diff --git a/editor-api/src/main/java/com/itsaky/androidide/editor/api/IEditor.java b/editor-api/src/main/java/com/itsaky/androidide/editor/api/IEditor.java index 2153217f2..2afd2c003 100644 --- a/editor-api/src/main/java/com/itsaky/androidide/editor/api/IEditor.java +++ b/editor-api/src/main/java/com/itsaky/androidide/editor/api/IEditor.java @@ -19,6 +19,7 @@ package com.itsaky.androidide.editor.api; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.itsaky.androidide.models.Position; import com.itsaky.androidide.models.Range; @@ -38,6 +39,7 @@ public interface IEditor { * * @return The file instance. */ + @Nullable File getFile(); /** @@ -59,7 +61,11 @@ public interface IEditor { * * @param range The range to select. */ - void setSelection(@NonNull Range range); + default void setSelection(@NonNull Range range) { + setSelection(range.getStart(), range.getEnd()); + } + + void setSelection(@NonNull Position start, @NonNull Position end); /** * Get the cursor's selection range in the form of {@link Range}. diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/AbstractPopupWindow.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/AbstractPopupWindow.kt index f6de476f8..a2bbbe4e9 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/AbstractPopupWindow.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/AbstractPopupWindow.kt @@ -27,6 +27,7 @@ import io.github.rosemoe.sora.widget.base.EditorPopupWindow */ abstract class AbstractPopupWindow(editor: CodeEditor, features: Int) : EditorPopupWindow(editor, features) { + override fun show() { (editor as? IDEEditor)?.ensureWindowsDismissed() super.show() diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorSearchLayout.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorSearchLayout.kt index f3974c54f..0b75c81c5 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorSearchLayout.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorSearchLayout.kt @@ -79,7 +79,7 @@ class EditorSearchLayout(context: Context, val editor: IDEEditor) : FrameLayout( 1 -> SearchOptions(ignoreCase, it.isChecked) else -> searchOptions } - editor.searcher?.updateSearchOptions(searchOptions) + editor.searcher.updateSearchOptions(searchOptions) true } else false @@ -115,7 +115,7 @@ class EditorSearchLayout(context: Context, val editor: IDEEditor) : FrameLayout( findInFileBinding.searchInput.removeTextChangedListener(this.searchInputTextWatcher) findInFileBinding.root.visibility = GONE this.searchInputTextWatcher = null - this.editor.searcher.onClose() + searcher.onClose() } if (!searcher.hasQuery()) { return diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java deleted file mode 100644 index e708b09ac..000000000 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java +++ /dev/null @@ -1,1022 +0,0 @@ -/* - * 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.ui; - -import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getTabSize; -import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getVisiblePasswordFlag; -import static com.itsaky.androidide.resources.R.string; -import static java.lang.Math.max; -import static java.lang.Math.min; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.view.inputmethod.EditorInfo; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.annotation.UiThread; -import com.blankj.utilcode.util.FileUtils; -import com.blankj.utilcode.util.ThreadUtils; -import com.itsaky.androidide.editor.R; -import com.itsaky.androidide.editor.adapters.CompletionListAdapter; -import com.itsaky.androidide.editor.api.IEditor; -import com.itsaky.androidide.editor.api.ILspEditor; -import com.itsaky.androidide.editor.language.IDELanguage; -import com.itsaky.androidide.editor.language.cpp.CppLanguage; -import com.itsaky.androidide.editor.language.groovy.GroovyLanguage; -import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage; -import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider; -import com.itsaky.androidide.editor.schemes.IDEColorScheme; -import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider; -import com.itsaky.androidide.editor.snippets.AbstractSnippetVariableResolver; -import com.itsaky.androidide.editor.snippets.FileVariableResolver; -import com.itsaky.androidide.editor.snippets.WorkspaceVariableResolver; -import com.itsaky.androidide.eventbus.events.editor.ChangeType; -import com.itsaky.androidide.eventbus.events.editor.ColorSchemeInvalidatedEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent; -import com.itsaky.androidide.flashbar.Flashbar; -import com.itsaky.androidide.lsp.api.ILanguageClient; -import com.itsaky.androidide.lsp.api.ILanguageServer; -import com.itsaky.androidide.lsp.models.Command; -import com.itsaky.androidide.lsp.models.DefinitionParams; -import com.itsaky.androidide.lsp.models.DefinitionResult; -import com.itsaky.androidide.lsp.models.ExpandSelectionParams; -import com.itsaky.androidide.lsp.models.ReferenceParams; -import com.itsaky.androidide.lsp.models.ReferenceResult; -import com.itsaky.androidide.lsp.models.ShowDocumentParams; -import com.itsaky.androidide.lsp.models.SignatureHelp; -import com.itsaky.androidide.lsp.models.SignatureHelpParams; -import com.itsaky.androidide.models.Position; -import com.itsaky.androidide.models.Range; -import com.itsaky.androidide.progress.ICancelChecker; -import com.itsaky.androidide.projects.FileManager; -import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme; -import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; -import com.itsaky.androidide.utils.DocumentUtils; -import com.itsaky.androidide.utils.FlashbarActivityUtilsKt; -import com.itsaky.androidide.utils.FlashbarUtilsKt; -import com.itsaky.androidide.utils.ILogger; -import io.github.rosemoe.sora.event.ContentChangeEvent; -import io.github.rosemoe.sora.event.SelectionChangeEvent; -import io.github.rosemoe.sora.event.Unsubscribe; -import io.github.rosemoe.sora.lang.EmptyLanguage; -import io.github.rosemoe.sora.lang.Language; -import io.github.rosemoe.sora.widget.CodeEditor; -import io.github.rosemoe.sora.widget.IDEEditorSearcher; -import io.github.rosemoe.sora.widget.component.EditorAutoCompletion; -import io.github.rosemoe.sora.widget.component.EditorTextActionWindow; -import io.github.rosemoe.sora.widget.snippet.variable.ISnippetVariableResolver; -import java.io.File; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import kotlin.io.FilesKt; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { - - private static final long SELECTION_CHANGE_DELAY = 500; - - private static final ILogger LOG = ILogger.newInstance("IDEEditor"); - private int fileVersion; - private File file; - private boolean isModified; - private boolean ensurePosAnimEnabled = true; - @Nullable - private EditorActionsMenu actionsMenu; - private IDEEditorSearcher searcher; - private ILanguageServer languageServer; - private SignatureHelpWindow signatureHelpWindow; - private DiagnosticWindow diagnosticWindow; - @Nullable - ILanguageClient languageClient; - - private final Handler selectionChangeHandler = new Handler(Looper.getMainLooper()); - - @Nullable - private Runnable selectionChangeRunner = () -> { - final var cursor = getCursor(); - - // we do not use getSignatureHelpWindow() to avoid initializing the window unnecessarily - if (languageClient == null || cursor == null || cursor.isSelected() || - (signatureHelpWindow != null && signatureHelpWindow.isShowing())) { - return; - } - - final var line = cursor.getLeftLine(); - final var column = cursor.getLeftColumn(); - - // diagnostics are expected to be sorted, so, we do a binary search - getDiagnosticWindow().showDiagnostic(languageClient.getDiagnosticAt(getFile(), line, column)); - }; - - public IDEEditor(Context context) { - this(context, null); - } - - public IDEEditor(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public IDEEditor(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public IDEEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - - this.isModified = false; - this.actionsMenu = new EditorActionsMenu(this); - this.actionsMenu.init(); - - final var window = new EditorCompletionWindow(this); - window.setAdapter(new CompletionListAdapter()); - replaceComponent(EditorAutoCompletion.class, window); - - setColorScheme(SchemeAndroidIDE.newInstance(context)); - setSearcher(new IDEEditorSearcher(this)); - getComponent(EditorTextActionWindow.class).setEnabled(false); - subscribeEvent(SelectionChangeEvent.class, this::handleSelectionChange); - subscribeEvent(ContentChangeEvent.class, this::handleContentChange); - - setInputType(createInputFlags()); - - EventBus.getDefault().register(this); - } - - private void handleSelectionChange(@NonNull SelectionChangeEvent event, Unsubscribe unsubscribe) { - if (getDiagnosticWindow().isShowing()) { - getDiagnosticWindow().dismiss(); - } - - if (selectionChangeRunner != null) { - selectionChangeHandler.removeCallbacks(selectionChangeRunner); - selectionChangeHandler.postDelayed(selectionChangeRunner, SELECTION_CHANGE_DELAY); - } - } - - private DiagnosticWindow getDiagnosticWindow() { - if (diagnosticWindow == null) { - diagnosticWindow = new DiagnosticWindow(this); - } - - return diagnosticWindow; - } - - public void setEnsurePosAnimEnabled(boolean ensurePosAnimEnabled) { - this.ensurePosAnimEnabled = ensurePosAnimEnabled; - } - - public boolean isEnsurePosAnimEnabled() { - return ensurePosAnimEnabled; - } - - /** - * Get the file that this editor is currently editing. - * - * @return The file instance. - */ - @Override - public File getFile() { - return file; - } - - /** - * Set the file that this editor is currently editing. - * - * @param file The file to set. - */ - public void setFile(File file) { - updateFile(file); - - if (file != null) { - dispatchDocumentOpenEvent(); - } - } - - public void updateFile(File file) { - this.file = file; - } - - @NonNull - @Override - public Bundle getExtraArguments() { - var args = super.getExtraArguments(); - final var path = file == null ? null : file.getAbsolutePath(); - args.putString(KEY_FILE, path); - return args; - } - - protected void dispatchDocumentOpenEvent() { - if (getFile() == null) { - return; - } - - final var openEvent = new DocumentOpenEvent(getFile().toPath(), getText().toString(), - fileVersion = 0); - - // Notify FileManager first - FileManager.INSTANCE.onDocumentOpen(openEvent); - - EventBus.getDefault().post(openEvent); - } - - @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 = CompletableFuture.supplyAsync( - () -> languageServer.signatureHelp( - new SignatureHelpParams(getFile().toPath(), getCursorLSPPosition()))); - - future.whenComplete((help, error) -> { - if (help == null || languageClient == null || future.isCancelled() || - future.isCompletedExceptionally()) { - LOG.error("An error occurred while finding signature help", error); - return; - } - - //noinspection ConstantConditions - ThreadUtils.runOnUiThread(() -> showSignatureHelp(help)); - }); - } - } - - @Override - public int getTabWidth() { - return getTabSize(); - } - - /** - * Shows the given signature help in the editor. - * - * @param help The signature help data to show. - */ - @Override - public void showSignatureHelp(SignatureHelp help) { - getSignatureHelpWindow().setupAndDisplay(help); - } - - private SignatureHelpWindow getSignatureHelpWindow() { - if (signatureHelpWindow == null) { - signatureHelpWindow = new SignatureHelpWindow(this); - } - - return signatureHelpWindow; - } - - /** - * 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 (isValidPosition(range.getStart(), true) && isValidPosition(range.getEnd(), true)) { - setSelectionRegion(range.getStart().getLine(), range.getStart().getColumn(), - range.getEnd().getLine(), range.getEnd().getColumn()); - } else { - LOG.warn("Selection range is invalid", range); - } - } - - @Override - public void ensurePositionVisible(int line, int column, boolean noAnimation) { - super.ensurePositionVisible(line, column, !isEnsurePosAnimEnabled() || noAnimation); - } - - /** - * 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. - */ - @Override - public void validateRange(@NonNull final Range range) { - final var start = range.getStart(); - final var end = range.getEnd(); - final var text = getText(); - final var lineCount = text.getLineCount(); - - start.setLine(min(max(0, start.getLine()), lineCount - 1)); - start.setColumn(min(max(0, start.getColumn()), text.getColumnCount(start.getLine()))); - - end.setLine(min(max(0, end.getLine()), lineCount - 1)); - end.setColumn(min(max(0, end.getColumn()), text.getColumnCount(end.getLine()))); - } - - /** - * 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, boolean allowColumnEqual) { - if (range == null) { - return false; - } - - final var start = range.getStart(); - final var end = range.getEnd(); - - return isValidPosition(start, allowColumnEqual) && isValidPosition(end, allowColumnEqual) && - 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, boolean allowColumnEqual) { - if (position == null) { - return false; - } - - return isValidLine(position.getLine()) && - isValidColumn(position.getLine(), position.getColumn(), allowColumnEqual); - } - - /** - * 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, boolean allowEqual) { - final var columnCount = getText().getColumnCount(line); - return column >= 0 && (column < columnCount || (allowEqual && column == columnCount)); - } - - @Override - @UiThread - public void replaceContent(final CharSequence newContent) { - final var lastLine = getText().getLineCount() - 1; - final var lastColumn = getText().getColumnCount(lastLine); - getText().replace(0, 0, lastLine, lastColumn, newContent == null ? "" : newContent); - } - - /** - * Set the language server that this editor will connect with. If the language client is not set, - * it'll be set to {@link ILanguageClient} set in the language server. - * - * @param server The server to set. Provide null to disable all the language server - * features. - */ - @Override - public void setLanguageServer(ILanguageServer server) { - this.languageServer = server; - - if (server != null) { - this.languageClient = server.getClient(); - getSnippetController().setFileVariableResolver(new FileVariableResolver(this)); - getSnippetController().setWorkspaceVariableResolver(new WorkspaceVariableResolver()); - } - } - - @Override - public void setLanguageClient(@Nullable final ILanguageClient languageClient) { - this.languageClient = languageClient; - } - - /** - * 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. - */ - @Override - public int append(CharSequence text) { - final var content = getText(); - if (getLineCount() <= 0) { - return 0; - } - final int line = getLineCount() - 1; - int col = content.getColumnCount(line); - if (col < 0) { - col = 0; - } - content.insert(line, col, text); - return line; - } - - /** - * 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); - } - - /** - * 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. - */ - @Override - public void findDefinition() { - if (getFile() == null) { - return; - } - - final var cancelChecker = new ICancelChecker.Default(); - - final var action = (Consumer) flashbar -> { - final var params = new DefinitionParams(getFile().toPath(), - getCursorLSPPosition()); - - final CompletableFuture future = CompletableFuture.supplyAsync( - () -> languageServer.findDefinition(params, cancelChecker)); - - future.whenComplete( - (result, error) -> onFindDefinitionResult(flashbar, future, result, error)); - }; - - FlashbarActivityUtilsKt.flashProgress(((Activity) getContext()), - builder -> configureFlashbar(builder, string.msg_finding_definition, cancelChecker), - action); - } - - private void onFindDefinitionResult(Flashbar flashbar, CompletableFuture future, - DefinitionResult result, Throwable error) { - LOG.debug("onFindDefinitionsResult"); - dismissOnUiThread(flashbar); - - if (result == null || languageClient == null || future.isCancelled() || - future.isCompletedExceptionally()) { - LOG.error("An error occurred while finding definition", error); - notify(string.msg_no_definition); - return; - } - - final var locations = result.getLocations(); - if (locations.size() == 0) { - LOG.error("No definitions found", "Size:", locations.size()); - notify(string.msg_no_definition); - return; - } - - ThreadUtils.runOnUiThread(() -> { - if (locations.size() == 1) { - var location = locations.get(0); - if (DocumentUtils.isSameFile(location.getFile(), getFile().toPath())) { - setSelection(location.getRange()); - return; - } - languageClient.showDocument( - new ShowDocumentParams(location.getFile(), location.getRange())); - } else { - languageClient.showLocations(locations); - } - }); - } - - /** - * Dismisses the given flashbar on the UI thread. - * - * @param flashbar The flashbar to dismiss. - */ - private void dismissOnUiThread(@NonNull final Flashbar flashbar) { - post(flashbar::dismiss); - } - - /** - * 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 - * ILanguageClient}. - */ - @Override - public void findReferences() { - if (getFile() == null) { - return; - } - - final var cancelChecker = new ICancelChecker.Default(); - - final var action = (Consumer) flashbar -> { - final var referenceParams = new ReferenceParams(getFile().toPath(), - getCursorLSPPosition(), true); - - final var future = CompletableFuture.supplyAsync( - () -> languageServer.findReferences(referenceParams, cancelChecker)); - - future.whenComplete( - (result, error) -> onFindReferencesResult(flashbar, future, result, error)); - }; - - FlashbarActivityUtilsKt.flashProgress(((Activity) getContext()), - builder -> configureFlashbar(builder, R.string.msg_finding_references, cancelChecker), - action); - } - - private void onFindReferencesResult(final Flashbar flashbar, - final CompletableFuture future, - final ReferenceResult result, final Throwable error - ) { - dismissOnUiThread(flashbar); - - if (result == null || languageClient == null || future.isCancelled() || - future.isCompletedExceptionally()) { - LOG.error("An error occurred while finding references", error); - notify(string.msg_no_references); - return; - } - - if (result.getLocations().isEmpty()) { - notify(string.msg_no_references); - return; - } - - ThreadUtils.runOnUiThread(() -> { - if (result.getLocations().size() == 1) { - final var loc = result.getLocations().get(0); - if (DocumentUtils.isSameFile(loc.getFile(), getFile().toPath())) { - setSelection(loc.getRange()); - return; - } - } - - languageClient.showLocations(result.getLocations()); - }); - } - - /** - * Notify the user that no results were found for a task. - * - */ - private void notify(@StringRes final int message) { - //noinspection ConstantConditions - ThreadUtils.runOnUiThread(() -> FlashbarUtilsKt.flashError(message)); - } - - private void configureFlashbar(Flashbar.Builder builder, @StringRes int message, - ICancelChecker cancelChecker) { - builder.message(message) - .negativeActionText(android.R.string.cancel) - .negativeActionTapListener(bar -> { - cancelChecker.cancel(); - bar.dismiss(); - }); - } - - /** - * 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; - } - - //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)); - })); - } - - @Override - protected void onFocusChanged(final boolean gainFocus, final int direction, - @Nullable final Rect previouslyFocusedRect - ) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (!gainFocus) { - ensureWindowsDismissed(); - } - } - - /** - * Ensures that all the windows are dismissed. - */ - @Override - public void ensureWindowsDismissed() { - if (getDiagnosticWindow().isShowing()) { - getDiagnosticWindow().dismiss(); - } - - if (getSignatureHelpWindow().isShowing()) { - getSignatureHelpWindow().dismiss(); - } - - if (actionsMenu != null && actionsMenu.isShowing()) { - actionsMenu.dismiss(); - } - } - - /** - * 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) { - isModified = true; - if (getFile() == 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 (languageServer == null) { - return; - } - final var changeLength = event.getChangedText().length(); - if (event.getAction() != ContentChangeEvent.ACTION_INSERT || changeLength > 0 && - changeLength <= 2) { // changeLength will be 2 as '(' and ')' are inserted at the same time - 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 changedText = event.getChangedText().toString(); - final var changeEvent = new DocumentChangeEvent(file, changedText, getText().toString(), - ++fileVersion, type, changeDelta, changeRange); - - // Notify FileManager first - FileManager.INSTANCE.onDocumentContentChange(changeEvent); - 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 markModified() { - this.isModified = true; - } - - 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(); - - if (actionsMenu != null) { - actionsMenu.unsubscribeEvents(); - } - - if (selectionChangeRunner != null) { - this.selectionChangeHandler.removeCallbacks(selectionChangeRunner); - this.selectionChangeRunner = null; - } - - ensureWindowsDismissed(); - } - - @Override - public void release() { - ensureWindowsDismissed(); - super.release(); - ISnippetVariableResolver resolver = getSnippetController().getFileVariableResolver(); - if (resolver instanceof AbstractSnippetVariableResolver) { - ((AbstractSnippetVariableResolver) resolver).close(); - } - - resolver = getSnippetController().getWorkspaceVariableResolver(); - if (resolver instanceof AbstractSnippetVariableResolver) { - ((AbstractSnippetVariableResolver) resolver).close(); - } - - getSnippetController().setFileVariableResolver(null); - getSnippetController().setWorkspaceVariableResolver(null); - - if (this.actionsMenu != null) { - this.actionsMenu.destroy(); - } - this.actionsMenu = null; - this.file = null; - this.languageServer = null; - this.languageClient = null; - if (EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().unregister(this); - } - } - - protected void dispatchDocumentCloseEvent() { - if (getFile() == null) { - return; - } - - final var closeEvent = new DocumentCloseEvent(getFile().toPath(), getCursorLSPRange()); - - // Notify FileManager first - FileManager.INSTANCE.onDocumentClose(closeEvent); - - EventBus.getDefault().post(closeEvent); - } - - public void onEditorSelected() { - if (getFile() == null) { - return; - } - - dispatchDocumentSelectedEvent(); - } - - protected void dispatchDocumentSelectedEvent() { - if (getFile() == null) { - return; - } - - final var selectedEvent = new DocumentSelectedEvent(getFile().toPath()); - EventBus.getDefault().post(selectedEvent); - } - - @Override - @SuppressWarnings("unused") - public void executeCommand(@Nullable Command command) { - if (command == null) { - LOG.warn("Cannot execute command in editor. Command is null."); - return; - } - - LOG.info(String.format("Executing command '%s' for completion item.", command.getTitle())); - switch (command.getCommand()) { - case Command.TRIGGER_COMPLETION: - final var completion = getComponent(EditorAutoCompletion.class); - completion.requireCompletion(); - break; - case Command.TRIGGER_PARAMETER_HINTS: - signatureHelp(); - break; - case Command.FORMAT_CODE: - formatCodeAsync(); - break; - } - } - - @Override - public IDEEditorSearcher getSearcher() { - return searcher; - } - - protected void setSearcher(@NonNull IDEEditorSearcher searcher) { - this.searcher = searcher; - } - - @Override - public void beginSearchMode() { - throw new UnsupportedOperationException( - "Search ActionMode is not supported. Use CodeEditorView.beginSearch() instead."); - } - - public void dispatchDocumentSaveEvent() { - if (getFile() == null) { - return; - } - - isModified = false; - final var saveEvent = new DocumentSaveEvent(getFile().toPath()); - EventBus.getDefault().post(saveEvent); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN_ORDERED) - public void onColorSchemeInvalidated(ColorSchemeInvalidatedEvent event) { - final var file = getFile(); - if (file == null) { - return; - } - - setupLanguage(file); - } - - public void setupLanguage(File file) { - if (file == null) { - return; - } - - final var language = createLanguage(file); - final var extension = FilesKt.getExtension(file); - if (language instanceof TreeSitterLanguage) { - IDEColorSchemeProvider.INSTANCE.readScheme(getContext(), extension, scheme -> { - applyTreeSitterLang(language, extension, scheme); - }); - } else { - setEditorLanguage(language); - } - } - - public void applyTreeSitterLang(final TreeSitterLanguage language, final String type, - @Nullable SchemeAndroidIDE scheme - ) { - applyTreeSitterLang(((Language) language), type, scheme); - } - - private void applyTreeSitterLang(final Language language, final String type, - @Nullable SchemeAndroidIDE scheme - ) { - if (scheme == null) { - LOG.error("Failed to read current color scheme"); - scheme = SchemeAndroidIDE.newInstance(getContext()); - } - - if (scheme instanceof IDEColorScheme && - ((IDEColorScheme) scheme).getLanguageScheme(type) == null) { - LOG.warn("Color scheme does not support file type '" + type + "'"); - scheme = SchemeAndroidIDE.newInstance(getContext()); - } - - if (scheme instanceof DynamicColorScheme) { - ((DynamicColorScheme) scheme).apply(getContext()); - } - - setColorScheme(scheme); - setEditorLanguage(language); - } - - private Language createLanguage(File file) { - if (!file.isFile()) { - return new EmptyLanguage(); - } - - final var tsLang = TreeSitterLanguageProvider.INSTANCE.forFile(file, getContext()); - if (tsLang != null) { - return tsLang; - } - - String ext = FileUtils.getFileExtension(file); - switch (ext) { - case "gradle": - return new GroovyLanguage(); - case "c": - case "h": - case "cc": - case "cpp": - case "cxx": - return new CppLanguage(); - default: - return new EmptyLanguage(); - } - } -} diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt new file mode 100644 index 000000000..568acb154 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -0,0 +1,890 @@ +/* + * 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.ui + +import android.app.Activity +import android.content.Context +import android.graphics.Rect +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import androidx.annotation.StringRes +import androidx.annotation.UiThread +import com.blankj.utilcode.util.FileUtils +import com.itsaky.androidide.editor.R.string +import com.itsaky.androidide.editor.adapters.CompletionListAdapter +import com.itsaky.androidide.editor.api.IEditor +import com.itsaky.androidide.editor.api.ILspEditor +import com.itsaky.androidide.editor.language.IDELanguage +import com.itsaky.androidide.editor.language.cpp.CppLanguage +import com.itsaky.androidide.editor.language.groovy.GroovyLanguage +import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage +import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider +import com.itsaky.androidide.editor.schemes.IDEColorScheme +import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider.readScheme +import com.itsaky.androidide.editor.snippets.AbstractSnippetVariableResolver +import com.itsaky.androidide.editor.snippets.FileVariableResolver +import com.itsaky.androidide.editor.snippets.WorkspaceVariableResolver +import com.itsaky.androidide.eventbus.events.editor.ChangeType +import com.itsaky.androidide.eventbus.events.editor.ColorSchemeInvalidatedEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent +import com.itsaky.androidide.flashbar.Flashbar +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.api.ILanguageServer +import com.itsaky.androidide.lsp.models.Command +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.ExpandSelectionParams +import com.itsaky.androidide.lsp.models.ReferenceParams +import com.itsaky.androidide.lsp.models.ReferenceResult +import com.itsaky.androidide.lsp.models.ShowDocumentParams +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import com.itsaky.androidide.preferences.internal.tabSize +import com.itsaky.androidide.preferences.internal.visiblePasswordFlag +import com.itsaky.androidide.progress.ICancelChecker +import com.itsaky.androidide.projects.FileManager.onDocumentClose +import com.itsaky.androidide.projects.FileManager.onDocumentContentChange +import com.itsaky.androidide.projects.FileManager.onDocumentOpen +import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme +import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE +import com.itsaky.androidide.utils.DocumentUtils +import com.itsaky.androidide.utils.ILogger +import com.itsaky.androidide.utils.flashError +import com.itsaky.androidide.utils.flashProgress +import io.github.rosemoe.sora.event.ContentChangeEvent +import io.github.rosemoe.sora.event.SelectionChangeEvent +import io.github.rosemoe.sora.lang.EmptyLanguage +import io.github.rosemoe.sora.lang.Language +import io.github.rosemoe.sora.widget.CodeEditor +import io.github.rosemoe.sora.widget.EditorSearcher +import io.github.rosemoe.sora.widget.IDEEditorSearcher +import io.github.rosemoe.sora.widget.component.EditorAutoCompletion +import io.github.rosemoe.sora.widget.component.EditorBuiltinComponent +import io.github.rosemoe.sora.widget.component.EditorTextActionWindow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.File +import java.util.function.Consumer + +/** + * [CodeEditor] implementation for the IDE. + * + * @author Akash Yadav + */ +open class IDEEditor @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor, ILspEditor { + + private var _actionsMenu: EditorActionsMenu? = null + private var _signatureHelpWindow: SignatureHelpWindow? = null + private var _diagnosticWindow: DiagnosticWindow? = null + private var file: File? = null + private var fileVersion = 0 + private var isModified = false + + private val selectionChangeHandler = Handler(Looper.getMainLooper()) + private var selectionChangeRunner: Runnable? = Runnable { + val languageClient = languageClient ?: return@Runnable + val cursor = this.cursor ?: return@Runnable + + if (cursor.isSelected || _signatureHelpWindow?.isShowing == true) { + return@Runnable + } + + diagnosticWindow.showDiagnostic( + languageClient.getDiagnosticAt(file, cursor.leftLine, cursor.leftColumn) + ) + } + + protected val editorScope = CoroutineScope(Dispatchers.Default) + + var languageServer: ILanguageServer? = null + private set + + var languageClient: ILanguageClient? = null + private set + + /** + * Whether the cursor position change animation is enabled for the editor. + */ + var isEnsurePosAnimEnabled = true + + /** + * The text searcher for the editor. + */ + lateinit var searcher: IDEEditorSearcher + + /** + * The signature help window for the editor. + */ + val signatureHelpWindow: SignatureHelpWindow + get() { + return _signatureHelpWindow ?: SignatureHelpWindow(this) + .also { _signatureHelpWindow = it } + } + + /** + * The diagnostic window for the editor. + */ + val diagnosticWindow: DiagnosticWindow + get() { + return _diagnosticWindow ?: DiagnosticWindow(this) + .also { _diagnosticWindow = it } + } + + companion object { + + private const val SELECTION_CHANGE_DELAY = 500L + + private val log = ILogger.newInstance("IDEEditor") + + /** + * Create input type flags for the editor. + */ + fun createInputTypeFlags(): Int { + var flags = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or + EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS + if (visiblePasswordFlag) { + flags = flags or EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } + return flags + } + } + + init { + initEditor() + } + + override fun getFile(): File? { + return this.file + } + + /** + * Set the file for this editor. + */ + fun setFile(file: File?) { + this.file = file + file?.also { + dispatchDocumentOpenEvent() + } + } + + override fun isModified(): Boolean { + return this.isModified + } + + override fun setSelection(position: Position) { + setSelection(position.line, position.column) + } + + override fun setSelection(start: Position, end: Position) { + if (!isValidPosition(start, true) || !isValidPosition(end, true)) { + log.warn("Invalid selection range: start=$start end=$end") + return + } + + setSelectionRegion(start.line, start.column, end.line, end.column) + } + + override fun getCursorLSPRange(): Range { + val end = cursor.right().let { + Position(line = it.line, column = it.column, index = it.index) + } + return Range(cursorLSPPosition, end) + } + + override fun getCursorLSPPosition(): Position { + return cursor.left().let { + Position(line = it.line, column = it.column, index = it.index) + } + } + + override fun validateRange(range: Range) { + val start = range.start + val end = range.end + val text = text + val lineCount = text.lineCount + + start.line = 0.coerceAtLeast(start.line).coerceAtMost(lineCount - 1) + start.column = 0.coerceAtLeast(start.column).coerceAtMost(text.getColumnCount(start.line)) + + end.line = 0.coerceAtLeast(end.line).coerceAtMost(lineCount - 1) + end.column = 0.coerceAtLeast(end.column).coerceAtMost(text.getColumnCount(end.line)) + } + + override fun isValidRange(range: Range?, allowColumnEqual: Boolean): Boolean { + if (range == null) { + return false + } + val start = range.start + val end = range.end + return isValidPosition(start, allowColumnEqual) + // make sure start position is before end position + && isValidPosition(end, allowColumnEqual) && start < end + } + + override fun isValidPosition(position: Position?, allowColumnEqual: Boolean): Boolean { + return if (position == null) { + false + } else isValidLine(position.line) && + isValidColumn(position.line, position.column, allowColumnEqual) + } + + override fun isValidLine(line: Int): Boolean { + return line >= 0 && line < text.lineCount + } + + override fun isValidColumn(line: Int, column: Int, allowEqual: Boolean): Boolean { + val columnCount = text.getColumnCount(line) + return column >= 0 && (column < columnCount || allowEqual && column == columnCount) + } + + override fun append(text: CharSequence?): Int { + val content = getText() + if (lineCount <= 0) { + return 0 + } + val line = lineCount - 1 + var col = content.getColumnCount(line) + if (col < 0) { + col = 0 + } + content.insert(line, col, text) + return line + } + + @UiThread + override fun replaceContent(newContent: CharSequence?) { + val lastLine = text.lineCount - 1 + val lastColumn = text.getColumnCount(lastLine) + text.replace(0, 0, lastLine, lastColumn, newContent ?: "") + } + + override fun goToEnd() { + val line = text.lineCount - 1 + setSelection(line, 0) + } + + override fun setLanguageServer(server: ILanguageServer?) { + this.languageServer = server + server?.also { + this.languageClient = it.client + snippetController.apply { + fileVariableResolver = FileVariableResolver(this@IDEEditor) + workspaceVariableResolver = WorkspaceVariableResolver() + } + } + } + + override fun setLanguageClient(client: ILanguageClient?) { + this.languageClient = client + } + + override fun executeCommand(command: Command?) { + if (command == null) { + log.warn("Cannot execute command in editor. Command is null.") + return + } + + log.info(String.format("Executing command '%s' for completion item.", command.title)) + when (command.command) { + Command.TRIGGER_COMPLETION -> { + val completion = getComponent(EditorAutoCompletion::class.java) + completion.requireCompletion() + } + + Command.TRIGGER_PARAMETER_HINTS -> signatureHelp() + Command.FORMAT_CODE -> formatCodeAsync() + } + } + + override fun signatureHelp() { + val languageServer = this.languageServer ?: return + val file = this.file ?: return + + this.languageClient ?: return + + editorScope.launch { + val params = SignatureHelpParams(file.toPath(), cursorLSPPosition) + val help = languageServer.signatureHelp(params) + + withContext(Dispatchers.Main) { + showSignatureHelp(help) + } + } + } + + override fun showSignatureHelp(help: SignatureHelp?) { + signatureHelpWindow.setupAndDisplay(help) + } + + override fun findDefinition() { + val languageServer = this.languageServer ?: return + val file = file ?: return + + launchAsyncWithProgress(string.msg_finding_definition) { flashbar, cancelChecker -> + val params = DefinitionParams(file.toPath(), cursorLSPPosition) + val result = languageServer.findDefinition(params, cancelChecker) + onFindDefinitionResult(flashbar, result) + } + } + + override fun findReferences() { + val languageServer = this.languageServer ?: return + val file = file ?: return + + launchAsyncWithProgress(string.msg_finding_references) { flashbar, cancelChecker -> + delay(3000L) + val params = ReferenceParams(file.toPath(), cursorLSPPosition, true) + val result = languageServer.findReferences(params, cancelChecker) + onFindReferencesResult(flashbar, result) + } + } + + override fun expandSelection() { + val languageServer = this.languageServer ?: return + val file = file ?: return + + launchAsyncWithProgress(string.please_wait) { flashbar, _ -> + val params = ExpandSelectionParams(file.toPath(), cursorLSPRange) + val result = languageServer.expandSelection(params) + + withContext(Dispatchers.Main) { + flashbar.dismiss() + setSelection(result) + } + } + } + + override fun ensureWindowsDismissed() { + if (_diagnosticWindow?.isShowing == true) { + _diagnosticWindow?.dismiss() + } + + if (_signatureHelpWindow?.isShowing == true) { + _signatureHelpWindow?.dismiss() + } + + if (_actionsMenu?.isShowing == true) { + _actionsMenu?.dismiss() + } + } + + // not overridable! + final override fun replaceComponent( + clazz: Class, + replacement: T & Any + ) { + super.replaceComponent(clazz, replacement) + } + + // not overridable + final override fun getComponent(clazz: Class): T & Any { + return super.getComponent(clazz) + } + + override fun release() { + ensureWindowsDismissed() + super.release() + + snippetController.apply { + (fileVariableResolver as? AbstractSnippetVariableResolver?)?.close() + (workspaceVariableResolver as? AbstractSnippetVariableResolver?)?.close() + + fileVariableResolver = null + workspaceVariableResolver = null + } + + _actionsMenu?.destroy() + _actionsMenu = null + languageServer = null + languageClient = null + + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } + + try { + if (editorScope.isActive) { + editorScope.cancel("Editor is releasing resources.") + } + } catch (e: Exception) { + log.error("Failed to close coroutine dispatcher", e) + } + } + + override fun getSearcher(): EditorSearcher { + return this.searcher + } + + override fun getExtraArguments(): Bundle { + return super.getExtraArguments().apply { + putString(IEditor.KEY_FILE, file?.absolutePath) + } + } + + override fun ensurePositionVisible(line: Int, column: Int, noAnimation: Boolean) { + super.ensurePositionVisible(line, column, !isEnsurePosAnimEnabled || noAnimation) + } + + override fun getTabWidth(): Int { + return tabSize + } + + override fun beginSearchMode() { + throw UnsupportedOperationException( + "Search ActionMode is not supported. Use CodeEditorView.beginSearch() instead.") + } + + override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (!gainFocus) { + ensureWindowsDismissed() + } + } + + /** + * Analyze the opened file and publish the diagnostics result. + */ + open fun analyze() { + if (editorLanguage !is IDELanguage) { + return + } + + val languageServer = languageServer ?: return + val file = file ?: return + + editorScope.launch { + val result = languageServer.analyze(file.toPath()) + languageClient?.publishDiagnostics(result) + } + } + + /** + * Mark this editor as NOT modified. + */ + open fun markUnmodified() { + this.isModified = false + } + + /** + * Mark this editor as modified. + */ + open fun markModified() { + this.isModified = true + } + + /** + * Notify the language server that the file in this editor is about to be closed. + */ + open fun notifyClose() { + file ?: run { + log.info("Cannot notify language server. File is null.") + return + } + + dispatchDocumentCloseEvent() + + _actionsMenu?.unsubscribeEvents() + selectionChangeRunner?.also { + selectionChangeHandler.removeCallbacks(it) + } + + selectionChangeRunner = null + + ensureWindowsDismissed() + } + + /** + * Called when this editor is selected and visible to the user. + */ + open fun onEditorSelected() { + file ?: return + dispatchDocumentSelectedEvent() + } + + /** + * Dispatches the [DocumentSaveEvent] for this editor. + */ + open fun dispatchDocumentSaveEvent() { + if (getFile() == null) { + return + } + val saveEvent = DocumentSaveEvent(getFile()!!.toPath()) + EventBus.getDefault().post(saveEvent) + } + + /** + * Called when the color scheme has been invalidated. This usually happens when the user reloads + * the color schemes. + */ + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN_ORDERED) + open fun onColorSchemeInvalidated(event: ColorSchemeInvalidatedEvent?) { + val file = getFile() ?: return + setupLanguage(file) + } + + /** + * Setup the editor language for the given [file]. + * + * This applies a proper [Language] and the color scheme to the editor. + */ + open fun setupLanguage(file: File?) { + if (file == null) { + return + } + val language = createLanguage(file) + val extension = file.extension + if (language is TreeSitterLanguage) { + readScheme(context, extension, + Consumer { scheme: SchemeAndroidIDE? -> + applyTreeSitterLang(language, extension, scheme) + }) + } else { + setEditorLanguage(language) + } + } + + /** + * Applies the given [TreeSitterLanguage] and the [color scheme][scheme] for the given [file type][type]. + */ + open fun applyTreeSitterLang(language: TreeSitterLanguage?, type: String, + scheme: SchemeAndroidIDE? + ) { + applyTreeSitterLang(language as Language?, type, scheme) + } + + private fun applyTreeSitterLang( + language: Language?, + type: String, + scheme: SchemeAndroidIDE? + ) { + var finalScheme = if (scheme != null) { + scheme + } else { + log.error("Failed to read current color scheme") + SchemeAndroidIDE.newInstance(context) + } + + if (finalScheme is IDEColorScheme && + finalScheme.getLanguageScheme(type) == null + ) { + log.warn("Color scheme does not support file type '$type'") + finalScheme = SchemeAndroidIDE.newInstance(context) + } + + if (finalScheme is DynamicColorScheme) { + finalScheme.apply(context) + } + + colorScheme = finalScheme!! + setEditorLanguage(language) + } + + private fun createLanguage(file: File): Language { + if (!file.isFile) { + return EmptyLanguage() + } + + val tsLang = TreeSitterLanguageProvider.forFile(file, context) + if (tsLang != null) { + return tsLang + } + + return when (FileUtils.getFileExtension(file)) { + "gradle" -> GroovyLanguage() + "c", "h", "cc", "cpp", "cxx" -> CppLanguage() + else -> EmptyLanguage() + } + } + + /** + * Update the file for this editor. This does not dispatch the [DocumentOpenEvent]. + */ + open fun updateFile(file: File?) { + this.file = file + } + + /** + * Initialize the editor. + */ + protected open fun initEditor() { + _actionsMenu = EditorActionsMenu(this).also { + it.init() + } + + searcher = IDEEditorSearcher(this) + colorScheme = SchemeAndroidIDE.newInstance(context) + inputType = createInputTypeFlags() + + val window = EditorCompletionWindow(this) + window.setAdapter(CompletionListAdapter()) + replaceComponent(EditorAutoCompletion::class.java, window) + + getComponent(EditorTextActionWindow::class.java).isEnabled = false + + subscribeEvent(ContentChangeEvent::class.java) { event, _ -> + file ?: return@subscribeEvent + + editorScope.launch { + dispatchDocumentChangeEvent(event) + checkForSignatureHelp(event) + } + } + + subscribeEvent(SelectionChangeEvent::class.java) { _, _ -> + if (_diagnosticWindow?.isShowing == true) { + _diagnosticWindow?.dismiss() + } + + selectionChangeRunner?.also { + selectionChangeHandler.removeCallbacks(it) + selectionChangeHandler.postDelayed(it, SELECTION_CHANGE_DELAY) + } + } + + EventBus.getDefault().register(this) + } + + protected open fun launchAsyncWithProgress( + message: Int, + action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit + ): Job { + val cancelChecker = object : ICancelChecker.Default() { + var job: Job? = null + + override fun cancel() { + job?.cancel("Cancelled by user") + job = null + super.cancel() + } + } + + return (context as Activity).flashProgress({ + configureFlashbar(this, string.msg_finding_definition, cancelChecker) + }) { flashbar -> + return@flashProgress editorScope.launch { + cancelChecker.job = coroutineContext[Job] + action(flashbar, cancelChecker) + } + }!! + } + + protected open suspend fun onFindDefinitionResult( + flashbar: Flashbar, + result: DefinitionResult?, + ) = withContext(Dispatchers.Main) { + + flashbar.dismiss() + + val languageClient = languageClient ?: run { + log.error("No language client found to handle the definitions result") + return@withContext + } + + if (result == null) { + log.error("Invalid definitions result from language server (null)") + flashError(string.msg_no_definition) + return@withContext + } + + val locations = result.locations + if (locations.isEmpty()) { + log.error("No definitions found") + flashError(string.msg_no_definition) + return@withContext + } + + if (locations.size != 1) { + languageClient.showLocations(locations) + return@withContext + } + + val (file1, range) = locations[0] + if (DocumentUtils.isSameFile(file1, getFile()!!.toPath())) { + setSelection(range) + return@withContext + } + + languageClient.showDocument( + ShowDocumentParams(file1, range) + ) + } + + protected open suspend fun onFindReferencesResult( + flashbar: Flashbar, + result: ReferenceResult? + ) = withContext(Dispatchers.Main) { + flashbar.dismiss() + + flashbar.dismiss() + + val languageClient = languageClient ?: run { + log.error("No language client found to handle the references result") + return@withContext + } + + if (result == null) { + log.error("Invalid references result from language server (null)") + flashError(string.msg_no_references) + return@withContext + } + + val locations = result.locations + if (locations.isEmpty()) { + log.error("No references found") + flashError(string.msg_no_references) + return@withContext + } + + if (result.locations.size == 1) { + val (file, range) = result.locations[0] + + if (DocumentUtils.isSameFile(file, getFile()!!.toPath())) { + setSelection(range) + return@withContext + } + } + + languageClient.showLocations(result.locations) + } + + protected open fun dispatchDocumentOpenEvent() { + val file = this.file ?: return + + this.fileVersion = 0 + + val openEvent = DocumentOpenEvent( + file.toPath(), + text.toString(), + fileVersion + ) + + // Notify FileManager first + onDocumentOpen(openEvent) + EventBus.getDefault().post(openEvent) + } + + protected open fun dispatchDocumentChangeEvent(event: ContentChangeEvent) { + val file = file?.toPath() ?: return + var type = ChangeType.INSERT + if (event.action == ContentChangeEvent.ACTION_DELETE) { + type = ChangeType.DELETE + } else if (event.action == ContentChangeEvent.ACTION_SET_NEW_TEXT) { + type = ChangeType.NEW_TEXT + } + var changeDelta = if (type == ChangeType.NEW_TEXT) 0 else event.changedText.length + if (type == ChangeType.DELETE) { + changeDelta = -changeDelta + } + val start = event.changeStart + val end = event.changeEnd + val changeRange = Range(Position(start.line, start.column, start.index), + Position(end.line, end.column, end.index)) + val changedText = event.changedText.toString() + val changeEvent = DocumentChangeEvent(file, changedText, text.toString(), + ++fileVersion, type, changeDelta, changeRange) + + // Notify FileManager first + onDocumentContentChange(changeEvent) + EventBus.getDefault().post(changeEvent) + } + + protected open fun dispatchDocumentSelectedEvent() { + val file = file ?: return + val selectedEvent = DocumentSelectedEvent(file.toPath()) + EventBus.getDefault().post(selectedEvent) + } + + protected open fun dispatchDocumentCloseEvent() { + val file = file ?: return + val closeEvent = DocumentCloseEvent(file.toPath(), cursorLSPRange) + + // Notify FileManager first + onDocumentClose(closeEvent) + EventBus.getDefault().post(closeEvent) + } + + /** + * Checks if the content change event should trigger signature help. Signature help trigger + * characters are : + * + * + * * `'('` (parentheses) + * * `','` (comma) + * + * + * @param event The content change event. + */ + private fun checkForSignatureHelp(event: ContentChangeEvent) { + if (languageServer == null) { + return + } + val changeLength = event.changedText.length + if (event.action != ContentChangeEvent.ACTION_INSERT || changeLength in 1..2) { + // changeLength will be 2 as '(' and ')' are inserted at the same time + return + } + + val ch = event.changedText[0] + if (ch == '(' || ch == ',') { + signatureHelp() + } + } + + private fun configureFlashbar( + builder: Flashbar.Builder, + @StringRes message: Int, + cancelChecker: ICancelChecker + ) { + builder.message(message) + .negativeActionText(android.R.string.cancel) + .negativeActionTapListener { bar: Flashbar -> + cancelChecker.cancel() + bar.dismiss() + } + } + + private suspend fun Flashbar.dismissOnUiThread() { + withContext(Dispatchers.Main) { + dismiss() + } + } +} \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java index 324433d1c..8f4215b85 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java @@ -19,20 +19,16 @@ package com.itsaky.androidide.lsp.api; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent; -import com.itsaky.androidide.projects.api.GradleProject; - import com.itsaky.androidide.projects.api.Project; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Thread-safe implementation of {@link ILanguageServerRegistry}. diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt similarity index 58% rename from lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.java rename to lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt index 43918c0cb..fc6fc3e86 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.java +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . */ - /* * This file is part of AndroidIDE. * @@ -31,83 +30,75 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.api; +package com.itsaky.androidide.lsp.api -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.itsaky.androidide.lsp.models.CodeFormatResult; -import com.itsaky.androidide.lsp.models.CompletionParams; -import com.itsaky.androidide.lsp.models.CompletionResult; -import com.itsaky.androidide.lsp.models.DefinitionParams; -import com.itsaky.androidide.lsp.models.DefinitionResult; -import com.itsaky.androidide.lsp.models.DiagnosticResult; -import com.itsaky.androidide.lsp.models.ExpandSelectionParams; -import com.itsaky.androidide.lsp.models.FormatCodeParams; -import com.itsaky.androidide.lsp.models.LSPFailure; -import com.itsaky.androidide.lsp.models.ReferenceParams; -import com.itsaky.androidide.lsp.models.ReferenceResult; -import com.itsaky.androidide.lsp.models.SignatureHelp; -import com.itsaky.androidide.lsp.models.SignatureHelpParams; -import com.itsaky.androidide.models.Range; -import com.itsaky.androidide.progress.ICancelChecker; -import com.itsaky.androidide.projects.api.Project; -import java.nio.file.Path; -import java.util.Collections; +import com.itsaky.androidide.lsp.models.CodeFormatResult +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.DiagnosticResult +import com.itsaky.androidide.lsp.models.ExpandSelectionParams +import com.itsaky.androidide.lsp.models.FormatCodeParams +import com.itsaky.androidide.lsp.models.LSPFailure +import com.itsaky.androidide.lsp.models.ReferenceParams +import com.itsaky.androidide.lsp.models.ReferenceResult +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.models.Range +import com.itsaky.androidide.progress.ICancelChecker +import com.itsaky.androidide.projects.api.Project +import java.nio.file.Path /** * A language server provides API for providing functions related to a specific file type. * * @author Akash Yadav */ -public interface ILanguageServer { +interface ILanguageServer { - /** - * Get the unique language server ID. - * - * @return The server ID. - */ - String getServerId(); + val serverId: String? /** * Called by client to notify the server to shutdown. Language servers must release all the * resources in use. * - *

After this is called, clients must re-initialize the server. + * + * After this is called, clients must re-initialize the server. */ - void shutdown(); + fun shutdown() /** * Set the client to whom notifications and events must be sent. * * @param client The client to set. */ - void connectClient(@Nullable ILanguageClient client); + fun connectClient(client: ILanguageClient?) /** * Get the instance of the language client connected to this server. * * @return The language client. */ - @Nullable - ILanguageClient getClient(); + val client: ILanguageClient? /** * Apply settings to the language server. Its up to the language server how it applies these * settings to the language service providers. * - * @param settings The new settings to use. Pass {@code null} to use default settings. + * @param settings The new settings to use. Pass `null` to use default settings. */ - void applySettings(@Nullable IServerSettings settings); + fun applySettings(settings: IServerSettings?) /** * Setup this language server with the given project. Servers are not expected to keep a reference * to the provided project. Instead, use - * {@link com.itsaky.androidide.projects.IProjectManager#getRootProject getRootProject()} to + * [getRootProject()][com.itsaky.androidide.projects.IProjectManager.rootProject] to * obtain the project instance. * * @param project The initialized project. */ - void setupWithProject(@NonNull Project project); + fun setupWithProject(project: Project) /** * Compute code completions for the given completion params. @@ -116,8 +107,7 @@ public interface ILanguageServer { * @param cancelChecker * @return The completion provider. */ - @NonNull - CompletionResult complete(CompletionParams params, ICancelChecker cancelChecker); + fun complete(params: CompletionParams?, cancelChecker: ICancelChecker?): CompletionResult /** * Find references using the given params. @@ -126,8 +116,7 @@ public interface ILanguageServer { * @param cancelChecker * @return The result of the computation. */ - @NonNull - ReferenceResult findReferences(@NonNull ReferenceParams params, ICancelChecker cancelChecker); + suspend fun findReferences(params: ReferenceParams, cancelChecker: ICancelChecker?): ReferenceResult /** * Find definition using the given params. @@ -136,8 +125,7 @@ public interface ILanguageServer { * @param cancelChecker * @return The result of the computation. */ - @NonNull - DefinitionResult findDefinition(@NonNull DefinitionParams params, ICancelChecker cancelChecker); + suspend fun findDefinition(params: DefinitionParams, cancelChecker: ICancelChecker?): DefinitionResult /** * Request the server to provide an expanded selection range for the current selection. @@ -145,8 +133,7 @@ public interface ILanguageServer { * @param params The params for computing the expanded selection range. * @return The expanded range or same selection range if computation was failed. */ - @NonNull - Range expandSelection(@NonNull ExpandSelectionParams params); + suspend fun expandSelection(params: ExpandSelectionParams): Range /** * Compute signature help with the given params. @@ -154,18 +141,16 @@ public interface ILanguageServer { * @param params The params to compute signature help. * @return The signature help. */ - @NonNull - SignatureHelp signatureHelp(@NonNull SignatureHelpParams params); + suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp /** * Analyze the given file and provide diagnostics from the analyze result. * * @param file The file to analyze. - * @return The diagnostic result. Points to {@link DiagnosticResult#NO_UPDATE} if no diagnotic + * @return The diagnostic result. Points to [DiagnosticResult.NO_UPDATE] if no diagnotic * items are available. */ - @NonNull - DiagnosticResult analyze(@NonNull Path file); + suspend fun analyze(file: Path): DiagnosticResult /** * Format the given source code input. @@ -173,18 +158,17 @@ public interface ILanguageServer { * @param params The code formatting parameters. * @return The formatted source. */ - @NonNull - default CodeFormatResult formatCode(FormatCodeParams params) { - return new CodeFormatResult(false, Collections.emptyList()); + fun formatCode(params: FormatCodeParams?): CodeFormatResult { + return CodeFormatResult(false, mutableListOf()) } /** * Handle failure caused by LSP * - * @param failure {@link LSPFailure} describing the failure. - * @return true if the failure was handled. false otherwise. + * @param failure [LSPFailure] describing the failure. + * @return `true` if the failure was handled. `false` otherwise. */ - default boolean handleFailure(LSPFailure failure) { - return false; + fun handleFailure(failure: LSPFailure?): Boolean { + return false } -} +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java deleted file mode 100644 index b90302034..000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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.lsp.java; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent; -import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent; -import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton; -import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider; -import com.itsaky.androidide.lsp.api.ILanguageClient; -import com.itsaky.androidide.lsp.api.ILanguageServer; -import com.itsaky.androidide.lsp.api.IServerSettings; -import com.itsaky.androidide.lsp.internal.model.CachedCompletion; -import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu; -import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService; -import com.itsaky.androidide.lsp.java.compiler.SourceFileManager; -import com.itsaky.androidide.lsp.java.models.JavaServerSettings; -import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider; -import com.itsaky.androidide.lsp.java.providers.CompletionProvider; -import com.itsaky.androidide.lsp.java.providers.DefinitionProvider; -import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider; -import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider; -import com.itsaky.androidide.lsp.java.providers.ReferenceProvider; -import com.itsaky.androidide.lsp.java.providers.SignatureProvider; -import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository; -import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer; -import com.itsaky.androidide.lsp.java.utils.CancelChecker; -import com.itsaky.androidide.lsp.models.CodeFormatResult; -import com.itsaky.androidide.lsp.models.CompletionParams; -import com.itsaky.androidide.lsp.models.CompletionResult; -import com.itsaky.androidide.lsp.models.DefinitionParams; -import com.itsaky.androidide.lsp.models.DefinitionResult; -import com.itsaky.androidide.lsp.models.DiagnosticResult; -import com.itsaky.androidide.lsp.models.ExpandSelectionParams; -import com.itsaky.androidide.lsp.models.FormatCodeParams; -import com.itsaky.androidide.lsp.models.LSPFailure; -import com.itsaky.androidide.lsp.models.ReferenceParams; -import com.itsaky.androidide.lsp.models.ReferenceResult; -import com.itsaky.androidide.lsp.models.SignatureHelp; -import com.itsaky.androidide.lsp.models.SignatureHelpParams; -import com.itsaky.androidide.lsp.util.LSPEditorActions; -import com.itsaky.androidide.models.Range; -import com.itsaky.androidide.progress.ICancelChecker; -import com.itsaky.androidide.projects.FileManager; -import com.itsaky.androidide.projects.IProjectManager; -import com.itsaky.androidide.projects.api.GradleProject; -import com.itsaky.androidide.projects.api.ModuleProject; -import com.itsaky.androidide.projects.api.Project; -import com.itsaky.androidide.utils.DocumentUtils; -import com.itsaky.androidide.utils.ILogger; -import com.itsaky.androidide.utils.VMUtils; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -public class JavaLanguageServer implements ILanguageServer { - - public static final String SERVER_ID = "java"; - private final CompletionProvider completionProvider; - private final JavaDiagnosticProvider diagnosticProvider; - private ILanguageClient client; - private IServerSettings settings; - private Path selectedFile; - private final AnalyzeTimer timer = new AnalyzeTimer(this::analyzeSelected); - private CachedCompletion cachedCompletion; - - private static final ILogger LOG = ILogger.newInstance("JavaLanguageServer"); - - public JavaLanguageServer() { - this.completionProvider = new CompletionProvider(); - this.diagnosticProvider = new JavaDiagnosticProvider(); - this.cachedCompletion = CachedCompletion.EMPTY; - - applySettings(getSettings()); - - if (!EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().register(this); - } - - JavaSnippetRepository.INSTANCE.init(); - } - - public IServerSettings getSettings() { - if (settings == null) { - settings = JavaServerSettings.getInstance(); - } - - return settings; - } - - @Override - public String getServerId() { - return SERVER_ID; - } - - @Override - public void shutdown() { - JavaCompilerProvider.getInstance().destroy(); - SourceFileManager.clearCache(); - CacheFSInfoSingleton.INSTANCE.clearCache(); - CachingJarFileSystemProvider.INSTANCE.clearCache(); - EventBus.getDefault().unregister(this); - - timer.cancel(); - } - - @Override - public void connectClient(@Nullable ILanguageClient client) { - this.client = client; - } - - @Nullable - @Override - public ILanguageClient getClient() { - return this.client; - } - - @Override - public void applySettings(@Nullable IServerSettings settings) { - this.settings = settings; - } - - @Override - public void setupWithProject(@NonNull final Project project) { - - LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu.INSTANCE); - - // Once we have project initialized - // Destory the NO_MODULE_COMPILER instance - JavaCompilerService.NO_MODULE_COMPILER.destroy(); - - // Clear cached file managers - SourceFileManager.clearCache(); - - // Clear cached JAR file system for R.jar - // Using the cached instance will result in completions not being updated for updated resources - // TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea - // Maybe this could be improved by using data from the AndroidModule project model - CachingJarFileSystemProvider.INSTANCE.clearCachesForPaths(path -> path.endsWith("/R.jar")); - - // Clear cached module-specific compilers - JavaCompilerProvider.getInstance().destroy(); - - // Cache classpath locations - for (final GradleProject subModule : project.getSubProjects()) { - if (!(subModule instanceof ModuleProject)) { - continue; - } - - SourceFileManager.forModule(((ModuleProject) subModule)); - } - - startOrRestartAnalyzeTimer(); - } - - @NonNull - @Override - public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) { - final JavaCompilerService compiler = getCompiler(params.getFile()); - if (compiler == null - || !settings.completionsEnabled() - || !this.completionProvider.canComplete(params.getFile())) { - return CompletionResult.EMPTY; - } - - if (diagnosticProvider.isAnalyzing()) { - LOG.warn("Cancelling source code analysis due to completion request"); - diagnosticProvider.cancel(); - } - - this.completionProvider.reset( - compiler, this.settings, this.cachedCompletion, this::updateCachedCompletion); - return this.completionProvider.complete(params); - } - - @NonNull - @Override - public ReferenceResult findReferences(@NonNull ReferenceParams params, - ICancelChecker cancelChecker) { - final JavaCompilerService compiler = getCompiler(params.getFile()); - if (!settings.referencesEnabled() || compiler == null) { - return new ReferenceResult(Collections.emptyList()); - } - - return new ReferenceProvider(compiler, cancelChecker).findReferences(params); - } - - @NonNull - @Override - public DefinitionResult findDefinition(@NonNull DefinitionParams params, - ICancelChecker cancelChecker) { - final JavaCompilerService compiler = getCompiler(params.getFile()); - if (!settings.definitionsEnabled() || compiler == null) { - return new DefinitionResult(Collections.emptyList()); - } - - return new DefinitionProvider(compiler, getSettings(), cancelChecker).findDefinition(params); - } - - @NonNull - @Override - public Range expandSelection(@NonNull ExpandSelectionParams params) { - final JavaCompilerService compiler = getCompiler(params.getFile()); - if (!settings.smartSelectionsEnabled() || compiler == null) { - return params.getSelection(); - } - - return new JavaSelectionProvider(compiler).expandSelection(params); - } - - @NonNull - @Override - public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) { - final JavaCompilerService compiler = getCompiler(params.getFile()); - if (!settings.signatureHelpEnabled() || compiler == null) { - return new SignatureHelp(Collections.emptyList(), -1, -1); - } - - return new SignatureProvider(compiler).signatureHelp(params); - } - - @NonNull - @Override - public DiagnosticResult analyze(@NonNull Path file) { - if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) { - return DiagnosticResult.NO_UPDATE; - } - - final JavaCompilerService compiler = getCompiler(file); - if (!settings.codeAnalysisEnabled() || compiler == null) { - return DiagnosticResult.NO_UPDATE; - } - - return this.diagnosticProvider.analyze(file); - } - - @NonNull - @Override - public CodeFormatResult formatCode(FormatCodeParams params) { - return new CodeFormatProvider(getSettings()).format(params); - } - - @Override - public boolean handleFailure(final LSPFailure failure) { - //noinspection SwitchStatementWithTooFewBranches - switch (failure.getType()) { - case COMPLETION: - if (CancelChecker.isCancelled(failure.getError())) { - return true; - } - - JavaCompilerProvider.getInstance().destroy(); - return true; - } - - return false; - } - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public JavaCompilerService getCompiler(Path file) { - if (!DocumentUtils.isJavaFile(file)) { - return JavaCompilerService.NO_MODULE_COMPILER; - } - - final var root = IProjectManager.getInstance().getRootProject(); - if (root == null) { - return JavaCompilerService.NO_MODULE_COMPILER; - } - - final ModuleProject module = root.findModuleForFile(file); - if (module == null) { - return JavaCompilerService.NO_MODULE_COMPILER; - } - - return JavaCompilerProvider.get(module); - } - - private void updateCachedCompletion(CachedCompletion cachedCompletion) { - Objects.requireNonNull(cachedCompletion); - this.cachedCompletion = cachedCompletion; - } - - private void startOrRestartAnalyzeTimer() { - - if (VMUtils.isJvm()) { - return; - } - - if (!this.timer.isStarted()) { - this.timer.start(); - } else { - this.timer.restart(); - } - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @SuppressWarnings("unused") - public void onContentChange(@NonNull DocumentChangeEvent event) { - if (!DocumentUtils.isJavaFile(event.getChangedFile())) { - return; - } - - // TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance - JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event); - - final ModuleProject module = IProjectManager.getInstance() - .findModuleForFile(event.getChangedFile()); - if (module != null) { - final JavaCompilerService compiler = JavaCompilerProvider.get(module); - compiler.onDocumentChange(event); - } - - startOrRestartAnalyzeTimer(); - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @SuppressWarnings("unused") - public void onFileSelected(@NonNull DocumentSelectedEvent event) { - this.selectedFile = event.getSelectedFile(); - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @SuppressWarnings("unused") - public void onFileOpened(@NonNull DocumentOpenEvent event) { - this.selectedFile = event.getOpenedFile(); - startOrRestartAnalyzeTimer(); - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @SuppressWarnings("unused") - public void onFileClosed(@NonNull DocumentCloseEvent event) { - if (this.diagnosticProvider != null) { - this.diagnosticProvider.clearTimestamp(event.getClosedFile()); - } - - if (FileManager.INSTANCE.getActiveDocumentCount() == 0) { - this.selectedFile = null; - this.timer.cancel(); - } - } - - private void analyzeSelected() { - if (this.selectedFile == null) { - return; - } - - CompletableFuture.supplyAsync(() -> analyze(selectedFile)) - .whenComplete( - ((diagnostics, throwable) -> { - if (client != null) { - client.publishDiagnostics(diagnostics); - } - })); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt new file mode 100644 index 000000000..800f29f85 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt @@ -0,0 +1,322 @@ +/* + * 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.lsp.java + +import androidx.annotation.RestrictTo +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent +import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent +import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton +import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCache +import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCachesForPaths +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.api.ILanguageServer +import com.itsaky.androidide.lsp.api.IServerSettings +import com.itsaky.androidide.lsp.internal.model.CachedCompletion +import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu +import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService +import com.itsaky.androidide.lsp.java.compiler.SourceFileManager +import com.itsaky.androidide.lsp.java.models.JavaServerSettings +import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider +import com.itsaky.androidide.lsp.java.providers.CompletionProvider +import com.itsaky.androidide.lsp.java.providers.DefinitionProvider +import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider +import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider +import com.itsaky.androidide.lsp.java.providers.ReferenceProvider +import com.itsaky.androidide.lsp.java.providers.SignatureProvider +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository.init +import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer +import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled +import com.itsaky.androidide.lsp.models.CodeFormatResult +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.DiagnosticResult +import com.itsaky.androidide.lsp.models.ExpandSelectionParams +import com.itsaky.androidide.lsp.models.FailureType +import com.itsaky.androidide.lsp.models.FormatCodeParams +import com.itsaky.androidide.lsp.models.LSPFailure +import com.itsaky.androidide.lsp.models.ReferenceParams +import com.itsaky.androidide.lsp.models.ReferenceResult +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.lsp.util.LSPEditorActions +import com.itsaky.androidide.models.Range +import com.itsaky.androidide.progress.ICancelChecker +import com.itsaky.androidide.projects.FileManager.getActiveDocumentCount +import com.itsaky.androidide.projects.IProjectManager.Companion.getInstance +import com.itsaky.androidide.projects.api.ModuleProject +import com.itsaky.androidide.projects.api.Project +import com.itsaky.androidide.utils.DocumentUtils +import com.itsaky.androidide.utils.ILogger +import com.itsaky.androidide.utils.VMUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.nio.file.Path +import java.util.Objects +import java.util.concurrent.CompletableFuture + +class JavaLanguageServer : ILanguageServer { + + private val completionProvider: CompletionProvider = CompletionProvider() + private val diagnosticProvider: JavaDiagnosticProvider? + override var client: ILanguageClient? = null + private set + + private var _settings: IServerSettings? = null + private var selectedFile: Path? = null + private val timer = AnalyzeTimer { analyzeSelected() } + private var cachedCompletion: CachedCompletion + + val settings: IServerSettings + get() { + return _settings ?: JavaServerSettings.getInstance() + .also { _settings = it } + } + + override val serverId: String = SERVER_ID + + companion object { + const val SERVER_ID = "ide.lsp.java" + private val LOG = ILogger.newInstance("JavaLanguageServer") + } + + init { + diagnosticProvider = JavaDiagnosticProvider() + cachedCompletion = CachedCompletion.EMPTY + + applySettings(JavaServerSettings.getInstance()) + + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + + init() + } + + override fun shutdown() { + JavaCompilerProvider.getInstance().destroy() + SourceFileManager.clearCache() + CacheFSInfoSingleton.clearCache() + clearCache() + EventBus.getDefault().unregister(this) + timer.cancel() + } + + override fun connectClient(client: ILanguageClient?) { + this.client = client + } + + override fun applySettings(settings: IServerSettings?) { + this._settings = settings + } + + override fun setupWithProject(project: Project) { + LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu) + + // Once we have project initialized + // Destory the NO_MODULE_COMPILER instance + JavaCompilerService.NO_MODULE_COMPILER.destroy() + + // Clear cached file managers + SourceFileManager.clearCache() + + // Clear cached JAR file system for R.jar + // Using the cached instance will result in completions not being updated for updated resources + // TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea + // Maybe this could be improved by using data from the AndroidModule project model + clearCachesForPaths { path: String -> path.endsWith("/R.jar") } + + // Clear cached module-specific compilers + JavaCompilerProvider.getInstance().destroy() + + // Cache classpath locations + for (subModule in project.subProjects) { + if (subModule !is ModuleProject) { + continue + } + SourceFileManager.forModule(subModule) + } + startOrRestartAnalyzeTimer() + } + + override fun complete(params: CompletionParams?, + cancelChecker: ICancelChecker?): CompletionResult { + val compiler = getCompiler(params!!.file) + if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file) + ) { + return CompletionResult.EMPTY + } + + if (diagnosticProvider!!.isAnalyzing()) { + LOG.warn("Cancelling source code analysis due to completion request") + diagnosticProvider.cancel() + } + + completionProvider.reset( + compiler, settings, cachedCompletion) { cachedCompletion: CachedCompletion -> + updateCachedCompletion(cachedCompletion) + } + + return completionProvider.complete(params) + } + + override suspend fun findReferences(params: ReferenceParams, + cancelChecker: ICancelChecker?): ReferenceResult { + val compiler = getCompiler(params.file) + return if (!settings.referencesEnabled()) { + ReferenceResult(emptyList()) + } else ReferenceProvider(compiler, cancelChecker).findReferences(params) + } + + override suspend fun findDefinition(params: DefinitionParams, + cancelChecker: ICancelChecker?): DefinitionResult { + val compiler = getCompiler(params.file) + return if (!settings.definitionsEnabled()) { + DefinitionResult(emptyList()) + } else DefinitionProvider(compiler, settings, cancelChecker).findDefinition(params) + } + + override suspend fun expandSelection(params: ExpandSelectionParams): Range { + val compiler = getCompiler(params.file) + return if (!settings.smartSelectionsEnabled()) { + params.selection + } else JavaSelectionProvider(compiler).expandSelection(params) + } + + override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { + val compiler = getCompiler(params.file) + return if (!settings.signatureHelpEnabled()) { + SignatureHelp(emptyList(), -1, -1) + } else SignatureProvider(compiler).signatureHelp(params) + } + + override suspend fun analyze(file: Path): DiagnosticResult { + if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) { + return DiagnosticResult.NO_UPDATE + } + + return if (!settings.codeAnalysisEnabled()) { + DiagnosticResult.NO_UPDATE + } else diagnosticProvider!!.analyze(file) + } + + override fun formatCode(params: FormatCodeParams?): CodeFormatResult { + return CodeFormatProvider(settings).format(params) + } + + override fun handleFailure(failure: LSPFailure?): Boolean { + return when (failure!!.type) { + FailureType.COMPLETION -> { + if (isCancelled(failure.error)) { + return true + } + JavaCompilerProvider.getInstance().destroy() + true + } + } + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getCompiler(file: Path?): JavaCompilerService { + if (!DocumentUtils.isJavaFile(file)) { + return JavaCompilerService.NO_MODULE_COMPILER + } + val root = getInstance().rootProject + ?: return JavaCompilerService.NO_MODULE_COMPILER + val module = root.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER + return JavaCompilerProvider.get(module) + } + + private fun updateCachedCompletion(cachedCompletion: CachedCompletion) { + Objects.requireNonNull(cachedCompletion) + this.cachedCompletion = cachedCompletion + } + + private fun startOrRestartAnalyzeTimer() { + if (VMUtils.isJvm()) { + return + } + if (!timer.isStarted) { + timer.start() + } else { + timer.restart() + } + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onContentChange(event: DocumentChangeEvent) { + if (!DocumentUtils.isJavaFile(event.changedFile)) { + return + } + + // TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance + JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event) + val module = getInstance() + .findModuleForFile(event.changedFile) + if (module != null) { + val compiler = JavaCompilerProvider.get(module) + compiler.onDocumentChange(event) + } + startOrRestartAnalyzeTimer() + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onFileSelected(event: DocumentSelectedEvent) { + selectedFile = event.selectedFile + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onFileOpened(event: DocumentOpenEvent) { + selectedFile = event.openedFile + startOrRestartAnalyzeTimer() + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onFileClosed(event: DocumentCloseEvent) { + diagnosticProvider?.clearTimestamp(event.closedFile) + + if (getActiveDocumentCount() == 0) { + selectedFile = null + timer.cancel() + } + } + + private fun analyzeSelected() { + if (selectedFile == null || client == null) { + return + } + + CoroutineScope(Dispatchers.Default).launch { + val result = analyze(selectedFile!!) + withContext(Dispatchers.Main) { + client?.publishDiagnostics(result) + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt index 3e5fb5be4..58d8f7408 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt @@ -29,7 +29,7 @@ import org.junit.Ignore object JavaLSPTest : LSPTest() { val server by lazy { - ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.serverId) as JavaLanguageServer } @@ -43,7 +43,7 @@ object JavaLSPTest : LSPTest() { ILanguageServerRegistry.getDefault().register(JavaLanguageServer()) } - override fun getServerId() = JavaLanguageServer.SERVER_ID + override fun getServerId() = JavaLanguageServer.serverId fun getCompiler(): JavaCompilerService { return JavaCompilerProvider.get(findAppModule()!!) diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.java deleted file mode 100644 index 2f3c238ee..000000000 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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.lsp.xml; - -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; - -import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent; -import com.itsaky.androidide.lsp.api.ICompletionProvider; -import com.itsaky.androidide.lsp.api.ILanguageClient; -import com.itsaky.androidide.lsp.api.ILanguageServer; -import com.itsaky.androidide.lsp.api.IServerSettings; -import com.itsaky.androidide.lsp.models.CodeFormatResult; -import com.itsaky.androidide.lsp.models.CompletionParams; -import com.itsaky.androidide.lsp.models.CompletionResult; -import com.itsaky.androidide.lsp.models.DefinitionParams; -import com.itsaky.androidide.lsp.models.DefinitionResult; -import com.itsaky.androidide.lsp.models.DiagnosticResult; -import com.itsaky.androidide.lsp.models.ExpandSelectionParams; -import com.itsaky.androidide.lsp.models.FormatCodeParams; -import com.itsaky.androidide.lsp.models.ReferenceParams; -import com.itsaky.androidide.lsp.models.ReferenceResult; -import com.itsaky.androidide.lsp.models.SignatureHelp; -import com.itsaky.androidide.lsp.models.SignatureHelpParams; -import com.itsaky.androidide.lsp.util.NoCompletionsProvider; -import com.itsaky.androidide.lsp.xml.models.XMLServerSettings; -import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider; -import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider; -import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider; -import com.itsaky.androidide.models.Range; -import com.itsaky.androidide.progress.ICancelChecker; - -import com.itsaky.androidide.projects.api.Project; -import com.itsaky.androidide.utils.DocumentUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.Collections; - -/** - * Language server implementation for XML files. - * - * @author Akash Yadav - */ -public class XMLLanguageServer implements ILanguageServer { - - public static final String SERVER_ID = "xml"; - - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - private ILanguageClient client; - - private IServerSettings settings; - - public XMLLanguageServer() { - EventBus.getDefault().register(this); - } - - @Override - public String getServerId() { - return SERVER_ID; - } - - @Override - public void shutdown() { - if (EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().unregister(this); - } - } - - @Override - public void connectClient(ILanguageClient client) { - this.client = client; - } - - @Nullable - @Override - public ILanguageClient getClient() { - return this.client; - } - - @Override - public void applySettings(IServerSettings settings) { - this.settings = settings; - } - - @Override - public void setupWithProject(@NonNull final Project project) {} - - @NonNull - @Override - public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) { - final ICompletionProvider completionProvider; - if (!getSettings().completionsEnabled()) { - completionProvider = new NoCompletionsProvider(); - } else { - completionProvider = new XmlCompletionProvider(this.getSettings()); - } - return completionProvider.complete(params); - } - - @NonNull - public IServerSettings getSettings() { - if (settings == null) { - settings = XMLServerSettings.INSTANCE; - } - - return settings; - } - - @NonNull - @Override - public ReferenceResult findReferences(@NonNull ReferenceParams params, - ICancelChecker cancelChecker) { - return new ReferenceResult(Collections.emptyList()); - } - - @NonNull - @Override - public DefinitionResult findDefinition(@NonNull DefinitionParams params, - ICancelChecker cancelChecker) { - return new DefinitionResult(Collections.emptyList()); - } - - @NonNull - @Override - public Range expandSelection(@NonNull ExpandSelectionParams params) { - return params.getSelection(); - } - - @NonNull - @Override - public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) { - return new SignatureHelp(Collections.emptyList(), -1, -1); - } - - @NonNull - @Override - public DiagnosticResult analyze(@NonNull Path file) { - return DiagnosticResult.NO_UPDATE; - } - - @NonNull - @Override - public CodeFormatResult formatCode(FormatCodeParams params) { - return new CodeFormatProvider().format(params); - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onDocumentChange(DocumentChangeEvent event) { - if (!DocumentUtils.isXmlFile(event.getChangedFile())) { - return; - } - - AdvancedEditProvider.INSTANCE.onContentChange(event); - } -} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.kt new file mode 100644 index 000000000..d1bbb8905 --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/XMLLanguageServer.kt @@ -0,0 +1,146 @@ +/* + * 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.lsp.xml + +import androidx.annotation.RestrictTo +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.lsp.api.ICompletionProvider +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.api.ILanguageServer +import com.itsaky.androidide.lsp.api.IServerSettings +import com.itsaky.androidide.lsp.models.CodeFormatResult +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.DiagnosticResult +import com.itsaky.androidide.lsp.models.ExpandSelectionParams +import com.itsaky.androidide.lsp.models.FormatCodeParams +import com.itsaky.androidide.lsp.models.LSPFailure +import com.itsaky.androidide.lsp.models.ReferenceParams +import com.itsaky.androidide.lsp.models.ReferenceResult +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.lsp.util.NoCompletionsProvider +import com.itsaky.androidide.lsp.xml.models.XMLServerSettings +import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider.onContentChange +import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider +import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider +import com.itsaky.androidide.models.Range +import com.itsaky.androidide.progress.ICancelChecker +import com.itsaky.androidide.projects.api.Project +import com.itsaky.androidide.utils.DocumentUtils +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.nio.file.Path + +/** + * Language server implementation for XML files. + * + * @author Akash Yadav + */ +class XMLLanguageServer : ILanguageServer { + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + override var client: ILanguageClient? = null + private set + + private var settings: IServerSettings? = null + + override val serverId: String = SERVER_ID + + init { + EventBus.getDefault().register(this) + } + + override fun shutdown() { + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } + } + + override fun connectClient(client: ILanguageClient?) { + this.client = client + } + + override fun applySettings(settings: IServerSettings?) { + this.settings = settings + } + + override fun setupWithProject(project: Project) {} + override fun complete(params: CompletionParams?, + cancelChecker: ICancelChecker?): CompletionResult { + val completionProvider: ICompletionProvider + completionProvider = if (!getSettings().completionsEnabled()) { + NoCompletionsProvider() + } else { + XmlCompletionProvider(getSettings()) + } + return completionProvider.complete(params) + } + + fun getSettings(): IServerSettings { + if (settings == null) { + settings = XMLServerSettings + } + return settings!! + } + + override suspend fun findReferences(params: ReferenceParams, + cancelChecker: ICancelChecker?): ReferenceResult { + return ReferenceResult(emptyList()) + } + + override suspend fun findDefinition(params: DefinitionParams, + cancelChecker: ICancelChecker?): DefinitionResult { + return DefinitionResult(emptyList()) + } + + override suspend fun expandSelection(params: ExpandSelectionParams): Range { + return params.selection + } + + override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { + return SignatureHelp(emptyList(), -1, -1) + } + + override suspend fun analyze(file: Path): DiagnosticResult { + return DiagnosticResult.NO_UPDATE + } + + override fun formatCode(params: FormatCodeParams?): CodeFormatResult { + return CodeFormatProvider().format(params) + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onDocumentChange(event: DocumentChangeEvent) { + if (!DocumentUtils.isXmlFile(event.changedFile)) { + return + } + onContentChange(event) + } + + override fun handleFailure(failure: LSPFailure?): Boolean { + return super.handleFailure(failure) + } + + companion object { + + const val SERVER_ID = "ide.lsp.xml" + } +} \ No newline at end of file diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt index 89ef6b248..1ad06910c 100644 --- a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt @@ -79,7 +79,7 @@ object XMLLSPTest : LSPTest() { } override fun getServerId(): String { - return XMLLanguageServer.SERVER_ID + return XMLLanguageServer.serverId } override fun test() {}