diff --git a/json/resources/intellij.json.xml b/json/resources/intellij.json.xml index 8f15df7abb97..ee403c1bded1 100644 --- a/json/resources/intellij.json.xml +++ b/json/resources/intellij.json.xml @@ -190,7 +190,7 @@ interface="com.jetbrains.jsonSchema.extension.JsonSchemaNestedCompletionsTreeProvider" dynamic="true"/> - diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionCustomizer.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionCustomizer.java new file mode 100644 index 000000000000..46d32c1909cc --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionCustomizer.java @@ -0,0 +1,38 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.jsonSchema.extension; + +import com.intellij.codeInsight.completion.InsertHandler; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface JsonSchemaCompletionCustomizer { + ExtensionPointName EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaCompletionCustomizer"); + + /** + * Whether this customization is applicable to a file. + * Normally there should be just one customizer per file, otherwise the behavior is not defined + */ + boolean isApplicable(@NotNull PsiFile file); + + /** + * Allows customizing insertion handler for enum values (e.g., to turn a value into a more complicated structure). + * If it returns null, the default handler will be invoked. + */ + default @Nullable InsertHandler createHandlerForEnumValue(JsonSchemaObject schema, String value) { + return null; + } + + /** + * Whether to accept the completion item for a property + */ + default boolean acceptsPropertyCompletionItem(JsonSchemaObject parentSchema, String propertyName, + @Nullable List nestedPath, + @NotNull PsiElement originalPosition) { return true; } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionHandlerProvider.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionHandlerProvider.java deleted file mode 100644 index 3ac88eff5dd3..000000000000 --- a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaCompletionHandlerProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.jsonSchema.extension; - -import com.intellij.codeInsight.completion.InsertHandler; -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.openapi.extensions.ExtensionPointName; -import com.jetbrains.jsonSchema.impl.JsonSchemaObject; -import org.jetbrains.annotations.Nullable; - -public interface JsonSchemaCompletionHandlerProvider { - ExtensionPointName EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaCompletionHandlerProvider"); - - default @Nullable InsertHandler createHandlerForEnumValue(JsonSchemaObject schema, String value) { - return null; - } -} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.kt b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.kt index 8d3af936689f..ad73a6a8a31e 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.kt @@ -41,7 +41,7 @@ import com.intellij.util.ThreeState import com.intellij.util.concurrency.ThreadingAssertions import com.intellij.util.containers.ContainerUtil import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker -import com.jetbrains.jsonSchema.extension.JsonSchemaCompletionHandlerProvider +import com.jetbrains.jsonSchema.extension.JsonSchemaCompletionCustomizer import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider import com.jetbrains.jsonSchema.extension.JsonSchemaNestedCompletionsTreeProvider.Companion.getNestedCompletionsData import com.jetbrains.jsonSchema.extension.SchemaType @@ -178,10 +178,13 @@ class JsonSchemaCompletionContributor : CompletionContributor() { adapter: JsonPropertyAdapter?, knownNames: MutableSet, completionPath: SchemaPath?) { + val customHandlers = JsonSchemaCompletionCustomizer.EXTENSION_POINT_NAME.extensionList + .filter { it.isApplicable(originalPosition.containingFile) } StreamEx.of(schema.propertyNames) .filter { name -> !forbiddenNames.contains(name) && !knownNames.contains(name) || adapter != null && name == adapter.name } .forEach { name -> knownNames.add(name) + if (customHandlers.size == 1 && !customHandlers[0].acceptsPropertyCompletionItem(schema, name, completionPath?.accessor(), originalPosition)) return@forEach val propertySchema = checkNotNull(schema.getPropertyByName(name)) addPropertyVariant(name, propertySchema, completionPath, adapter?.nameValueAdapter) } @@ -194,7 +197,8 @@ class JsonSchemaCompletionContributor : CompletionContributor() { if (schema.enum != null && completionPath == null) { // custom insert handlers are currently applicable only to enum values but can be extended later to cover more cases - val customHandlers = JsonSchemaCompletionHandlerProvider.EXTENSION_POINT_NAME.extensionList + val customHandlers = JsonSchemaCompletionCustomizer.EXTENSION_POINT_NAME.extensionList + .filter { it.isApplicable(originalPosition.containingFile) } val metadata = schema.enumMetadata val isEnumOrderSensitive = schema.readChildNodeValue(X_INTELLIJ_ENUM_ORDER_SENSITIVE).toBoolean() val anEnum = schema.enum @@ -866,4 +870,9 @@ class JsonSchemaCompletionContributor : CompletionContributor() { codeStyleManager.reformatText(context.file, context.startOffset, context.tailOffset + offset) } } -} \ No newline at end of file +} + +class JsonSchemaMetadataEntry( + val key: String, + val values: List +) \ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java index aaab33a78892..ef04ee4bef0a 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java @@ -166,6 +166,10 @@ public abstract class JsonSchemaObject { public abstract @Nullable JsonSchemaObject getSchemaDependencyByName(@NotNull String name); + // custom metadata provided by schemas, can be used in IDE features + // the format in the schema is a key with either a single string value or an array of string values + public abstract @Nullable List getMetadata(); + // also remove? public abstract @Nullable List getAllOf(); diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObjectImpl.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObjectImpl.java index f7dbb9818ddd..e96d25434233 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObjectImpl.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObjectImpl.java @@ -55,6 +55,8 @@ public class JsonSchemaObjectImpl extends JsonSchemaObject { public @Nullable String myLanguageInjectionPrefix; public @Nullable String myLanguageInjectionPostfix; + public @Nullable List myMetadataEntries; + public @Nullable JsonSchemaType myType; public @Nullable Object myDefault; public @Nullable Map myExample; @@ -220,6 +222,15 @@ public class JsonSchemaObjectImpl extends JsonSchemaObject { return myRawFile; } + @Override + public @Nullable List getMetadata() { + return myMetadataEntries; + } + + public void setMetadata(@Nullable List entries) { + myMetadataEntries = entries; + } + public void setLanguageInjection(@Nullable String injection) { myLanguageInjection = injection; } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java index 22f365754934..2d8949b186d0 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java @@ -193,6 +193,8 @@ public final class JsonSchemaReader { (element, object, queue, virtualFile) -> readInjectionMetadata(element, object)); READERS_MAP.put(X_INTELLIJ_ENUM_METADATA, (element, object, queue, virtualFile) -> readEnumMetadata(element, object)); + READERS_MAP.put(X_INTELLIJ_METADATA, + (element, object, queue, virtualFile) -> readCustomMetadata(element, object)); READERS_MAP.put(X_INTELLIJ_CASE_INSENSITIVE, (element, object, queue, virtualFile) -> { if (element.isBooleanLiteral()) object.setForceCaseInsensitive(getBoolean(element)); }); @@ -259,6 +261,29 @@ public final class JsonSchemaReader { READERS_MAP.put("typeof", ((element, object, queue, virtualFile) -> object.setShouldValidateAgainstJSType(true))); } + private static void readCustomMetadata(JsonValueAdapter element, JsonSchemaObjectImpl object) { + if (!(element instanceof JsonObjectValueAdapter)) return; + List filters = new ArrayList<>(); + for (JsonPropertyAdapter adapter : ((JsonObjectValueAdapter)element).getPropertyList()) { + String name = adapter.getName(); + if (name == null) continue; + Collection values = adapter.getValues(); + if (values.size() != 1) continue; + JsonValueAdapter valueAdapter = values.iterator().next(); + if (valueAdapter.isStringLiteral()) { + filters.add(new JsonSchemaMetadataEntry(name, Collections.singletonList(getString(valueAdapter)))); + } + else if (valueAdapter.isArray()) { + filters.add(new JsonSchemaMetadataEntry(name, + Objects.requireNonNull(valueAdapter.getAsArray()).getElements().stream() + .filter(v -> v.isStringLiteral()) + .map(v -> getString(v)) + .toList())); + } + } + object.setMetadata(filters); + } + private static void readEnumMetadata(JsonValueAdapter element, JsonSchemaObjectImpl object) { if (!(element instanceof JsonObjectValueAdapter)) return; Map> metadataMap = new HashMap<>(); diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/legacy/LegacyJsonSchemaObjectMerger.java b/json/src/com/jetbrains/jsonSchema/impl/light/legacy/LegacyJsonSchemaObjectMerger.java index 91e5545350b9..70791c853bdb 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/legacy/LegacyJsonSchemaObjectMerger.java +++ b/json/src/com/jetbrains/jsonSchema/impl/light/legacy/LegacyJsonSchemaObjectMerger.java @@ -85,7 +85,8 @@ public class LegacyJsonSchemaObjectMerger implements JsonSchemaObjectMerger { if (other.getMinProperties() != null) base.setMinProperties(other.getMinProperties()); if (other.getEnum() != null) base.setEnum(other.getEnum()); if (other.getNot() != null) base.setNot(other.getNot()); - if (other.getLanguageInjection() == null) base.setLanguageInjection(other.getLanguageInjection()); + if (other.getLanguageInjection() != null) base.setLanguageInjection(other.getLanguageInjection()); + if (other.getMetadata() != null) base.setMetadata(other.getMetadata()); //computed together because influence each other var mergedExclusionAndType = computeMergedExclusionAndType(base.getType(), other.getType(), other.getTypeVariants()); diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/InheritedJsonSchemaObjectView.kt b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/InheritedJsonSchemaObjectView.kt index 62ad301f7eff..e2ba48320c0a 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/InheritedJsonSchemaObjectView.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/InheritedJsonSchemaObjectView.kt @@ -6,10 +6,7 @@ import com.intellij.util.asSafely import com.jetbrains.jsonSchema.extension.JsonSchemaValidation import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter import com.jetbrains.jsonSchema.ide.JsonSchemaService -import com.jetbrains.jsonSchema.impl.IfThenElse -import com.jetbrains.jsonSchema.impl.JsonSchemaObject -import com.jetbrains.jsonSchema.impl.JsonSchemaType -import com.jetbrains.jsonSchema.impl.MergedJsonSchemaObject +import com.jetbrains.jsonSchema.impl.* import com.jetbrains.jsonSchema.impl.light.legacy.LegacyJsonSchemaObjectMerger import com.jetbrains.jsonSchema.impl.light.versions.JsonSchemaInterpretationStrategy @@ -121,6 +118,10 @@ internal class InheritedJsonSchemaObjectView( return LightweightJsonSchemaObjectMerger.mergeObjects(baseDef, otherDef, otherDef) } + override fun getMetadata(): MutableList? { + return other.metadata + } + override fun hasPatternProperties(): Boolean { return other.hasPatternProperties() } diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/JsonSchemaObjectBackedByJacksonBase.kt b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/JsonSchemaObjectBackedByJacksonBase.kt index f5865e82a2fa..8c2331986eb9 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/JsonSchemaObjectBackedByJacksonBase.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/JsonSchemaObjectBackedByJacksonBase.kt @@ -2,6 +2,7 @@ package com.jetbrains.jsonSchema.impl.light.nodes import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ArrayNode import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.keyFMap.KeyFMap @@ -495,6 +496,19 @@ abstract class JsonSchemaObjectBackedByJacksonBase( return JacksonSchemaNodeAccessor.readTextNodeValue(rawSchemaNode, X_INTELLIJ_HTML_DESCRIPTION) } + override fun getMetadata(): List? { + return JacksonSchemaNodeAccessor.readNodeAsMapEntries(rawSchemaNode, X_INTELLIJ_METADATA) + ?.mapNotNull { + val values = (it.second as? ArrayNode)?.let { + it.elements().asSequence().mapNotNull { + it.takeIf { it.isTextual }?.asText() + }.toList() + } ?: it.second.takeIf { it.isTextual }?.asText()?.let { listOf(it) } + if (values.isNullOrEmpty()) null + else JsonSchemaMetadataEntry(it.first, values) + }?.toList() + } + override fun getLanguageInjection(): String? { return JacksonSchemaNodeAccessor.readTextNodeValue(rawSchemaNode, X_INTELLIJ_LANGUAGE_INJECTION) ?: JacksonSchemaNodeAccessor.readTextNodeValue(rawSchemaNode, X_INTELLIJ_LANGUAGE_INJECTION, LANGUAGE) diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MergedJsonSchemaObjectView.kt b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MergedJsonSchemaObjectView.kt index 874aaab057bc..845c30ee5d86 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MergedJsonSchemaObjectView.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MergedJsonSchemaObjectView.kt @@ -6,10 +6,7 @@ import com.intellij.util.asSafely import com.jetbrains.jsonSchema.extension.JsonSchemaValidation import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter import com.jetbrains.jsonSchema.ide.JsonSchemaService -import com.jetbrains.jsonSchema.impl.IfThenElse -import com.jetbrains.jsonSchema.impl.JsonSchemaObject -import com.jetbrains.jsonSchema.impl.JsonSchemaType -import com.jetbrains.jsonSchema.impl.MergedJsonSchemaObject +import com.jetbrains.jsonSchema.impl.* import com.jetbrains.jsonSchema.impl.light.legacy.LegacyJsonSchemaObjectMerger import com.jetbrains.jsonSchema.impl.light.versions.JsonSchemaInterpretationStrategy @@ -125,6 +122,10 @@ internal class MergedJsonSchemaObjectView( return LightweightJsonSchemaObjectMerger.mergeObjects(baseDef, otherDef, otherDef) } + override fun getMetadata(): MutableList? { + return other.metadata ?: base.metadata + } + override fun hasPatternProperties(): Boolean { return booleanOr(JsonSchemaObject::hasPatternProperties) } diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MissingJsonSchemaObject.kt b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MissingJsonSchemaObject.kt index b5bee89215de..6a72f2e2376f 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MissingJsonSchemaObject.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/nodes/MissingJsonSchemaObject.kt @@ -4,6 +4,7 @@ package com.jetbrains.jsonSchema.impl.light.nodes import com.fasterxml.jackson.databind.node.MissingNode import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.jsonSchema.impl.IfThenElse +import com.jetbrains.jsonSchema.impl.JsonSchemaMetadataEntry import com.jetbrains.jsonSchema.impl.JsonSchemaObject import com.jetbrains.jsonSchema.impl.JsonSchemaType import com.jetbrains.jsonSchema.impl.light.SCHEMA_ROOT_POINTER @@ -265,6 +266,10 @@ internal object MissingJsonSchemaObject : JsonSchemaObjectBackedByJacksonBase(Mi throw UnsupportedOperationException(ERROR_MESSAGE) } + override fun getMetadata(): MutableList? { + throw UnsupportedOperationException(ERROR_MESSAGE) + } + override fun hasChildFieldsExcept(namesToSkip: Array): Boolean { throw UnsupportedOperationException(ERROR_MESSAGE) } diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/schemaKeywords.kt b/json/src/com/jetbrains/jsonSchema/impl/light/schemaKeywords.kt index f7a1333b5445..936924c86e3a 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/schemaKeywords.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/schemaKeywords.kt @@ -69,5 +69,6 @@ const val X_INTELLIJ_LANGUAGE_INJECTION = "x-intellij-language-injection" const val X_INTELLIJ_CASE_INSENSITIVE = "x-intellij-case-insensitive" const val X_INTELLIJ_ENUM_METADATA = "x-intellij-enum-metadata" const val X_INTELLIJ_ENUM_ORDER_SENSITIVE = "x-intellij-enum-order-sensitive" +const val X_INTELLIJ_METADATA = "x-intellij-metadata" internal val ROOT_POINTER_VARIANTS = setOf(SCHEMA_ROOT_POINTER, "#/", "#", "") \ No newline at end of file