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 extends JsonSchemaObject> 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