Skip to content

Commit 31db1d8

Browse files
committed
Add value factories to property value completions
1 parent 38e1d64 commit 31db1d8

File tree

4 files changed

+224
-113
lines changed

4 files changed

+224
-113
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
33

44
[versions]
5-
gradle-tooling = "8.13-20250127002038+0000"
6-
declarative-dsl = "8.13-20250121001720+0000"
5+
gradle-tooling = "8.13-20250128002155+0000"
6+
declarative-dsl = "8.13-20250128002155+0000"
77
detekt = "1.23.6"
88
lsp4j = "0.23.1"
99
logback = "1.5.6"

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-8.13-20250128002155+0000-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

lsp/src/main/kotlin/org/gradle/declarative/lsp/DeclarativeTextDocumentService.kt

Lines changed: 120 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ import org.eclipse.lsp4j.services.TextDocumentService
4343
import org.gradle.declarative.dsl.schema.AnalysisSchema
4444
import org.gradle.declarative.dsl.schema.DataClass
4545
import org.gradle.declarative.dsl.schema.DataParameter
46+
import org.gradle.declarative.dsl.schema.DataProperty
4647
import org.gradle.declarative.dsl.schema.DataType
4748
import org.gradle.declarative.dsl.schema.DataTypeRef
4849
import org.gradle.declarative.dsl.schema.EnumClass
50+
import org.gradle.declarative.dsl.schema.FqName
4951
import org.gradle.declarative.dsl.schema.FunctionSemantics
5052
import org.gradle.declarative.dsl.schema.SchemaFunction
53+
import org.gradle.declarative.dsl.schema.SchemaMemberFunction
5154
import org.gradle.declarative.lsp.build.model.DeclarativeResourcesModel
5255
import org.gradle.declarative.lsp.extension.indexBasedOverlayResultFromDocuments
5356
import org.gradle.declarative.lsp.extension.toLspRange
@@ -93,7 +96,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
9396
this.documentStore = documentStore
9497
this.mutationRegistry = mutationRegistry
9598
this.declarativeResources = declarativeResources
96-
99+
97100
this.schemaAnalysisEvaluator = SimpleAnalysisEvaluator.withSchema(
98101
declarativeResources.settingsInterpretationSequence,
99102
declarativeResources.projectInterpretationSequence
@@ -183,8 +186,11 @@ class DeclarativeTextDocumentService : TextDocumentService {
183186
?.getDataClass(dom.overlayResolutionContainer)
184187
.let { it ?: schema.topLevelReceiverType }
185188
.let { dataClass ->
186-
computePropertyCompletions(dataClass, schema) +
187-
computeFunctionCompletions(dataClass, schema)
189+
computePropertyCompletions(dataClass, schema) + computePropertyByValueFactoryCompletions(
190+
dataClass,
191+
schema
192+
) +
193+
computeFunctionCompletions(dataClass, schema)
188194
}
189195
}
190196
}.orEmpty().toMutableList()
@@ -309,7 +315,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
309315
)
310316
)
311317
}
312-
318+
313319
data class ParsedDocument(
314320
val documentOverlayResult: DocumentOverlayResult,
315321
val analysisSchemas: List<AnalysisSchema>
@@ -318,7 +324,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
318324
private fun parse(uri: URI, text: String): ParsedDocument {
319325
val fileName = uri.path.substringAfterLast('/')
320326
val analysisResult = schemaAnalysisEvaluator.evaluate(fileName, text)
321-
327+
322328
// Workaround: for now, the mutation utilities cannot handle mutations that touch the underlay document content.
323329
// To avoid that, use the utility that produces an overlay result with no real underlay content.
324330
// This utility also takes care of multi-step resolution results and merges them, presenting .
@@ -351,14 +357,77 @@ private fun computePropertyCompletions(
351357
dataClass: DataClass,
352358
analysisSchema: AnalysisSchema
353359
): List<CompletionItem> {
354-
return dataClass.properties.map { property ->
355-
val propertyName = property.name
356-
val targetType = property.valueType.toSimpleName()
360+
return dataClass.properties.mapNotNull { property ->
361+
when (val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(property.valueType)) {
362+
is EnumClass -> completionItem(property, resolvedType)
363+
is DataType.BooleanDataType -> completionItem(property, resolvedType)
364+
is DataType.IntDataType -> completionItem(property, resolvedType)
365+
is DataType.LongDataType -> completionItem(property, resolvedType)
366+
is DataType.StringDataType -> completionItem(property, resolvedType)
367+
else -> null
368+
}
369+
}
370+
}
357371

358-
CompletionItem("$propertyName = $targetType").apply {
359-
kind = CompletionItemKind.Field
360-
insertTextFormat = InsertTextFormat.Snippet
361-
insertText = "${property.name} = ${computeTypedPlaceholder(1, property.valueType, analysisSchema)}"
372+
private fun completionItem(property: DataProperty, resolvedType: DataType) =
373+
CompletionItem("${property.name} = ${property.valueType.toSimpleName()}").apply {
374+
kind = CompletionItemKind.Field
375+
insertTextFormat = InsertTextFormat.Snippet
376+
insertText = "${property.name} = ${computeTypedPlaceholder(1, resolvedType)}"
377+
}
378+
379+
private typealias LabelAndInsertText = Pair<String, String>
380+
381+
private fun computePropertyByValueFactoryCompletions(
382+
dataClass: DataClass,
383+
analysisSchema: AnalysisSchema
384+
): List<CompletionItem> {
385+
fun indexValueFactories(analysisSchema: AnalysisSchema, type: DataClass, namePrefix: String): Map<FqName, List<LabelAndInsertText>> {
386+
val factoryIndex = mutableMapOf<FqName, List<LabelAndInsertText>>()
387+
type.memberFunctions
388+
.filter { it.semantics is FunctionSemantics.Pure && it.returnValueType is DataTypeRef.Name }
389+
.forEach {
390+
val indexKey = (it.returnValueType as DataTypeRef.Name).fqName
391+
val labelAndInsertText = "$namePrefix${computeCompletionLabel(it)}" to "$namePrefix${
392+
computeCompletionInsertText(
393+
it,
394+
analysisSchema
395+
)
396+
}"
397+
factoryIndex.merge(indexKey, listOf(labelAndInsertText)) { oldVal, newVal -> oldVal + newVal }
398+
}
399+
type.properties.filter { it.valueType is DataTypeRef.Name }.forEach {
400+
when (val propType = analysisSchema.dataClassTypesByFqName[(it.valueType as DataTypeRef.Name).fqName]) {
401+
is DataClass -> {
402+
val propName = it.name
403+
val propIndex = indexValueFactories(analysisSchema, propType, "$namePrefix${propName}.")
404+
propIndex.forEach { (key, value) ->
405+
factoryIndex.merge(key, value) { oldVal, newVal -> oldVal + newVal }
406+
}
407+
}
408+
409+
is EnumClass -> Unit
410+
null -> Unit
411+
}
412+
}
413+
return factoryIndex
414+
}
415+
416+
val factories = indexValueFactories(analysisSchema, analysisSchema.topLevelReceiverType, "")
417+
return dataClass.properties.flatMap { property ->
418+
val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(property.valueType)
419+
if (resolvedType is DataType.ClassDataType) {
420+
val factoriesForProperty = factories[resolvedType.name]
421+
factoriesForProperty
422+
?.map {
423+
CompletionItem("${property.name} = ${it.first}").apply {
424+
kind = CompletionItemKind.Field
425+
insertTextFormat = InsertTextFormat.Snippet
426+
insertText = "${property.name} = ${it.second}"
427+
}
428+
} ?: emptyList()
429+
} else {
430+
emptyList()
362431
}
363432
}
364433
}
@@ -368,28 +437,56 @@ private fun computeFunctionCompletions(
368437
analysisSchema: AnalysisSchema
369438
): List<CompletionItem> =
370439
dataClass.memberFunctions.map { function ->
371-
val functionName = function.simpleName
372-
val parameterSignature = when (function.parameters.isEmpty()) {
373-
true -> ""
374-
false -> function.parameters.joinToString(",", "(", ")") { it.toSignatureLabel() }
375-
}
376-
val configureBlockLabel = function.semantics.toBlockConfigurabilityLabel().orEmpty()
377-
378-
CompletionItem("$functionName$parameterSignature$configureBlockLabel").apply {
440+
val label = computeCompletionLabel(function)
441+
val text = computeCompletionInsertText(function, analysisSchema)
442+
CompletionItem(label).apply {
379443
kind = CompletionItemKind.Method
380444
insertTextFormat = InsertTextFormat.Snippet
381445
insertTextMode = InsertTextMode.AdjustIndentation
382-
insertText = computeCompletionInsertText(function, analysisSchema)
446+
insertText = text
383447
}
384448
}
385449

450+
/**
451+
* Computes a [placeholder](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#placeholders)
452+
* based on the given data type.
453+
*
454+
* If there is a specific placeholder for the given data type, it will be used.
455+
* Otherwise, a simple indexed will be used
456+
*/
457+
private fun computeTypedPlaceholder(
458+
index: Int,
459+
resolvedType: DataType
460+
): String {
461+
return when (resolvedType) {
462+
is DataType.BooleanDataType -> "\${$index|true,false|}"
463+
is EnumClass -> "\${$index|${resolvedType.entryNames.joinToString(",")}|}"
464+
is DataType.IntDataType -> "\${$index:0}"
465+
is DataType.LongDataType -> "\${$index:0L}"
466+
is DataType.StringDataType -> "\"\${$index}\""
467+
else -> "\$$index"
468+
}
469+
}
470+
471+
private fun computeCompletionLabel(function: SchemaMemberFunction): String {
472+
val functionName = function.simpleName
473+
val parameterSignature = when (function.parameters.isEmpty()) {
474+
true -> ""
475+
false -> function.parameters.joinToString(",", "(", ")") { it.toSignatureLabel() }
476+
}
477+
val configureBlockLabel = function.semantics.toBlockConfigurabilityLabel().orEmpty()
478+
479+
return "$functionName$parameterSignature$configureBlockLabel"
480+
}
481+
386482
private fun computeCompletionInsertText(
387483
function: SchemaFunction,
388-
analysisSchema: AnalysisSchema
484+
analysisSchema: AnalysisSchema,
389485
): String {
390486
val parameterSnippet = function.parameters.mapIndexed { index, parameter ->
391487
// Additional placeholders are indexed from 1
392-
computeTypedPlaceholder(index + 1, parameter.type, analysisSchema)
488+
val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(parameter.type)
489+
computeTypedPlaceholder(index + 1, resolvedType)
393490
}.joinToString(", ", "(", ")")
394491

395492
return when (val semantics = function.semantics) {
@@ -416,28 +513,6 @@ private fun computeCompletionInsertText(
416513
}
417514
}
418515

419-
/**
420-
* Computes a [placeholder](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#placeholders)
421-
* based on the given data type.
422-
*
423-
* If there is a specific placeholder for the given data type, it will be used.
424-
* Otherwise, a simple indexed will be used
425-
*/
426-
private fun computeTypedPlaceholder(
427-
index: Int,
428-
type: DataTypeRef,
429-
analysisSchema: AnalysisSchema
430-
): String {
431-
return when (val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(type)) {
432-
is DataType.BooleanDataType -> "\${$index|true,false|}"
433-
is EnumClass -> "\${$index|${resolvedType.entryNames.joinToString(",")}|}"
434-
is DataType.IntDataType -> "\${$index:0}"
435-
is DataType.LongDataType -> "\${$index:0L}"
436-
is DataType.StringDataType -> "\"\${$index}\""
437-
else -> "\$$index"
438-
}
439-
}
440-
441516
// Extension functions -------------------------------------------------------------------------------------------------
442517

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

0 commit comments

Comments
 (0)