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 index 8261fb02b..554c2ec4a 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java @@ -235,7 +235,7 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { languageServer.signatureHelp( new SignatureHelpParams( getFile().toPath(), - new Position(getCursor().getLeftLine(), getCursor().getLeftColumn())))); + getCursorLSPPosition()))); future.whenComplete( (help, error) -> { @@ -743,8 +743,10 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { * @param event The content change event. */ private void checkForSignatureHelp(@NonNull ContentChangeEvent event) { + final var changeLength = event.getChangedText().length(); if (event.getAction() != ContentChangeEvent.ACTION_INSERT - || event.getChangedText().length() != 1) { + || changeLength > 0 + && changeLength <= 2) { // changeLength will be 2 as '(' and ')' are inserted at the same time return; } @@ -880,7 +882,7 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { @Override @SuppressWarnings("unused") - public void executeCommand(Command command) { + public void executeCommand(@Nullable Command command) { if (command == null) { LOG.warn("Cannot execute command in editor. Command is null."); return; diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/helpers.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/helpers.kt index 406b2a2e6..398b40b97 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/helpers.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/helpers.kt @@ -18,5 +18,12 @@ package com.itsaky.androidide.lsp.api import com.itsaky.androidide.lsp.models.SnippetDescription +import io.github.rosemoe.sora.lang.completion.snippet.CodeSnippet -fun describeSnippet(prefix: String) = SnippetDescription(prefix.length) +@JvmOverloads +fun describeSnippet( + prefix: String, + deleteSelected: Boolean = true, + snippet: CodeSnippet? = null, + allowCommandExecution: Boolean = false +) = SnippetDescription(prefix.length, deleteSelected, snippet, allowCommandExecution) diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/IJavaEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt similarity index 86% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/IJavaEditHandler.kt rename to lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt index ed0d8199d..f067ea64a 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/IJavaEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt @@ -17,7 +17,6 @@ package com.itsaky.androidide.lsp.java.edits -import com.itsaky.androidide.lsp.edits.IEditHandler import com.itsaky.androidide.lsp.java.JavaCompilerProvider import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.lsp.models.CompletionItem @@ -31,13 +30,22 @@ import java.nio.file.Path * * @author Akash Yadav */ -abstract class IJavaEditHandler(protected val file: Path) : IEditHandler { +abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditHandler() { - override fun performEdits(item: CompletionItem, editor: CodeEditor, text: Content, line: Int, column: Int, index: Int) { + override fun performEdits( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { val compiler = JavaCompilerProvider.get(ProjectManager.findModuleForFile(file) ?: return) if (compiler != null) { performEdits(compiler, editor, item) } + + executeCommand(editor, item.command) } /** diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt new file mode 100644 index 000000000..71db6f6b0 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt @@ -0,0 +1,40 @@ +/* + * 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.edits + +import com.itsaky.androidide.editor.api.ILspEditor +import com.itsaky.androidide.lsp.edits.DefaultEditHandler +import com.itsaky.androidide.lsp.models.Command +import io.github.rosemoe.sora.widget.CodeEditor + +/** + * Implementation of [DefaultEditHandler] which avoids reflection in + * [DefaultEditHandler.executeCommand]. + * + * @author Akash Yadav + */ +open class BaseJavaEditHandler : DefaultEditHandler() { + + override fun executeCommand(editor: CodeEditor, command: Command?) { + if (editor is ILspEditor) { + editor.executeCommand(command) + return + } + super.executeCommand(editor, command) + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt index b9665e9ea..1f73882dc 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt @@ -32,7 +32,7 @@ import java.nio.file.Path * @param file The file in which this edit will be performed. * @author Akash Yadav */ -class ClassImportEditHandler(val imports: Set, file: Path) : IJavaEditHandler(file) { +class ClassImportEditHandler(val imports: Set, file: Path) : AdvancedJavaEditHandler(file) { override fun performEdits( compiler: JavaCompilerService, diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt index 90c3e118a..02da367fe 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt @@ -34,7 +34,7 @@ class MultipleClassImportEditHandler( private val classes: Set, private val imported: Set, file: Path -) : IJavaEditHandler(file) { +) : AdvancedJavaEditHandler(file) { private val log = ILogger.newInstance(javaClass.simpleName) diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt new file mode 100644 index 000000000..af0ea8520 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt @@ -0,0 +1,77 @@ +/* + * 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.models + +import com.itsaky.androidide.lsp.edits.IEditHandler +import com.itsaky.androidide.lsp.java.edits.BaseJavaEditHandler +import com.itsaky.androidide.lsp.models.Command +import com.itsaky.androidide.lsp.models.CompletionItem +import com.itsaky.androidide.lsp.models.CompletionItemKind +import com.itsaky.androidide.lsp.models.ICompletionData +import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.lsp.models.MatchLevel +import com.itsaky.androidide.lsp.models.TextEdit + +/** + * Completion item model for java completion items. + * + * @author Akash Yadav + */ +class JavaCompletionItem( + label: String, + detail: String, + insertText: String?, + insertTextFormat: InsertTextFormat?, + sortText: String?, + command: Command?, + kind: CompletionItemKind, + matchLevel: MatchLevel, + additionalTextEdits: List?, + data: ICompletionData?, + + // Override the default edit handler + editHandler: IEditHandler = BaseJavaEditHandler() +) : + CompletionItem( + label, + detail, + insertText, + insertTextFormat, + sortText, + command, + kind, + matchLevel, + additionalTextEdits, + data, + editHandler + ) { + + constructor() : + this( + "", // label + "", // detail + null, // insertText + null, // insertTextFormat + null, // sortText + null, // command + CompletionItemKind.NONE, // kind + MatchLevel.NO_MATCH, // match level + ArrayList(), // additionalEdits + null // data + ) +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt index 6e09f5cbe..ca993648c 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt @@ -22,6 +22,7 @@ import com.itsaky.androidide.lsp.api.describeSnippet import com.itsaky.androidide.lsp.java.compiler.CompileTask import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler +import com.itsaky.androidide.lsp.java.models.JavaCompletionItem import com.itsaky.androidide.lsp.java.providers.BaseJavaServiceProvider import com.itsaky.androidide.lsp.java.utils.EditHelper import com.itsaky.androidide.lsp.models.ClassCompletionData @@ -147,7 +148,7 @@ abstract class IJavaCompletionProvider( ): CompletionItem { abortIfCancelled() abortCompletionIfCancelled() - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(keyword) item.kind = KEYWORD item.detail = "keyword" @@ -166,7 +167,7 @@ abstract class IJavaCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() val first = overloads[0] - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(first.simpleName.toString()) item.kind = CompletionItemKind.METHOD item.detail = printMethodDetail(first) @@ -186,7 +187,7 @@ abstract class IJavaCompletionProvider( item.command = Command("Trigger Parameter Hints", Command.TRIGGER_PARAMETER_HINTS) } item.insertTextFormat = SNIPPET // Snippet - item.snippetDescription = describeSnippet(partial) + item.snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true) } return item } @@ -217,7 +218,7 @@ abstract class IJavaCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(element.simpleName.toString()) item.kind = kind(element) item.detail = element.toString() @@ -247,7 +248,7 @@ abstract class IJavaCompletionProvider( ): CompletionItem { abortIfCancelled() abortCompletionIfCancelled() - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(simpleName(className).toString()) item.kind = CompletionItemKind.CLASS item.detail = packageName(className).toString() @@ -280,7 +281,7 @@ abstract class IJavaCompletionProvider( if (packageName == name) { packageName = " " } - return CompletionItem().apply { + return JavaCompletionItem().apply { setLabel(simpleName) this.detail = packageName this.insertText = simpleName diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt index 77c0d189d..cb473db2f 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt @@ -22,6 +22,7 @@ import com.itsaky.androidide.lsp.api.describeSnippet import com.itsaky.androidide.lsp.java.compiler.CompileTask import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.lsp.java.edits.MultipleClassImportEditHandler +import com.itsaky.androidide.lsp.java.models.JavaCompletionItem import com.itsaky.androidide.lsp.java.utils.EditHelper import com.itsaky.androidide.lsp.java.utils.EditHelper.repeatSpaces import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils.Companion.buildMethod @@ -172,7 +173,7 @@ class ScopeCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(methodSpec.name) item.kind = com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD item.detail = method.returnType.toString() + " " + method diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/TopLevelSnippetsProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/TopLevelSnippetsProvider.kt index 9bdac931a..99788f55f 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/TopLevelSnippetsProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/TopLevelSnippetsProvider.kt @@ -20,7 +20,7 @@ package com.itsaky.androidide.lsp.java.providers.completion import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.api.ICompletionCancelChecker import com.itsaky.androidide.lsp.api.describeSnippet -import com.itsaky.androidide.lsp.java.parser.ParseTask +import com.itsaky.androidide.lsp.java.models.JavaCompletionItem import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionItemKind.SNIPPET import com.itsaky.androidide.lsp.models.CompletionResult @@ -83,7 +83,7 @@ class TopLevelSnippetsProvider { ): CompletionItem { abortIfCancelled() abortCompletionIfCancelled() - val item = CompletionItem() + val item = JavaCompletionItem() item.setLabel(label) item.kind = SNIPPET item.insertText = snippet diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt index 1023056a7..83558884a 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt @@ -53,7 +53,7 @@ open class DefaultEditHandler : IEditHandler { performEditsInternal(item, editor, text, line, column, index) } - private fun performEditsInternal( + protected open fun performEditsInternal( item: CompletionItem, editor: CodeEditor, text: Content, @@ -89,23 +89,27 @@ open class DefaultEditHandler : IEditHandler { column: Int, index: Int ) { - item.snippetDescription!! + val snippetDescription = item.snippetDescription!! val snippet = CodeSnippetParser.parse(item.insertText) - val prefixLength = item.snippetDescription!!.selectedLength + val prefixLength = snippetDescription.selectedLength val selectedText = text.subSequence(index - prefixLength, index).toString() var actionIndex = index - if (item.snippetDescription!!.deleteSelected) { + if (snippetDescription.deleteSelected) { text.delete(index - prefixLength, index) actionIndex -= prefixLength } editor.snippetController.startSnippet(actionIndex, snippet, selectedText) + + if (snippetDescription.allowCommandExecution) { + executeCommand(editor, item.command) + } } protected open fun executeCommand(editor: CodeEditor, command: Command?) { if (command == null) { return } - + try { val klass = editor::class.java val method = klass.getMethod("executeCommand", Command::class.java) diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt index 538b6d0b4..7975752b8 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt @@ -124,7 +124,8 @@ open class CompletionItem( var kind: CompletionItemKind, var matchLevel: MatchLevel, var additionalTextEdits: List?, - var data: ICompletionData? + var data: ICompletionData?, + var editHandler: IEditHandler = DefaultEditHandler() ) : io.github.rosemoe.sora.lang.completion.CompletionItem(label, detail), Comparable { @@ -147,7 +148,6 @@ open class CompletionItem( } var insertTextFormat: InsertTextFormat = insertTextFormat ?: PLAIN_TEXT - var editHandler: IEditHandler = DefaultEditHandler() var additionalEditHandler: IEditHandler? = null var snippetDescription: SnippetDescription? = null var overrideTypeText: String? = null @@ -268,7 +268,8 @@ data class SnippetDescription constructor( val selectedLength: Int, val deleteSelected: Boolean = true, - val snippet: CodeSnippet? = null + val snippet: CodeSnippet? = null, + val allowCommandExecution: Boolean = false ) data class Command(var title: String, var command: String) { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/AttrValueEditHandler.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/AttrValueEditHandler.kt index 7ec7b7777..f0dae9283 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/AttrValueEditHandler.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/AttrValueEditHandler.kt @@ -17,16 +17,14 @@ package com.itsaky.androidide.lsp.xml.edits -import com.itsaky.androidide.lsp.edits.DefaultEditHandler - /** * Handles insertion of attribute values. * * @author Akash Yadav */ class AttrValueEditHandler : QualifiedValueEditHandler() { - + override fun isPartialPart(c: Char): Boolean { return super.isPartialPart(c) || c in charArrayOf('@', '.', ':', '/') } -} \ No newline at end of file +} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/QualifiedValueEditHandler.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/QualifiedValueEditHandler.kt index 2f2e34fe2..1d9ecc8fa 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/QualifiedValueEditHandler.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/edits/QualifiedValueEditHandler.kt @@ -18,10 +18,7 @@ package com.itsaky.androidide.lsp.xml.edits import com.itsaky.androidide.lsp.edits.DefaultEditHandler -import com.itsaky.androidide.lsp.edits.IEditHandler import com.itsaky.androidide.lsp.models.CompletionItem -import io.github.rosemoe.sora.text.Content -import io.github.rosemoe.sora.widget.CodeEditor /** * Handles edits for attribute values with qualified binary names. The default implementation in @@ -30,7 +27,7 @@ import io.github.rosemoe.sora.widget.CodeEditor * @author Akash Yadav */ open class QualifiedValueEditHandler : DefaultEditHandler() { - + override fun isPartialPart(c: Char): Boolean { return super.isPartialPart(c) || c == '.' // Tags can contain '.' as well }