[json schema] support adding arbitrary metadata to schema elements and use that facility in amper for filtering of completion items

GitOrigin-RevId: 34d3641afeb2c2d3e6932d33f1e3c091b76e2205
This commit is contained in:
Anton Lobov
2024-06-25 17:42:30 +02:00
committed by intellij-monorepo-bot
parent 0b0fe86bfc
commit c58f381025
13 changed files with 123 additions and 29 deletions

View File

@@ -190,7 +190,7 @@
interface="com.jetbrains.jsonSchema.extension.JsonSchemaNestedCompletionsTreeProvider" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.json.jsonSchemaEnabler" interface="com.jetbrains.jsonSchema.extension.JsonSchemaEnabler"
dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.json.jsonSchemaCompletionHandlerProvider" interface="com.jetbrains.jsonSchema.extension.JsonSchemaCompletionHandlerProvider"
<extensionPoint qualifiedName="com.intellij.json.jsonSchemaCompletionCustomizer" interface="com.jetbrains.jsonSchema.extension.JsonSchemaCompletionCustomizer"
dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.json.jsonWidgetSuppressor"
interface="com.jetbrains.jsonSchema.extension.JsonWidgetSuppressor" dynamic="true"/>

View File

@@ -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<JsonSchemaCompletionCustomizer> 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<LookupElement> 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<String> nestedPath,
@NotNull PsiElement originalPosition) { return true; }
}

View File

@@ -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<JsonSchemaCompletionHandlerProvider> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaCompletionHandlerProvider");
default @Nullable InsertHandler<LookupElement> createHandlerForEnumValue(JsonSchemaObject schema, String value) {
return null;
}
}

View File

@@ -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<String>,
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)
}
}
}
}
class JsonSchemaMetadataEntry(
val key: String,
val values: List<String>
)

View File

@@ -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<JsonSchemaMetadataEntry> getMetadata();
// also remove?
public abstract @Nullable List<? extends JsonSchemaObject> getAllOf();

View File

@@ -55,6 +55,8 @@ public class JsonSchemaObjectImpl extends JsonSchemaObject {
public @Nullable String myLanguageInjectionPrefix;
public @Nullable String myLanguageInjectionPostfix;
public @Nullable List<JsonSchemaMetadataEntry> myMetadataEntries;
public @Nullable JsonSchemaType myType;
public @Nullable Object myDefault;
public @Nullable Map<String, Object> myExample;
@@ -220,6 +222,15 @@ public class JsonSchemaObjectImpl extends JsonSchemaObject {
return myRawFile;
}
@Override
public @Nullable List<JsonSchemaMetadataEntry> getMetadata() {
return myMetadataEntries;
}
public void setMetadata(@Nullable List<JsonSchemaMetadataEntry> entries) {
myMetadataEntries = entries;
}
public void setLanguageInjection(@Nullable String injection) {
myLanguageInjection = injection;
}

View File

@@ -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<JsonSchemaMetadataEntry> filters = new ArrayList<>();
for (JsonPropertyAdapter adapter : ((JsonObjectValueAdapter)element).getPropertyList()) {
String name = adapter.getName();
if (name == null) continue;
Collection<JsonValueAdapter> 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<String, Map<String, String>> metadataMap = new HashMap<>();

View File

@@ -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());

View File

@@ -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<JsonSchemaMetadataEntry>? {
return other.metadata
}
override fun hasPatternProperties(): Boolean {
return other.hasPatternProperties()
}

View File

@@ -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<JsonSchemaMetadataEntry>? {
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)

View File

@@ -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<JsonSchemaMetadataEntry>? {
return other.metadata ?: base.metadata
}
override fun hasPatternProperties(): Boolean {
return booleanOr(JsonSchemaObject::hasPatternProperties)
}

View File

@@ -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<JsonSchemaMetadataEntry>? {
throw UnsupportedOperationException(ERROR_MESSAGE)
}
override fun hasChildFieldsExcept(namesToSkip: Array<String>): Boolean {
throw UnsupportedOperationException(ERROR_MESSAGE)
}

View File

@@ -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, "#/", "#", "")