diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt index f067ea64a..60c240499 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt @@ -41,9 +41,7 @@ abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditH index: Int ) { val compiler = JavaCompilerProvider.get(ProjectManager.findModuleForFile(file) ?: return) - if (compiler != null) { - performEdits(compiler, editor, item) - } + performEdits(compiler, editor, item) executeCommand(editor, item.command) } 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 a95984766..bdf870a0f 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 @@ -24,6 +24,7 @@ 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.providers.snippet.JavaSnippet import com.itsaky.androidide.lsp.java.utils.EditHelper import com.itsaky.androidide.lsp.models.ClassCompletionData import com.itsaky.androidide.lsp.models.Command @@ -42,6 +43,7 @@ import com.itsaky.androidide.lsp.models.ICompletionData import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET import com.itsaky.androidide.lsp.models.MatchLevel import com.itsaky.androidide.lsp.models.MethodCompletionData +import com.itsaky.androidide.preferences.utils.indentationString import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled import com.itsaky.androidide.utils.ILogger import java.nio.file.Path @@ -291,6 +293,23 @@ abstract class IJavaCompletionProvider( } } + protected open fun snippetItem(snippet: JavaSnippet, matchLevel: MatchLevel, partial: String, indent: Int) : CompletionItem { + return JavaCompletionItem().apply { + this.label = snippet.label + this.detail = snippet.description + this.completionKind = CompletionItemKind.SNIPPET + this.matchLevel = matchLevel + this.ideSortText = snippet.prefix + this.snippetDescription = describeSnippet(snippet.prefix) + + val indentation = indentationString(indent) + this.insertTextFormat = SNIPPET + this.insertText = snippet.content.joinToString(separator = "\n") { + indentation + it.replace("\t", indentationString) + } + } + } + protected open fun kind(e: Element): CompletionItemKind { abortIfCancelled() abortCompletionIfCancelled() diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt index 38127841a..ff1393fe6 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt @@ -23,8 +23,8 @@ import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled -import openjdk.source.util.TreePath import java.nio.file.Path +import openjdk.source.util.TreePath /** @author Akash Yadav */ class IdentifierCompletionProvider( @@ -44,6 +44,12 @@ class IdentifierCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() + + val snippets = + SnippetCompletionProvider(cursor, file, compiler, settings) + .complete(task, path, partial, endsWithParen) + list.addAll(snippets.items) + val scopeMembers = ScopeCompletionProvider(file, cursor, compiler, settings) .complete(task, path, partial, endsWithParen) @@ -52,13 +58,7 @@ class IdentifierCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() val staticImports = - StaticImportCompletionProvider( - file, - cursor, - compiler, - settings, - path.compilationUnit - ) + StaticImportCompletionProvider(file, cursor, compiler, settings, path.compilationUnit) .complete(task, path, partial, endsWithParen) list.addAll(staticImports.items) @@ -68,13 +68,7 @@ class IdentifierCompletionProvider( abortIfCancelled() abortCompletionIfCancelled() val classNames = - ClassNamesCompletionProvider( - file, - cursor, - compiler, - settings, - path.compilationUnit - ) + ClassNamesCompletionProvider(file, cursor, compiler, settings, path.compilationUnit) .complete(task, path, partial, endsWithParen) list.addAll(classNames.items) } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt new file mode 100644 index 000000000..33b63bf1b --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt @@ -0,0 +1,114 @@ +/* + * 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.providers.completion + +import com.itsaky.androidide.lsp.api.IServerSettings +import com.itsaky.androidide.lsp.java.compiler.CompileTask +import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippet +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetScope +import com.itsaky.androidide.lsp.java.utils.EditHelper +import com.itsaky.androidide.lsp.models.CompletionItem +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.MatchLevel +import java.nio.file.Path +import openjdk.source.tree.ClassTree +import openjdk.source.tree.CompilationUnitTree +import openjdk.source.tree.MethodTree +import openjdk.source.util.TreePath + +/** + * Provides snippet completion for Java files. + * + * @author Akash Yadav + */ +class SnippetCompletionProvider( + cursor: Long, + completingFile: Path, + compiler: JavaCompilerService, + settings: IServerSettings +) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) { + + override fun doComplete( + task: CompileTask, + path: TreePath, + partial: String, + endsWithParen: Boolean + ): CompletionResult { + val scope = findSnippetScope(path) ?: return CompletionResult.EMPTY + return when (scope.leaf) { + is CompilationUnitTree -> completeTopLevelSnippets(task, path, partial) + is ClassTree -> completeMemberSnippets(task, path, partial) + is MethodTree -> completeLocalSnippets(task, path, partial) + else -> CompletionResult.EMPTY + } + } + + private fun completeTopLevelSnippets( + task: CompileTask, + path: TreePath, + partial: String + ): CompletionResult { + return CompletionResult.EMPTY + } + + private fun completeMemberSnippets( + task: CompileTask, + path: TreePath, + partial: String + ): CompletionResult { + return CompletionResult.EMPTY + } + + private fun completeLocalSnippets( + task: CompileTask, + path: TreePath, + partial: String + ): CompletionResult { + val items = mutableListOf() + val snippets = + mutableListOf().apply { + JavaSnippetRepository.snippets[JavaSnippetScope.LOCAL]?.let { addAll(it) } + JavaSnippetRepository.snippets[JavaSnippetScope.GLOBAL]?.let { addAll(it) } + } + + val indent = EditHelper.indent(task.task, task.root(), path.leaf) + for (snippet in snippets) { + val matchLevel = matchLevel(snippet.prefix, partial) + if (matchLevel == MatchLevel.NO_MATCH) { + continue + } + + items.add(snippetItem(snippet, matchLevel, partial, indent)) + } + + return CompletionResult(items) + } + + private fun findSnippetScope(path: TreePath?): TreePath? { + var treePath = path + while (treePath != null) { + if (treePath.leaf.let { it is CompilationUnitTree || it is ClassTree || it is MethodTree }) { + return treePath + } + treePath = treePath.parentPath + } + return null + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt new file mode 100644 index 000000000..71e264749 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt @@ -0,0 +1,58 @@ +/* + * 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.providers.snippet + +/** + * Java snippet. + * + * @author Akash Yadav + */ +data class JavaSnippet( + val prefix: String, + val label: String, + val description: String, + val content: Array +) { + + constructor( + prefix: String, + label: String, + description: String, + content: () -> Array + ) : this(prefix, label, description, content()) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JavaSnippet) return false + + if (label != other.label) return false + if (description != other.description) return false + if (prefix != other.prefix) return false + if (!content.contentEquals(other.content)) return false + + return true + } + + override fun hashCode(): Int { + var result = label.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + prefix.hashCode() + result = 31 * result + content.contentHashCode() + return result + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt new file mode 100644 index 000000000..2cf7ea7e0 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt @@ -0,0 +1,60 @@ +/* + * 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.providers.snippet + +/** + * Repository to store various snippets for Java. + * + * @author Akash Yadav + */ +object JavaSnippetRepository { + + val snippets: Map> by lazy { createSnippets() } + + private fun createSnippets(): Map> { + return mutableMapOf>().apply { + this[JavaSnippetScope.GLOBAL] = createGlobalSnippets() + this[JavaSnippetScope.TOP_LEVEL] = createTopLevelSnippets() + this[JavaSnippetScope.MEMBER] = createMemberSnippets() + this[JavaSnippetScope.LOCAL] = createLocalSnippets() + } + } + + private fun createGlobalSnippets(): List { + return listOf() + } + + private fun createTopLevelSnippets(): List { + return listOf() + } + + private fun createMemberSnippets(): List { + return listOf() + } + + private fun createLocalSnippets(): List { + return listOf( + JavaSnippet("for", "Indexed for loop", "0 -> count") { + arrayOf("for (int \${1:i} = 0; \$1 < \${2:count}; ++\$1) {", "\t\$0", "}") + }, + JavaSnippet("forr", "Reverse-indexed for loop", "count-1 -> 0") { + arrayOf("for (int \${1:i} = \${2:count} - 1; \$1 >= 0; --\$1) {", "\t\$0", "}") + } + ) + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt new file mode 100644 index 000000000..e9bf1850c --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt @@ -0,0 +1,41 @@ +/* + * 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.providers.snippet + +/** + * Scope for [JavaSnippet]. + * + * @author Akash Yadav + */ +enum class JavaSnippetScope { + + /** + * Snippets that can be used at the top level. This includes snippes such as class, interface, + * enum templates. + */ + TOP_LEVEL, + + /** Snippets that can be used at the member level i.e. inside a class tree. */ + MEMBER, + + /** Snippets that can be used at a local level. E.g. inside a method or constructor. */ + LOCAL, + + /** Snippets that can be used anywhere in the code, irrespective of the current scope. */ + GLOBAL +}