Added: Long click java completion item for documentation

This commit is contained in:
Akash Yadav
2022-10-28 22:55:15 +05:30
parent 5c10cd56d4
commit b16dc13156
11 changed files with 317 additions and 111 deletions
@@ -28,17 +28,21 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import com.itsaky.androidide.databinding.LayoutCompletionItemBinding
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.CompletionItemKind.CLASS
import com.itsaky.androidide.lsp.models.CompletionItemKind.CONSTRUCTOR
import com.itsaky.androidide.lsp.models.CompletionItemKind.ENUM
import com.itsaky.androidide.lsp.models.CompletionItemKind.FIELD
import com.itsaky.androidide.lsp.models.CompletionItemKind.INTERFACE
import com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD
import com.itsaky.androidide.lsp.models.MemberCompletionData
import com.itsaky.androidide.lsp.models.MethodCompletionData
import com.itsaky.androidide.resources.R.color
import com.itsaky.androidide.resources.R.string.msg_api_info_deprecated
import com.itsaky.androidide.resources.R.string.msg_api_info_removed
import com.itsaky.androidide.resources.R.string.msg_api_info_since
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.TypefaceUtils
import com.itsaky.androidide.xml.versions.ApiVersions
import com.itsaky.androidide.xml.versions.Info
@@ -48,6 +52,9 @@ import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter
import com.itsaky.androidide.lsp.models.CompletionItem as LspCompletionItem
class CompletionListAdapter : EditorCompletionAdapter() {
private val log = ILogger.newInstance("CompletionListAdapter")
override fun attachValues(window: EditorAutoCompletion, items: List<CompletionItem>) {
super.attachValues(window, items)
}
@@ -105,32 +112,34 @@ class CompletionListAdapter : EditorCompletionAdapter() {
}
val data = item.data
if (data?.className == null) {
return@executeAsync null
}
val versions =
Lookup.DEFAULT.lookup(ApiVersions.COMPLETION_LOOKUP_KEY) ?: return@executeAsync null
val className = data.className
val className =
when (data) {
is ClassCompletionData -> data.className
is MemberCompletionData -> data.classInfo.className
else -> return@executeAsync null
}
val kind = item.kind
val clazz = versions.getClass(className) ?: return@executeAsync null
var info: Info? = clazz
if (
kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank()
) {
val method = clazz.getMethod(data.memberName, *data.erasedParameterTypes)
if (method != null) {
info = method
}
} else if (kind == FIELD && data.memberName.isNotBlank()) {
val field = clazz.getField(data.memberName)
if (field != null) {
info = field
if (data is MethodCompletionData) {
if (
kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank()
) {
val method = clazz.getMethod(data.memberName, *data.erasedParameterTypes.toTypedArray())
if (method != null) {
info = method
}
} else if (kind == FIELD && data.memberName.isNotBlank()) {
val field = clazz.getField(data.memberName)
if (field != null) {
info = field
}
}
}
val sb = StringBuilder()
if (info!!.since > 1) {
sb.append(textView.context.getString(msg_api_info_since, info.since))
@@ -177,7 +186,13 @@ class CompletionListAdapter : EditorCompletionAdapter() {
// A field type
type === FIELD) && data != null
) {
!TextUtils.isEmpty(data.className)
val className =
when (data) {
is ClassCompletionData -> data.className
is MemberCompletionData -> data.classInfo.className
else -> null
}
!TextUtils.isEmpty(className)
} else false
}
}
@@ -67,7 +67,6 @@ class FileTreeFragment :
): View {
binding = LayoutEditorFileTreeBinding.inflate(inflater, container, false)
binding?.root?.doOnApplyWindowInsets { view, insets, _, _ ->
LOG.debug("onApplyWindowInsets")
insets.getInsets(statusBars()).apply { view.updatePadding(top = top + SizeUtils.dp2px(8f)) }
}
return binding!!.root
@@ -17,8 +17,11 @@
package com.itsaky.androidide.views.editor
import android.content.Intent
import android.net.Uri
import android.widget.ListView
import com.blankj.utilcode.util.ReflectUtils
import com.itsaky.androidide.lsp.util.DocumentationReferenceProvider
import com.itsaky.androidide.progress.ProgressManager
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.lang.completion.CompletionItem
@@ -26,7 +29,7 @@ import io.github.rosemoe.sora.lang.completion.CompletionPublisher
import io.github.rosemoe.sora.widget.component.CompletionLayout
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion
import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter
import java.util.concurrent.atomic.*
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.min
/**
@@ -46,8 +49,24 @@ class EditorCompletionWindow(val editor: IDEEditor) : EditorAutoCompletion(edito
override fun setLayout(layout: CompletionLayout) {
super.setLayout(layout)
mListView = layout.completionList as ListView
mListView!!.adapter = mAdapter
(layout.completionList as? ListView)?.let {
mListView = it
it.adapter = mAdapter
it.setOnItemLongClickListener { _, view, position, _ ->
val data =
(mItems[position] as? com.itsaky.androidide.lsp.models.CompletionItem)?.data
?: return@setOnItemLongClickListener false
val url =
DocumentationReferenceProvider.getUrl(data) ?: return@setOnItemLongClickListener false
Intent().apply {
action = Intent.ACTION_VIEW
setData(Uri.parse(url))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
view.context.startActivity(this)
}
true
}
}
}
override fun setAdapter(adapter: EditorCompletionAdapter) {
@@ -58,7 +77,7 @@ class EditorCompletionWindow(val editor: IDEEditor) : EditorAutoCompletion(edito
mListView!!.adapter = adapter
}
override fun select(pos: Int) : Boolean {
override fun select(pos: Int): Boolean {
if (pos > mAdapter!!.count) {
return false
}
@@ -70,7 +89,7 @@ class EditorCompletionWindow(val editor: IDEEditor) : EditorAutoCompletion(edito
}
}
override fun select() : Boolean {
override fun select(): Boolean {
return try {
super.select()
} catch (e: Throwable) {
@@ -0,0 +1,93 @@
/*
* 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.util
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.ICompletionData
import com.itsaky.androidide.lsp.models.MemberCompletionData
import com.itsaky.androidide.lsp.models.MethodCompletionData
import com.itsaky.androidide.utils.ILogger
/**
* Provides the documentation URL for classes, methods, fields, etc.
*
* @author Akash Yadav
*/
object DocumentationReferenceProvider {
const val DOCS_BASE_URL = "https://developer.android.com/reference/"
/**
* Package names whose documentation is most likely to be available on the Android Developers
* website.
*/
private val availablePackages =
setOf(
"android", // Android APIs
"androidx", // AndroidX libraries
"com.google.android.material", // Material Components
"java" // Java APIs
)
/**
* Get the documentation URL for given completion data.
*
* @return The URL or `null` if cannot be determined.
*/
@JvmStatic
fun getUrl(data: ICompletionData): String? {
val klass =
when (data) {
is ClassCompletionData -> data
is MemberCompletionData -> data.classInfo
else -> return null
}
val url = StringBuilder(DOCS_BASE_URL)
val baseName =
if (klass.isNested) {
klass.topLevelClass
} else klass.className
if (availablePackages.find { baseName.startsWith("$it.") } == null) {
// This package is probably not listed on Android Developers documentation
return null
}
url.append(baseName.replace('.', '/'))
if (klass.isNested) {
url.append('.')
url.append(klass.nameWithoutTopLevel)
}
if (data is MemberCompletionData) {
url.append('#')
url.append(data.memberName)
}
if (data is MethodCompletionData) {
url.append('(')
url.append(data.parameterTypes.joinToString(separator = ", "))
url.append(')')
}
log.debug(url)
return url.toString()
}
private val log = ILogger.newInstance("DocumentationReferenceProvider")
}
@@ -19,6 +19,7 @@ package com.itsaky.androidide.lsp.java.edits
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.util.RewriteHelper
import io.github.rosemoe.sora.widget.CodeEditor
@@ -38,7 +39,8 @@ class ClassImportEditHandler(val imports: Set<String>, file: Path) : IJavaEditHa
editor: CodeEditor,
completionItem: CompletionItem
) {
val className = completionItem.data!!.className
val data = completionItem.data as? ClassCompletionData ?: return
val className = data.className
val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className)
RewriteHelper.performEdits(edits, editor)
}
@@ -24,8 +24,8 @@ import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler
import com.itsaky.androidide.lsp.java.providers.BaseJavaServiceProvider
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.CompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionItemKind
import com.itsaky.androidide.lsp.models.CompletionItemKind.ENUM_MEMBER
@@ -36,8 +36,11 @@ import com.itsaky.androidide.lsp.models.CompletionItemKind.NONE
import com.itsaky.androidide.lsp.models.CompletionItemKind.PROPERTY
import com.itsaky.androidide.lsp.models.CompletionItemKind.VARIABLE
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.FieldCompletionData
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.progress.ProgressManager.Companion.abortIfCancelled
import com.itsaky.androidide.utils.ILogger
import com.sun.source.tree.Tree
@@ -80,6 +83,7 @@ abstract class IJavaCompletionProvider(
protected lateinit var filePackage: String
protected lateinit var fileImports: Set<String>
@Suppress("Since15")
open fun complete(
task: CompileTask,
path: TreePath,
@@ -250,7 +254,7 @@ abstract class IJavaCompletionProvider(
item.detail = packageName(className).toString()
item.sortText = item.label.toString()
item.matchLevel = matchLevel
item.data = CompletionData().apply { this.className = className }
item.data = ClassCompletionData(className)
// If file is not provided, we are probably completing an import path
item.additionalEditHandler = if (file == null) null else ClassImportEditHandler(imports, file)
@@ -312,34 +316,70 @@ abstract class IJavaCompletionProvider(
}
}
protected open fun data(task: CompileTask, element: Element, overloads: Int): CompletionData? {
protected open fun data(task: CompileTask, element: Element, overloads: Int): ICompletionData? {
abortIfCancelled()
abortCompletionIfCancelled()
val data = CompletionData()
when {
element is TypeElement -> data.className = element.qualifiedName.toString()
element.kind == FIELD -> {
val field = element as VariableElement
val type = field.enclosingElement as TypeElement
data.className = type.qualifiedName.toString()
data.memberName = field.simpleName.toString()
}
element is ExecutableElement -> {
val types = task.task.types
val type = element.enclosingElement as TypeElement
data.className = type.qualifiedName.toString()
data.memberName = element.simpleName.toString()
data.erasedParameterTypes = Array(element.parameters.size) { "" }
for (i in 0 until data.erasedParameterTypes.size) {
val p = element.parameters[i].asType()
data.erasedParameterTypes[i] = types.erasure(p).toString()
}
data.plusOverloads = overloads - 1
}
else -> {
return null
}
return when {
element is TypeElement -> getClassCompletionData(element)
element.kind == FIELD -> getFieldCompletionData(element)
element is ExecutableElement -> getMethodCompletionData(task, element, overloads)
else -> return null
}
return data
}
protected open fun getMethodCompletionData(
task: CompileTask,
element: ExecutableElement,
overloads: Int
): MethodCompletionData {
val types = task.task.types
val type = element.enclosingElement as TypeElement
val parameterTypes = Array(element.parameters.size) { "" }
val erasedParameterTypes = Array(parameterTypes.size) { "" }
val plusOverloads = overloads - 1
for (i in element.parameters.indices) {
val p = element.parameters[i].asType()
parameterTypes[i] = p.toString()
erasedParameterTypes[i] = types.erasure(p).toString()
}
return MethodCompletionData(
element.simpleName.toString(),
getClassCompletionData(type),
parameterTypes.toList(),
erasedParameterTypes.toList(),
plusOverloads
)
}
protected open fun getFieldCompletionData(element: Element): FieldCompletionData {
val field = element as VariableElement
val type = field.enclosingElement as TypeElement
return FieldCompletionData(field.simpleName.toString(), getClassCompletionData(type))
}
protected open fun getClassCompletionData(element: TypeElement) =
ClassCompletionData(
element.qualifiedName.toString(),
element.enclosingElement.kind != PACKAGE,
element.findTopLevelElement().qualifiedName.toString()
)
protected open fun TypeElement.findTopLevelElement(): TypeElement {
if (enclosingElement.kind == PACKAGE) {
return this
}
var element: TypeElement? = this
while (true) {
if (element?.enclosingElement?.kind == PACKAGE) {
break
}
element = element?.enclosingElement as? TypeElement
}
return element!!
}
}
@@ -0,0 +1,81 @@
/*
* 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.models
/** Contains information about a completion item. */
interface ICompletionData
/**
* Information about a class-related completion item.
*
* @property className The fully qualified name of the class.
* @property isNested Whether the given class is a nested class or not.
* @property topLevelClass If [isNested] is true, then this must be set to the fully qualified name
* @property simpleName The simple name of the class.
* @property nameWithoutTopLevel The name of this class without the fully qualified name of its top
* level class. For example, the value of this property for class name
* `com.my.pck.MyClass.Inner.InnerInner` will be `Inner.InnerInner`
*/
data class ClassCompletionData
@JvmOverloads
constructor(val className: String, val isNested: Boolean = false, val topLevelClass: String = "") :
ICompletionData {
val simpleName: String
get() {
return className.substringAfterLast(delimiter = '.')
}
val nameWithoutTopLevel: String
get() {
if (!isNested) {
return className
}
return className.substring(topLevelClass.length + 1)
}
}
/**
* Information about a member of a class.
*
* @property memberName The simple name of the class member.
* @property classInfo Information about the class [memberName] is a member of.
*/
interface MemberCompletionData : ICompletionData {
val memberName: String
val classInfo: ClassCompletionData
}
/** Information about a field-related completion item. */
data class FieldCompletionData(
override val memberName: String,
override val classInfo: ClassCompletionData
) : MemberCompletionData
/**
* Information about a method-related completion item.
*
* @property erasedParameterTypes The erased parameter types of the method.
* @property plusOverloads The number of existing overloaded versions of this method.
*/
data class MethodCompletionData(
override val memberName: String,
override val classInfo: ClassCompletionData,
val parameterTypes: List<String>,
val erasedParameterTypes: List<String>,
val plusOverloads: Int
) : MemberCompletionData
@@ -79,7 +79,11 @@ open class CompletionResult(items: Collection<CompletionItem>) {
@JvmStatic
@JvmOverloads
fun mapAndFilter(src: CompletionResult, partial: String, map: Consumer<CompletionItem> = Consumer { }): CompletionResult {
fun mapAndFilter(
src: CompletionResult,
partial: String,
map: Consumer<CompletionItem> = Consumer {}
): CompletionResult {
val newItems = src.items.toMutableList()
newItems.forEach(map)
newItems.removeIf { !it.label.startsWith(partial) }
@@ -120,7 +124,7 @@ open class CompletionItem(
var kind: CompletionItemKind,
var matchLevel: MatchLevel,
var additionalTextEdits: List<TextEdit>?,
var data: CompletionData?
var data: ICompletionData?
) :
io.github.rosemoe.sora.lang.completion.CompletionItem(label, detail), Comparable<CompletionItem> {
@@ -267,54 +271,6 @@ constructor(
val snippet: CodeSnippet? = null
)
data class CompletionData(
var className: String,
var memberName: String,
var erasedParameterTypes: Array<String>,
var plusOverloads: Int
) {
constructor() : this("", "", arrayOf(), -1)
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as CompletionData
if (className != other.className) {
return false
}
if (memberName != other.memberName) {
return false
}
if (!erasedParameterTypes.contentEquals(other.erasedParameterTypes)) {
return false
}
if (plusOverloads != other.plusOverloads) {
return false
}
return true
}
override fun hashCode(): Int {
var result = className.hashCode()
result = 31 * result + memberName.hashCode()
result = 31 * result + erasedParameterTypes.contentHashCode()
result = 31 * result + plusOverloads
return result
}
}
data class Command(var title: String, var command: String) {
companion object {
@@ -23,8 +23,8 @@ import com.android.aaptcompiler.ResourceTable
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.api.describeSnippet
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.CompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionItemKind.CLASS
import com.itsaky.androidide.lsp.models.CompletionItemKind.FIELD
@@ -149,7 +149,7 @@ abstract class IXmlCompletionProvider(private val provider: ICompletionProvider)
this.editHandler = TagEditHandler()
this.matchLevel = matchLevel
this.kind = CLASS
this.data = CompletionData().apply { className = qualifiedName }
this.data = ClassCompletionData(className = qualifiedName)
}
/**
@@ -28,12 +28,13 @@ import com.android.aaptcompiler.ResourcePathData
import com.android.aaptcompiler.ResourceTable
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.models.CompletionData
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionItemKind.FIELD
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.CompletionResult.Companion.EMPTY
import com.itsaky.androidide.lsp.models.FieldCompletionData
import com.itsaky.androidide.lsp.models.InsertTextFormat.PLAIN_TEXT
import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
import com.itsaky.androidide.lsp.xml.providers.completion.AttrValueCompletionProvider
@@ -72,7 +73,7 @@ class ManifestAttrValueCompletionProvider(provider: ICompletionProvider) :
}
return super.doComplete(params, pathData, document, type, prefix)
}
override fun resTableForFindAttr(): ResourceTable? {
return manifestResourceTable().firstOrNull()
}
@@ -96,10 +97,10 @@ class ManifestAttrValueCompletionProvider(provider: ICompletionProvider) :
// Show API information
item.kind = FIELD
item.data =
CompletionData().apply {
this.className = SdkConstants.CLASS_MANIFEST_PERMISSION
this.memberName = value.name
}
FieldCompletionData(
memberName = value.name,
classInfo = ClassCompletionData(className = SdkConstants.CLASS_MANIFEST_PERMISSION)
)
result.add(item)
}
return CompletionResult(result)
Binary file not shown.