From 679bc3f56001c4af6bcddbc19b95ea9b4adf40e8 Mon Sep 17 00:00:00 2001 From: Aleksei Trifonov Date: Thu, 10 Feb 2022 17:11:24 +0300 Subject: [PATCH] [toml] IDEA-288468 Provide completion for values GitOrigin-RevId: b46aaa32f7f4c93b46d6d8545f0ea41b32dcb3bc --- .../ide/completion/TomlCompletionFixture.kt | 22 +++++ .../json/TomlJsonSchemaCompletionTestBase.kt | 5 + .../json/TomlJsonSchemaValueCompletionTest.kt | 93 +++++++++++++++++++ .../resources/jsonSchemas/test-schema.json | 7 ++ .../org/toml/ide/json/TomlJsonPsiWalker.kt | 13 +++ .../TomlJsonSchemaCompletionContributor.kt | 48 ++++++++++ 6 files changed, 188 insertions(+) create mode 100644 plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaValueCompletionTest.kt diff --git a/plugins/toml/core/src/test/kotlin/org/toml/ide/completion/TomlCompletionFixture.kt b/plugins/toml/core/src/test/kotlin/org/toml/ide/completion/TomlCompletionFixture.kt index 76da5ba285e8..8f748dccd096 100644 --- a/plugins/toml/core/src/test/kotlin/org/toml/ide/completion/TomlCompletionFixture.kt +++ b/plugins/toml/core/src/test/kotlin/org/toml/ide/completion/TomlCompletionFixture.kt @@ -69,4 +69,26 @@ class TomlCompletionFixture( error("Expected completions that don't contain $variants, but got ${lookups.map { it.render() }}") } } + + private fun executeSoloCompletion() { + val lookups = myFixture.completeBasic() + + if (lookups != null) { + if (lookups.size == 1) { + // for cases like `frob/*caret*/nicate()`, + // completion won't be selected automatically. + myFixture.type('\n') + return + } + fun LookupElement.debug(): String = "$lookupString ($psiElement)" + error("Expected a single completion, but got ${lookups.size}\n" + + lookups.joinToString("\n") { it.debug() }) + } + } + + fun doSingleCompletion(code: String, after: String) { + myFixture.configureByText(defaultFileName, code.trimIndent()) + executeSoloCompletion() + myFixture.checkResult(after.trimIndent()) + } } diff --git a/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionTestBase.kt b/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionTestBase.kt index 20fa1dab3c7f..9d688a95b61a 100644 --- a/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionTestBase.kt +++ b/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionTestBase.kt @@ -29,4 +29,9 @@ abstract class TomlJsonSchemaCompletionTestBase : TomlJsonSchemaTestBase() { @Language("TOML") code: String, render: LookupElement.() -> String = { lookupString } ) = completionFixture.checkNotContainsCompletion(variants, code, render) + + fun doSingleCompletion( + @Language("TOML") before: String, + @Language("TOML") after: String + ) = completionFixture.doSingleCompletion(before, after) } diff --git a/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaValueCompletionTest.kt b/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaValueCompletionTest.kt new file mode 100644 index 000000000000..a4da091de1d8 --- /dev/null +++ b/plugins/toml/core/src/test/kotlin/org/toml/ide/json/TomlJsonSchemaValueCompletionTest.kt @@ -0,0 +1,93 @@ +package org.toml.ide.json + +class TomlJsonSchemaValueCompletionTest : TomlJsonSchemaCompletionTestBase() { + fun `test enum variants`() = checkContainsCompletion(setOf("\"2015\"", "\"2018\"", "\"2021\""), """ + [package] + edition = + """) + + fun `test variants inside array table`() = checkContainsCompletion(setOf("\"\""), """ + [[bin]] + name = + """) + + fun `test string literal inside literal variants`() = checkNotContainsCompletion(setOf("\"\""), """ + [package] + name = "" + """) + + fun `test boolean value variants`() = checkContainsCompletion(setOf("true", "false"), """ + [[bin]] + test = + """) + + // TODO: Support value completion in inline tables + fun `test variants inside inline table`() { + assertThrows(IllegalStateException::class.java) { + checkContainsCompletion(setOf("\"\""), """ + package = { name = } + """) + } + } + + fun `test inside inside inline array`() { + assertThrows(IllegalStateException::class.java) { + checkContainsCompletion(setOf("{}"), """ + bin = [] + """) + } + } + + fun `test variants inside inline array and table`() { + assertThrows(IllegalStateException::class.java) { + checkContainsCompletion(setOf("\"\""), """ + bin = [{ name = }] + """) + } + } + + fun `test enum string value completion in literal`() = doSingleCompletion(""" + [package] + edition = "21" + """, """ + [package] + edition = "2021" + """) + + fun `test enum string value completion`() = doSingleCompletion(""" + [package] + edition = 21 + """, """ + [package] + edition = "2021" + """) + + fun `test array completion`() = doSingleCompletion(""" + [package] + authors = + """, """ + [package] + authors = [] + """) + + fun `test inline table completion`() = doSingleCompletion(""" + [dependencies] + foo = + """, """ + [dependencies] + foo = {} + """) + + fun `test string literal completion`() = doSingleCompletion(""" + [package] + name = + """, """ + [package] + name = "" + """) + + fun `test number enum variants`() = checkContainsCompletion(setOf("1", "2"), """ + [foo] + number-enum = + """) +} \ No newline at end of file diff --git a/plugins/toml/core/src/test/resources/jsonSchemas/test-schema.json b/plugins/toml/core/src/test/resources/jsonSchemas/test-schema.json index bf00c5e0fc82..c9f165d07e77 100644 --- a/plugins/toml/core/src/test/resources/jsonSchemas/test-schema.json +++ b/plugins/toml/core/src/test/resources/jsonSchemas/test-schema.json @@ -39,6 +39,9 @@ }, "path": { "type": "string" + }, + "test": { + "type": "boolean" } } } @@ -96,6 +99,10 @@ }, "a": { "type": "integer" + }, + "number-enum": { + "type": "number", + "enum": [1, 2, 3] } } } diff --git a/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonPsiWalker.kt b/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonPsiWalker.kt index 341c4997c9cb..acbeffd41b43 100644 --- a/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonPsiWalker.kt +++ b/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonPsiWalker.kt @@ -51,6 +51,19 @@ object TomlJsonPsiWalker : JsonLikePsiWalker { val parentKey = parent.header.key ?: break // add table header segments to process all the previous siblings to handle nested array tables cases parentKey.segments.mapTo(tableHeaderSegments) { it.name } + + // TODO: Workaround, should be fixed in TOML grammar + // if it is a request from inspections, it walks only properties, + // so we don't have path calculation from key or value + if (element is TomlKeyValue) { + val currentKey = current.key + + for (segment in currentKey.segments.asReversed()) { + if (segment != element || forceLastTransition) { + position.addPrecedingStep(segment.name) + } + } + } } current is TomlValue && parent is TomlArray -> { if (current != element || forceLastTransition) { diff --git a/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionContributor.kt b/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionContributor.kt index 860c0c27b0b3..0317cede7e0f 100644 --- a/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionContributor.kt +++ b/plugins/toml/json/src/main/kotlin/org/toml/ide/json/TomlJsonSchemaCompletionContributor.kt @@ -11,7 +11,9 @@ import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.EditorModificationUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiElement import com.intellij.psi.util.parentOfType import com.intellij.util.Consumer @@ -24,7 +26,10 @@ import com.jetbrains.jsonSchema.impl.JsonSchemaObject import com.jetbrains.jsonSchema.impl.JsonSchemaResolver import com.jetbrains.jsonSchema.impl.JsonSchemaType import org.toml.ide.experiments.TomlExperiments +import org.toml.lang.psi.TomlLiteral import org.toml.lang.psi.TomlTableHeader +import org.toml.lang.psi.ext.TomlLiteralKind +import org.toml.lang.psi.ext.kind class TomlJsonSchemaCompletionContributor : CompletionContributor() { override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { @@ -68,6 +73,10 @@ class TomlJsonSchemaCompletionContributor : CompletionContributor() { val schemaProperties = schema.properties addAllPropertyVariants(properties, adapter, schemaProperties, knownNames, originalPosition) } + + if (isName != ThreeState.YES) { + suggestValues(schema, isName == ThreeState.NO) + } } for (variant in variants) { @@ -118,6 +127,45 @@ class TomlJsonSchemaCompletionContributor : CompletionContributor() { variants.add(lookupElement) } + private val isInsideStringLiteral: Boolean + get() = (position.parent as? TomlLiteral)?.kind is TomlLiteralKind.String + + private fun suggestValues(schema: JsonSchemaObject, isSurelyValue: Boolean) { + val enumVariants = schema.enum + if (enumVariants != null) { + for (o in enumVariants) { + if (isInsideStringLiteral && o !is String) continue + + val variant = if (isInsideStringLiteral) { + StringUtil.unquoteString(o.toString()) + } else { + o.toString() + } + variants.add(LookupElementBuilder.create(variant)) + } + } else if (isSurelyValue) { + variants.addAll(suggestValuesByType(schema.guessType())) + } + } + + private fun suggestValuesByType(type: JsonSchemaType?): List = when (type) { + JsonSchemaType._object -> listOf(buildPairLookupElement("{}")) + JsonSchemaType._array -> listOf(buildPairLookupElement("[]")) + JsonSchemaType._string -> if (isInsideStringLiteral) { + emptyList() + } else { + listOf(buildPairLookupElement("\"\"")) + } + JsonSchemaType._boolean -> listOf("true", "false").map { LookupElementBuilder.create(it) } + else -> emptyList() + } + + private fun buildPairLookupElement(element: String): LookupElementBuilder = + LookupElementBuilder.create(element) + .withInsertHandler { context, _ -> + EditorModificationUtil.moveCaretRelatively(context.editor, -1) + } + private fun getIconForType(type: JsonSchemaType?) = when (type) { JsonSchemaType._object -> AllIcons.Json.Object JsonSchemaType._array -> AllIcons.Json.Array