feat(lsp/java): add basic snippet support for Java

This commit is contained in:
Akash Yadav
2023-03-31 17:37:48 +05:30
parent e8e203f548
commit 5be28d078e
7 changed files with 302 additions and 18 deletions
@@ -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)
}
@@ -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()
@@ -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)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<CompletionItem>()
val snippets =
mutableListOf<JavaSnippet>().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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>
) {
constructor(
prefix: String,
label: String,
description: String,
content: () -> Array<String>
) : 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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.providers.snippet
/**
* Repository to store various snippets for Java.
*
* @author Akash Yadav
*/
object JavaSnippetRepository {
val snippets: Map<JavaSnippetScope, List<JavaSnippet>> by lazy { createSnippets() }
private fun createSnippets(): Map<JavaSnippetScope, List<JavaSnippet>> {
return mutableMapOf<JavaSnippetScope, List<JavaSnippet>>().apply {
this[JavaSnippetScope.GLOBAL] = createGlobalSnippets()
this[JavaSnippetScope.TOP_LEVEL] = createTopLevelSnippets()
this[JavaSnippetScope.MEMBER] = createMemberSnippets()
this[JavaSnippetScope.LOCAL] = createLocalSnippets()
}
}
private fun createGlobalSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createTopLevelSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createMemberSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createLocalSnippets(): List<JavaSnippet> {
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", "}")
}
)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}