mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
[yaml] IDEA-323117 Support for YAML Schema using $schama: and yaml-language-server comments
GitOrigin-RevId: c08a674116f465db810d52a113ff6fc6ed227fd5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ab6e843532
commit
dbb9e11769
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user