Skip to content

Commit

Permalink
Add value factories to property value completions
Browse files Browse the repository at this point in the history
  • Loading branch information
jbartok committed Feb 5, 2025
1 parent 38e1d64 commit f4693dd
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 101 deletions.
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

[versions]
gradle-tooling = "8.13-20250127002038+0000"
declarative-dsl = "8.13-20250121001720+0000"
gradle-tooling = "8.13-20250128002155+0000"
declarative-dsl = "8.13-20250128002155+0000"
detekt = "1.23.6"
lsp4j = "0.23.1"
logback = "1.5.6"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-8.13-20250128002155+0000-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import org.gradle.declarative.dsl.schema.DataParameter
import org.gradle.declarative.dsl.schema.DataType
import org.gradle.declarative.dsl.schema.DataTypeRef
import org.gradle.declarative.dsl.schema.EnumClass
import org.gradle.declarative.dsl.schema.FqName
import org.gradle.declarative.dsl.schema.FunctionSemantics
import org.gradle.declarative.dsl.schema.SchemaFunction
import org.gradle.declarative.dsl.schema.SchemaMemberFunction
import org.gradle.declarative.lsp.build.model.DeclarativeResourcesModel
import org.gradle.declarative.lsp.extension.indexBasedOverlayResultFromDocuments
import org.gradle.declarative.lsp.extension.toLspRange
Expand Down Expand Up @@ -183,7 +185,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
?.getDataClass(dom.overlayResolutionContainer)
.let { it ?: schema.topLevelReceiverType }
.let { dataClass ->
computePropertyCompletions(dataClass, schema) +
computePropertyCompletions(dataClass, schema) + computePropertyByValueFactoryCompletions(dataClass, schema) +
computeFunctionCompletions(dataClass, schema)
}
}
Expand Down Expand Up @@ -363,29 +365,107 @@ private fun computePropertyCompletions(
}
}

private typealias LabelAndInsertText = Pair<String, String>

private fun computePropertyByValueFactoryCompletions(
dataClass: DataClass,
analysisSchema: AnalysisSchema
): List<CompletionItem> {
fun indexValueFactories(analysisSchema: AnalysisSchema, type: DataClass, namePrefix: String): Map<FqName, List<LabelAndInsertText>> {
val factoryIndex = mutableMapOf<FqName, List<LabelAndInsertText>>()
type.memberFunctions
.filter { it.semantics is FunctionSemantics.Pure && it.returnValueType is DataTypeRef.Name}
.forEach {
val indexKey = (it.returnValueType as DataTypeRef.Name).fqName
val labelAndInsertText = "$namePrefix${computeCompletionLabel(it)}" to "$namePrefix${computeCompletionInsertText(it, analysisSchema)}"
factoryIndex.merge(indexKey, listOf(labelAndInsertText)) { oldVal, newVal -> oldVal + newVal }
}
type.properties.filter {it.valueType is DataTypeRef.Name}.forEach {
when(val propType = analysisSchema.dataClassTypesByFqName[(it.valueType as DataTypeRef.Name).fqName]) {
is DataClass -> {
val propName = it.name
val propIndex = indexValueFactories(analysisSchema, propType, "$namePrefix${propName}.")
propIndex.forEach { (key, value) ->
factoryIndex.merge(key, value) { oldVal, newVal -> oldVal + newVal}
}
}
is EnumClass -> Unit
null -> Unit
}
}
return factoryIndex
}

val factories = indexValueFactories(analysisSchema, analysisSchema.topLevelReceiverType, "")
return dataClass.properties.flatMap { property ->
val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(property.valueType)
if (resolvedType is DataType.ClassDataType) {
val factoriesForProperty = factories[resolvedType.name]
factoriesForProperty
?.map {
CompletionItem("${property.name} = ${it.first}").apply {
kind = CompletionItemKind.Field
insertTextFormat = InsertTextFormat.Snippet
insertText = "${property.name} = ${it.second}"
}
} ?: emptyList()
} else {
emptyList()
}
}
}

private fun computeFunctionCompletions(
dataClass: DataClass,
analysisSchema: AnalysisSchema
): List<CompletionItem> =
dataClass.memberFunctions.map { function ->
val functionName = function.simpleName
val parameterSignature = when (function.parameters.isEmpty()) {
true -> ""
false -> function.parameters.joinToString(",", "(", ")") { it.toSignatureLabel() }
}
val configureBlockLabel = function.semantics.toBlockConfigurabilityLabel().orEmpty()

CompletionItem("$functionName$parameterSignature$configureBlockLabel").apply {
val label = computeCompletionLabel(function)
val text = computeCompletionInsertText(function, analysisSchema)
CompletionItem(label).apply {
kind = CompletionItemKind.Method
insertTextFormat = InsertTextFormat.Snippet
insertTextMode = InsertTextMode.AdjustIndentation
insertText = computeCompletionInsertText(function, analysisSchema)
insertText = text
}
}

/**
* Computes a [placeholder](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#placeholders)
* based on the given data type.
*
* If there is a specific placeholder for the given data type, it will be used.
* Otherwise, a simple indexed will be used
*/
private fun computeTypedPlaceholder(
index: Int,
type: DataTypeRef,
analysisSchema: AnalysisSchema
): String {
return when (val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(type)) {
is DataType.BooleanDataType -> "\${$index|true,false|}"
is EnumClass -> "\${$index|${resolvedType.entryNames.joinToString(",")}|}"
is DataType.IntDataType -> "\${$index:0}"
is DataType.LongDataType -> "\${$index:0L}"
is DataType.StringDataType -> "\"\${$index}\""
else -> "\$$index"
}
}

private fun computeCompletionLabel(function: SchemaMemberFunction): String {
val functionName = function.simpleName
val parameterSignature = when (function.parameters.isEmpty()) {
true -> ""
false -> function.parameters.joinToString(",", "(", ")") { it.toSignatureLabel() }
}
val configureBlockLabel = function.semantics.toBlockConfigurabilityLabel().orEmpty()

return "$functionName$parameterSignature$configureBlockLabel"
}

private fun computeCompletionInsertText(
function: SchemaFunction,
analysisSchema: AnalysisSchema
analysisSchema: AnalysisSchema,
): String {
val parameterSnippet = function.parameters.mapIndexed { index, parameter ->
// Additional placeholders are indexed from 1
Expand Down Expand Up @@ -416,28 +496,6 @@ private fun computeCompletionInsertText(
}
}

/**
* Computes a [placeholder](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#placeholders)
* based on the given data type.
*
* If there is a specific placeholder for the given data type, it will be used.
* Otherwise, a simple indexed will be used
*/
private fun computeTypedPlaceholder(
index: Int,
type: DataTypeRef,
analysisSchema: AnalysisSchema
): String {
return when (val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(type)) {
is DataType.BooleanDataType -> "\${$index|true,false|}"
is EnumClass -> "\${$index|${resolvedType.entryNames.joinToString(",")}|}"
is DataType.IntDataType -> "\${$index:0}"
is DataType.LongDataType -> "\${$index:0L}"
is DataType.StringDataType -> "\"\${$index}\""
else -> "\$$index"
}
}

// Extension functions -------------------------------------------------------------------------------------------------

// TODO: this might not be the best way to resolve the type name, but it works for now
Expand Down
Loading

0 comments on commit f4693dd

Please sign in to comment.