[yaml] IDEA-323117 Support for YAML Schema using $schama: and yaml-language-server comments

GitOrigin-RevId: c08a674116f465db810d52a113ff6fc6ed227fd5
This commit is contained in:
Nicolay Mitropolsky
2023-08-18 17:58:10 +02:00
committed by intellij-monorepo-bot
parent ab6e843532
commit dbb9e11769
6 changed files with 283 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.jsonSchema.impl
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.util.PsiTreeUtil
import java.util.regex.Pattern
object JsonSchemaByCommentProvider {
@JvmStatic
fun getCommentSchema(file: VirtualFile, project: Project): String? {
val psiFile = PsiManager.getInstance(project).findFile(file) ?: return null
return getCommentSchema(psiFile)
}
@JvmStatic
fun getCommentSchema(psiFile: PsiFile): String? {
for (comment in PsiTreeUtil.findChildrenOfType(psiFile, PsiComment::class.java)) {
val chars = comment.getNode()?.getChars()?.let { StringUtil.newBombedCharSequence(it, 300) } ?: continue
detectInComment(chars)?.let {
return it.subSequence(chars).toString()
}
}
return null
}
fun detectInComment(chars: CharSequence): TextRange? {
for (schemaComment in availableComments) {
schemaComment.detect(chars)?.let {
return it
}
}
return null
}
private val availableComments: List<SchemaComment> =
listOf(SchemaComment("yaml-language-server: ${'$'}schema=",
Pattern.compile("#\\s*yaml-language-server:\\s*\\\$schema=(?<id>\\S+).*", Pattern.CASE_INSENSITIVE),
forCompletion = false),
SchemaComment("${'$'}schema: ",
Pattern.compile("#\\s*\\\$schema:\\s*(?<id>\\S+).*", Pattern.CASE_INSENSITIVE)))
val schemaCommentsForCompletion: List<String> = availableComments.filter { it.forCompletion }.map { it.commentText }
}
private class SchemaComment(val commentText: String, val detectPattern: Pattern, val forCompletion: Boolean = true) {
fun detect(chars: CharSequence): TextRange? {
val matcher = detectPattern.matcher(chars)
if (!matcher.matches()) return null
return TextRange.create(matcher.start("id"), matcher.end("id"))
}
}

View File

@@ -193,6 +193,7 @@ public class JsonSchemaServiceImpl implements JsonSchemaService, ModificationTra
boolean checkSchemaProperty = true;
if (!onlyUserSchemas && providers.stream().noneMatch(p -> p.getSchemaType() == SchemaType.userSchema)) {
if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
if (schemaUrl == null) schemaUrl = JsonSchemaByCommentProvider.getCommentSchema(file, myProject);
VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
if (virtualFile != null) return Collections.singletonList(virtualFile);
checkSchemaProperty = false;

View File

@@ -75,7 +75,8 @@ public class JsonSchemaInfoPopupStep extends BaseListPopupStep<JsonSchemaInfo> i
if (value == STOP_IGNORE_FILE) {
return AllIcons.Actions.AddFile;
}
return EMPTY_ICON;
return AllIcons.FileTypes.JsonSchema;
}
@Nullable

View File

@@ -116,6 +116,8 @@
implementationClass="org.jetbrains.yaml.schema.YamlJsonSchemaDeprecationInspection"/>
<psi.referenceContributor language="yaml" implementation="org.jetbrains.yaml.YAMLWebReferenceContributor"/>
<psi.referenceContributor language="yaml" implementation="org.jetbrains.yaml.YAMLJsonSchemaIdReferenceContributor"/>
<completion.contributor language="yaml" implementationClass="org.jetbrains.yaml.YAMLJsonSchemaInCommentCompletionContributor"/>
<pluginSuggestionProvider implementation="org.jetbrains.yaml.swagger.OpenApiSuggestionProvider"/>
<intentionAction>

View File

@@ -0,0 +1,89 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.yaml
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.icons.AllIcons.FileTypes.JsonSchema
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.*
import com.intellij.util.ProcessingContext
import com.intellij.util.asSafely
import com.jetbrains.jsonSchema.ide.JsonSchemaService
import com.jetbrains.jsonSchema.impl.JsonSchemaByCommentProvider
import org.jetbrains.yaml.psi.YAMLDocument
import org.jetbrains.yaml.psi.YAMLMapping
class YAMLJsonSchemaIdReferenceContributor : PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiComment().with(TopDocumentComment), object : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
val charSequence = StringUtil.newBombedCharSequence(element.node.chars, 300)
val inComment = JsonSchemaByCommentProvider.detectInComment(charSequence) ?: return emptyArray()
return arrayOf(JsonSchemaIdReference(element, inComment))
}
}
)
}
}
private object TopDocumentComment : PatternCondition<PsiComment?>("inTopOfDocument") {
override fun accepts(t: PsiComment, context: ProcessingContext?): Boolean {
var element: PsiElement = t
element = element.parent.takeIf { it is YAMLMapping } ?: element
element = element.parent.takeIf { it is YAMLDocument } ?: element
return element.parent is PsiFile
}
}
class JsonSchemaIdReference(element: PsiElement,
rangeInElement: TextRange) : PsiReferenceBase.Immediate<PsiElement>(element, rangeInElement, true, element) {
override fun getVariants(): Array<Any> {
val project = this.element.project
val schemaService = JsonSchemaService.Impl.get(project)
return schemaService.allUserVisibleSchemas
.filter { it.provider == null || it.provider?.remoteSource != null }
.map { jsonSchemaInfo ->
val url = jsonSchemaInfo.getUrl(project)
LookupElementBuilder.create(url).withIcon(JsonSchema)
}.toTypedArray()
}
}
class YAMLJsonSchemaInCommentCompletionContributor : CompletionContributor() {
init {
extend(CompletionType.BASIC, PlatformPatterns.psiComment().with(TopDocumentComment),
object : CompletionProvider<CompletionParameters?>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val psiComment = parameters.originalPosition?.asSafely<PsiComment>() ?: return
val trimmedBody = psiComment.text.removePrefix("#").trimStart()
for (schemaComment in JsonSchemaByCommentProvider.schemaCommentsForCompletion) {
if (trimmedBody.isBlank() ||
trimmedBody.length != schemaComment.length && schemaComment.startsWith(trimmedBody, true)) {
result.addElement(
LookupElementBuilder.create(schemaComment)
.withIcon(JsonSchema)
.withTypeText("JSON Schema specifying comment", true)
.withInsertHandler(
InsertHandler { context: InsertionContext, item: LookupElement? ->
context.laterRunnable = Runnable {
CodeCompletionHandlerBase(CompletionType.BASIC).invokeCompletion(context.project, context.editor)
}
})
)
}
}
}
})
}
}

View File

@@ -0,0 +1,127 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.yaml.schema
import com.intellij.codeInsight.TargetElementUtil
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.impl.source.resolve.reference.PsiReferenceUtil
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration
import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration
import com.jetbrains.jsonSchema.ide.JsonSchemaService
import com.jetbrains.jsonSchema.impl.JsonSchemaVersion
import junit.framework.TestCase
import org.jetbrains.yaml.JsonSchemaIdReference
class YamlSchemaPointerTest : BasePlatformTestCase() {
override fun setUp() {
super.setUp()
val schema = myFixture.configureByText("schema.json", """
{
"${'$'}id": "https://example.com/schemas/address",
"${'$'}schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
""".trimIndent()).virtualFile
JsonSchemaMappingsProjectConfiguration.getInstance(project).setState(mapOf(
UserDefinedJsonSchemaConfiguration("my-schema", JsonSchemaVersion.SCHEMA_7, schema.url, false, emptyList()).let { it.name to it }
))
JsonSchemaService.Impl.get(project).reset()
psiManager.dropPsiCaches()
}
fun `test schema configured by ref`() {
myFixture.configureByText("document.yaml", """
{
"${'$'}schema": https://example.com/schemas/address,
<caret>
}
""".trimIndent()).virtualFile
myFixture.testCompletionVariants("document.yaml", "city")
}
fun `test schema configured by language server comment`() {
myFixture.configureByText("document.yaml", """
# yaml-language-server: ${'$'}schema=https://example.com/schemas/address
<caret>
""".trimIndent())
myFixture.testCompletionVariants("document.yaml", "city")
}
fun `test schema ref available`() {
myFixture.configureByText("document.yaml", """
# yaml-language-server: ${'$'}schema=https://<caret>example.com/schemas/address
""".trimIndent())
val refs = TargetElementUtil.findReference(myFixture.editor)?.let(PsiReferenceUtil::unwrapMultiReference).orEmpty()
TestCase.assertTrue(refs.filterIsInstance<JsonSchemaIdReference>().isNotEmpty())
}
fun `test schema ls completion`() {
myFixture.configureByText("document.yaml", """
# yaml-language-server: ${'$'}schema=<caret>
""".trimIndent())
UsefulTestCase.assertContainsElements(myFixture.getCompletionVariants("document.yaml").orEmpty(),
"http://json-schema.org/draft-06/schema")
}
fun `test schema ls completion in complex comment`() {
myFixture.configureByText("document.yaml", """
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
# yaml-language-server: ${'$'}schema=<caret>
run:
deadline: 5m
go: '1.20'
linters:
enable-all: true
disable:
- cyclop
- exhaustivestruct
- forbidigo
""".trimIndent())
UsefulTestCase.assertContainsElements(myFixture.getCompletionVariants("document.yaml").orEmpty(),
"http://json-schema.org/draft-06/schema")
}
fun `test schema ref completion`() {
myFixture.configureByText("document.yaml", """
# <caret>
""".trimIndent())
UsefulTestCase.assertContainsElements(myFixture.getCompletionVariants("document.yaml").orEmpty(),
"\$schema: ")
myFixture.type("\$schema: ")
PsiDocumentManager.getInstance(project).commitAllDocuments()
FileDocumentManager.getInstance().saveDocument(myFixture.editor.document)
UsefulTestCase.assertContainsElements(myFixture.getCompletionVariants("document.yaml").orEmpty(),
"http://json-schema.org/draft-04/schema")
}
fun `test schema detected`() {
myFixture.configureByText("document.yaml", """
# ${"\$"}schema: https://myexternal-schema-service.com/schemas/def
<caret>
""".trimIndent())
val schemaFilesForFile = JsonSchemaService.Impl.get(project).getSchemaFilesForFile(myFixture.file.virtualFile)
UsefulTestCase.assertSameElements(schemaFilesForFile.map { it.url }, "https://myexternal-schema-service.com/schemas/def")
}
}