summaryrefslogtreecommitdiff
path: root/json
diff options
context:
space:
mode:
authorAndrej Shadura <andrew.shadura@collabora.co.uk>2019-08-28 14:13:29 +0200
committerAndrej Shadura <andrew.shadura@collabora.co.uk>2019-08-29 17:48:13 +0200
commite19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch)
tree9e3852cb9abc81ed6aa444465928d45fd7763dea /json
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'json')
-rw-r--r--json/gen/com/intellij/json/JsonElementTypes.java68
-rw-r--r--json/gen/com/intellij/json/JsonParser.java387
-rw-r--r--json/gen/com/intellij/json/_JsonLexer.java708
-rw-r--r--json/gen/com/intellij/json/json5/_Json5Lexer.java730
-rw-r--r--json/gen/com/intellij/json/psi/JsonArray.java17
-rw-r--r--json/gen/com/intellij/json/psi/JsonBooleanLiteral.java12
-rw-r--r--json/gen/com/intellij/json/psi/JsonContainer.java10
-rw-r--r--json/gen/com/intellij/json/psi/JsonElementVisitor.java64
-rw-r--r--json/gen/com/intellij/json/psi/JsonLiteral.java12
-rw-r--r--json/gen/com/intellij/json/psi/JsonNullLiteral.java10
-rw-r--r--json/gen/com/intellij/json/psi/JsonNumberLiteral.java12
-rw-r--r--json/gen/com/intellij/json/psi/JsonObject.java20
-rw-r--r--json/gen/com/intellij/json/psi/JsonProperty.java24
-rw-r--r--json/gen/com/intellij/json/psi/JsonReferenceExpression.java13
-rw-r--r--json/gen/com/intellij/json/psi/JsonStringLiteral.java20
-rw-r--r--json/gen/com/intellij/json/psi/JsonValue.java10
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java40
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java32
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java28
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java32
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java28
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java32
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java40
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java49
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java34
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java44
-rw-r--r--json/gen/com/intellij/json/psi/impl/JsonValueImpl.java28
-rw-r--r--json/intellij.json.iml20
-rw-r--r--json/json.bnf145
-rw-r--r--json/resources/inspectionDescriptions/Json5StandardCompliance.html5
-rw-r--r--json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html5
-rw-r--r--json/resources/inspectionDescriptions/JsonSchemaCompliance.html5
-rw-r--r--json/resources/inspectionDescriptions/JsonSchemaRefReference.html5
-rw-r--r--json/resources/inspectionDescriptions/JsonStandardCompliance.html5
-rw-r--r--json/src/com/intellij/json/JsonBraceMatcher.java34
-rw-r--r--json/src/com/intellij/json/JsonBundle.java36
-rw-r--r--json/src/com/intellij/json/JsonBundle.properties66
-rw-r--r--json/src/com/intellij/json/JsonDialectUtil.java25
-rw-r--r--json/src/com/intellij/json/JsonElementType.java11
-rw-r--r--json/src/com/intellij/json/JsonFileType.java50
-rw-r--r--json/src/com/intellij/json/JsonFileTypeFactory.java15
-rw-r--r--json/src/com/intellij/json/JsonLanguage.java22
-rw-r--r--json/src/com/intellij/json/JsonLexer.java12
-rw-r--r--json/src/com/intellij/json/JsonNamesValidator.java38
-rw-r--r--json/src/com/intellij/json/JsonParserDefinition.java83
-rw-r--r--json/src/com/intellij/json/JsonQuoteHandler.java40
-rw-r--r--json/src/com/intellij/json/JsonSpellcheckerStrategy.java87
-rw-r--r--json/src/com/intellij/json/JsonTokenType.java11
-rw-r--r--json/src/com/intellij/json/JsonUtil.java94
-rw-r--r--json/src/com/intellij/json/_JsonLexer.flex58
-rw-r--r--json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java73
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java61
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java156
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java93
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java21
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java234
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java44
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java84
-rw-r--r--json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java73
-rw-r--r--json/src/com/intellij/json/editor/JsonCommenter.java41
-rw-r--r--json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java169
-rw-r--r--json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java72
-rw-r--r--json/src/com/intellij/json/editor/JsonEditorOptions.java38
-rw-r--r--json/src/com/intellij/json/editor/JsonEnterHandler.java147
-rw-r--r--json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java42
-rw-r--r--json/src/com/intellij/json/editor/JsonTypedHandler.java142
-rw-r--r--json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java110
-rw-r--r--json/src/com/intellij/json/editor/lineMover/JsonLineMover.java197
-rw-r--r--json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java16
-rw-r--r--json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java43
-rw-r--r--json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java123
-rw-r--r--json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java54
-rw-r--r--json/src/com/intellij/json/findUsages/JsonWordScanner.java19
-rw-r--r--json/src/com/intellij/json/formatter/JsonBlock.java221
-rw-r--r--json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java78
-rw-r--r--json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java57
-rw-r--r--json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java42
-rw-r--r--json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java116
-rw-r--r--json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java70
-rw-r--r--json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java111
-rw-r--r--json/src/com/intellij/json/highlighting/JsonColorsPage.java105
-rw-r--r--json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java164
-rw-r--r--json/src/com/intellij/json/json5/Json5FileType.java32
-rw-r--r--json/src/com/intellij/json/json5/Json5FileTypeFactory.java13
-rw-r--r--json/src/com/intellij/json/json5/Json5Language.java17
-rw-r--r--json/src/com/intellij/json/json5/Json5Lexer.java10
-rw-r--r--json/src/com/intellij/json/json5/Json5ParserDefinition.java31
-rw-r--r--json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java36
-rw-r--r--json/src/com/intellij/json/json5/_Json5Lexer.flex64
-rw-r--r--json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java52
-rw-r--r--json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java81
-rw-r--r--json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java20
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonContextType.java22
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java21
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java33
-rw-r--r--json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java20
-rw-r--r--json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java69
-rw-r--r--json/src/com/intellij/json/psi/JsonElement.java10
-rw-r--r--json/src/com/intellij/json/psi/JsonElementGenerator.java79
-rw-r--r--json/src/com/intellij/json/psi/JsonFile.java23
-rw-r--r--json/src/com/intellij/json/psi/JsonParserUtil.java25
-rw-r--r--json/src/com/intellij/json/psi/JsonPsiChangeUtils.java42
-rw-r--r--json/src/com/intellij/json/psi/JsonPsiUtil.java231
-rw-r--r--json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java32
-rw-r--r--json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java194
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonElementImpl.java23
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonFileImpl.java43
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java22
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonObjectMixin.java56
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java42
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java74
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java233
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java17
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java45
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java40
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java32
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewElement.java86
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewModel.java37
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java84
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java57
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java18
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java85
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java19
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonMappingKind.java41
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java60
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java38
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java110
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java40
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java137
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java115
-rw-r--r--json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java323
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java81
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java33
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java31
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java37
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java22
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java132
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java125
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java42
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java138
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java17
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/SchemaType.java23
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java32
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java32
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java33
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java40
-rw-r--r--json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java75
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java41
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java25
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java25
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java192
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java10
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java217
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java262
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java62
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java1147
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java58
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java672
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java157
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java120
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java218
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java171
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java48
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java1180
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java511
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java94
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java68
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java144
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java488
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java193
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java84
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java595
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java60
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java163
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/MatchResult.java74
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java23
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java81
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java79
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java156
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java46
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java89
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java46
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java93
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java92
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java19
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java173
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java125
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java150
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java52
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java219
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java333
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java339
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java104
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java6
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java169
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java82
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java310
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java16
-rw-r--r--json/src/jsonSchema/build.xml126
-rw-r--r--json/src/jsonSchema/schema.json154
-rw-r--r--json/src/jsonSchema/schema06.json158
-rw-r--r--json/src/jsonSchema/schema07.json172
-rw-r--r--json/tests/intellij.json.tests.iml17
-rw-r--r--json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java19
-rw-r--r--json/tests/test/com/intellij/json/JsonCommenterTest.java32
-rw-r--r--json/tests/test/com/intellij/json/JsonCompletionTest.java61
-rw-r--r--json/tests/test/com/intellij/json/JsonCopyPasteTest.java119
-rw-r--r--json/tests/test/com/intellij/json/JsonEditingTest.java76
-rw-r--r--json/tests/test/com/intellij/json/JsonFoldingTest.java38
-rw-r--r--json/tests/test/com/intellij/json/JsonFormattingTest.java112
-rw-r--r--json/tests/test/com/intellij/json/JsonHighlightingTest.java74
-rw-r--r--json/tests/test/com/intellij/json/JsonHighlightingTestBase.java14
-rw-r--r--json/tests/test/com/intellij/json/JsonLexerTest.java34
-rw-r--r--json/tests/test/com/intellij/json/JsonLineMoverTest.java92
-rw-r--r--json/tests/test/com/intellij/json/JsonLiteralApiTest.java111
-rw-r--r--json/tests/test/com/intellij/json/JsonLiveTemplateTest.java101
-rw-r--r--json/tests/test/com/intellij/json/JsonNavigationTest.java20
-rw-r--r--json/tests/test/com/intellij/json/JsonParsingTest.java105
-rw-r--r--json/tests/test/com/intellij/json/JsonPsiUtilTest.java69
-rw-r--r--json/tests/test/com/intellij/json/JsonQuickFixTest.java54
-rw-r--r--json/tests/test/com/intellij/json/JsonRenameTest.java28
-rw-r--r--json/tests/test/com/intellij/json/JsonSmartEnterTest.java43
-rw-r--r--json/tests/test/com/intellij/json/JsonSpellcheckerTest.java73
-rw-r--r--json/tests/test/com/intellij/json/JsonStructureViewTest.java165
-rw-r--r--json/tests/test/com/intellij/json/JsonSurroundWithTest.java56
-rw-r--r--json/tests/test/com/intellij/json/JsonTestCase.java50
-rw-r--r--json/tests/test/com/intellij/json/JsonTypingHandlingTest.java118
-rw-r--r--json/tests/test/com/intellij/json/JsonWordSelectionTest.java17
-rw-r--r--json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java18
-rw-r--r--json/tests/test/com/intellij/json/json5/Json5ParsingTest.java27
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java89
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java892
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java33
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java87
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java1051
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java69
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java82
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java51
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java187
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java79
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java42
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java39
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java66
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java49
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java41
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt349
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java163
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java83
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java75
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java161
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java14
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java63
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java47
-rw-r--r--json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java16
263 files changed, 26879 insertions, 0 deletions
diff --git a/json/gen/com/intellij/json/JsonElementTypes.java b/json/gen/com/intellij/json/JsonElementTypes.java
new file mode 100644
index 00000000..be5505cf
--- /dev/null
+++ b/json/gen/com/intellij/json/JsonElementTypes.java
@@ -0,0 +1,68 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json;
+
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.PsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.json.psi.impl.*;
+
+public interface JsonElementTypes {
+
+ IElementType ARRAY = new JsonElementType("ARRAY");
+ IElementType BOOLEAN_LITERAL = new JsonElementType("BOOLEAN_LITERAL");
+ IElementType LITERAL = new JsonElementType("LITERAL");
+ IElementType NULL_LITERAL = new JsonElementType("NULL_LITERAL");
+ IElementType NUMBER_LITERAL = new JsonElementType("NUMBER_LITERAL");
+ IElementType OBJECT = new JsonElementType("OBJECT");
+ IElementType PROPERTY = new JsonElementType("PROPERTY");
+ IElementType REFERENCE_EXPRESSION = new JsonElementType("REFERENCE_EXPRESSION");
+ IElementType STRING_LITERAL = new JsonElementType("STRING_LITERAL");
+ IElementType VALUE = new JsonElementType("VALUE");
+
+ IElementType BLOCK_COMMENT = new JsonTokenType("BLOCK_COMMENT");
+ IElementType COLON = new JsonTokenType(":");
+ IElementType COMMA = new JsonTokenType(",");
+ IElementType DOUBLE_QUOTED_STRING = new JsonTokenType("DOUBLE_QUOTED_STRING");
+ IElementType FALSE = new JsonTokenType("false");
+ IElementType IDENTIFIER = new JsonTokenType("IDENTIFIER");
+ IElementType LINE_COMMENT = new JsonTokenType("LINE_COMMENT");
+ IElementType L_BRACKET = new JsonTokenType("[");
+ IElementType L_CURLY = new JsonTokenType("{");
+ IElementType NULL = new JsonTokenType("null");
+ IElementType NUMBER = new JsonTokenType("NUMBER");
+ IElementType R_BRACKET = new JsonTokenType("]");
+ IElementType R_CURLY = new JsonTokenType("}");
+ IElementType SINGLE_QUOTED_STRING = new JsonTokenType("SINGLE_QUOTED_STRING");
+ IElementType TRUE = new JsonTokenType("true");
+
+ class Factory {
+ public static PsiElement createElement(ASTNode node) {
+ IElementType type = node.getElementType();
+ if (type == ARRAY) {
+ return new JsonArrayImpl(node);
+ }
+ else if (type == BOOLEAN_LITERAL) {
+ return new JsonBooleanLiteralImpl(node);
+ }
+ else if (type == NULL_LITERAL) {
+ return new JsonNullLiteralImpl(node);
+ }
+ else if (type == NUMBER_LITERAL) {
+ return new JsonNumberLiteralImpl(node);
+ }
+ else if (type == OBJECT) {
+ return new JsonObjectImpl(node);
+ }
+ else if (type == PROPERTY) {
+ return new JsonPropertyImpl(node);
+ }
+ else if (type == REFERENCE_EXPRESSION) {
+ return new JsonReferenceExpressionImpl(node);
+ }
+ else if (type == STRING_LITERAL) {
+ return new JsonStringLiteralImpl(node);
+ }
+ throw new AssertionError("Unknown element type: " + type);
+ }
+ }
+}
diff --git a/json/gen/com/intellij/json/JsonParser.java b/json/gen/com/intellij/json/JsonParser.java
new file mode 100644
index 00000000..39dcfc55
--- /dev/null
+++ b/json/gen/com/intellij/json/JsonParser.java
@@ -0,0 +1,387 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json;
+
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.PsiBuilder.Marker;
+import static com.intellij.json.JsonElementTypes.*;
+import static com.intellij.json.psi.JsonParserUtil.*;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.lang.PsiParser;
+import com.intellij.lang.LightPsiParser;
+
+@SuppressWarnings({"SimplifiableIfStatement", "UnusedAssignment"})
+public class JsonParser implements PsiParser, LightPsiParser {
+
+ public ASTNode parse(IElementType t, PsiBuilder b) {
+ parseLight(t, b);
+ return b.getTreeBuilt();
+ }
+
+ public void parseLight(IElementType t, PsiBuilder b) {
+ boolean r;
+ b = adapt_builder_(t, b, this, EXTENDS_SETS_);
+ Marker m = enter_section_(b, 0, _COLLAPSE_, null);
+ if (t == ARRAY) {
+ r = array(b, 0);
+ }
+ else if (t == BOOLEAN_LITERAL) {
+ r = boolean_literal(b, 0);
+ }
+ else if (t == LITERAL) {
+ r = literal(b, 0);
+ }
+ else if (t == NULL_LITERAL) {
+ r = null_literal(b, 0);
+ }
+ else if (t == NUMBER_LITERAL) {
+ r = number_literal(b, 0);
+ }
+ else if (t == OBJECT) {
+ r = object(b, 0);
+ }
+ else if (t == PROPERTY) {
+ r = property(b, 0);
+ }
+ else if (t == REFERENCE_EXPRESSION) {
+ r = reference_expression(b, 0);
+ }
+ else if (t == STRING_LITERAL) {
+ r = string_literal(b, 0);
+ }
+ else if (t == VALUE) {
+ r = value(b, 0);
+ }
+ else {
+ r = parse_root_(t, b, 0);
+ }
+ exit_section_(b, 0, m, t, r, true, TRUE_CONDITION);
+ }
+
+ protected boolean parse_root_(IElementType t, PsiBuilder b, int l) {
+ return json(b, l + 1);
+ }
+
+ public static final TokenSet[] EXTENDS_SETS_ = new TokenSet[] {
+ create_token_set_(ARRAY, BOOLEAN_LITERAL, LITERAL, NULL_LITERAL,
+ NUMBER_LITERAL, OBJECT, REFERENCE_EXPRESSION, STRING_LITERAL,
+ VALUE),
+ };
+
+ /* ********************************************************** */
+ // '[' array_element* ']'
+ public static boolean array(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "array")) return false;
+ if (!nextTokenIs(b, L_BRACKET)) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_, ARRAY, null);
+ r = consumeToken(b, L_BRACKET);
+ p = r; // pin = 1
+ r = r && report_error_(b, array_1(b, l + 1));
+ r = p && consumeToken(b, R_BRACKET) && r;
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ // array_element*
+ private static boolean array_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "array_1")) return false;
+ while (true) {
+ int c = current_position_(b);
+ if (!array_element(b, l + 1)) break;
+ if (!empty_element_parsed_guard_(b, "array_1", c)) break;
+ }
+ return true;
+ }
+
+ /* ********************************************************** */
+ // value (','|&']')
+ static boolean array_element(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "array_element")) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_);
+ r = value(b, l + 1);
+ p = r; // pin = 1
+ r = r && array_element_1(b, l + 1);
+ exit_section_(b, l, m, r, p, not_bracket_or_next_value_parser_);
+ return r || p;
+ }
+
+ // ','|&']'
+ private static boolean array_element_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "array_element_1")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, COMMA);
+ if (!r) r = array_element_1_1(b, l + 1);
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ // &']'
+ private static boolean array_element_1_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "array_element_1_1")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _AND_);
+ r = consumeToken(b, R_BRACKET);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // TRUE | FALSE
+ public static boolean boolean_literal(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "boolean_literal")) return false;
+ if (!nextTokenIs(b, "<boolean literal>", FALSE, TRUE)) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _NONE_, BOOLEAN_LITERAL, "<boolean literal>");
+ r = consumeToken(b, TRUE);
+ if (!r) r = consumeToken(b, FALSE);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // value+
+ static boolean json(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "json")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = value(b, l + 1);
+ while (r) {
+ int c = current_position_(b);
+ if (!value(b, l + 1)) break;
+ if (!empty_element_parsed_guard_(b, "json", c)) break;
+ }
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // string_literal | number_literal | boolean_literal | null_literal
+ public static boolean literal(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "literal")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _COLLAPSE_, LITERAL, "<literal>");
+ r = string_literal(b, l + 1);
+ if (!r) r = number_literal(b, l + 1);
+ if (!r) r = boolean_literal(b, l + 1);
+ if (!r) r = null_literal(b, l + 1);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // !('}'|value)
+ static boolean not_brace_or_next_value(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "not_brace_or_next_value")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _NOT_);
+ r = !not_brace_or_next_value_0(b, l + 1);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ // '}'|value
+ private static boolean not_brace_or_next_value_0(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "not_brace_or_next_value_0")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, R_CURLY);
+ if (!r) r = value(b, l + 1);
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // !(']'|value)
+ static boolean not_bracket_or_next_value(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "not_bracket_or_next_value")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _NOT_);
+ r = !not_bracket_or_next_value_0(b, l + 1);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ // ']'|value
+ private static boolean not_bracket_or_next_value_0(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "not_bracket_or_next_value_0")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, R_BRACKET);
+ if (!r) r = value(b, l + 1);
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // NULL
+ public static boolean null_literal(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "null_literal")) return false;
+ if (!nextTokenIs(b, NULL)) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, NULL);
+ exit_section_(b, m, NULL_LITERAL, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // NUMBER
+ public static boolean number_literal(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "number_literal")) return false;
+ if (!nextTokenIs(b, NUMBER)) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, NUMBER);
+ exit_section_(b, m, NUMBER_LITERAL, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // '{' object_element* '}'
+ public static boolean object(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "object")) return false;
+ if (!nextTokenIs(b, L_CURLY)) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_, OBJECT, null);
+ r = consumeToken(b, L_CURLY);
+ p = r; // pin = 1
+ r = r && report_error_(b, object_1(b, l + 1));
+ r = p && consumeToken(b, R_CURLY) && r;
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ // object_element*
+ private static boolean object_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "object_1")) return false;
+ while (true) {
+ int c = current_position_(b);
+ if (!object_element(b, l + 1)) break;
+ if (!empty_element_parsed_guard_(b, "object_1", c)) break;
+ }
+ return true;
+ }
+
+ /* ********************************************************** */
+ // property (','|&'}')
+ static boolean object_element(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "object_element")) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_);
+ r = property(b, l + 1);
+ p = r; // pin = 1
+ r = r && object_element_1(b, l + 1);
+ exit_section_(b, l, m, r, p, not_brace_or_next_value_parser_);
+ return r || p;
+ }
+
+ // ','|&'}'
+ private static boolean object_element_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "object_element_1")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, COMMA);
+ if (!r) r = object_element_1_1(b, l + 1);
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ // &'}'
+ private static boolean object_element_1_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "object_element_1_1")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _AND_);
+ r = consumeToken(b, R_CURLY);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // property_name (':' value)
+ public static boolean property(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "property")) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_, PROPERTY, "<property>");
+ r = property_name(b, l + 1);
+ p = r; // pin = 1
+ r = r && property_1(b, l + 1);
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ // ':' value
+ private static boolean property_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "property_1")) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_);
+ r = consumeToken(b, COLON);
+ p = r; // pin = 1
+ r = r && value(b, l + 1);
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ /* ********************************************************** */
+ // literal | reference_expression
+ static boolean property_name(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "property_name")) return false;
+ boolean r;
+ r = literal(b, l + 1);
+ if (!r) r = reference_expression(b, l + 1);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // IDENTIFIER
+ public static boolean reference_expression(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "reference_expression")) return false;
+ if (!nextTokenIs(b, IDENTIFIER)) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeToken(b, IDENTIFIER);
+ exit_section_(b, m, REFERENCE_EXPRESSION, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // SINGLE_QUOTED_STRING | DOUBLE_QUOTED_STRING
+ public static boolean string_literal(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "string_literal")) return false;
+ if (!nextTokenIs(b, "<string literal>", DOUBLE_QUOTED_STRING, SINGLE_QUOTED_STRING)) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _NONE_, STRING_LITERAL, "<string literal>");
+ r = consumeToken(b, SINGLE_QUOTED_STRING);
+ if (!r) r = consumeToken(b, DOUBLE_QUOTED_STRING);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // object | array | literal | reference_expression
+ public static boolean value(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "value")) return false;
+ boolean r;
+ Marker m = enter_section_(b, l, _COLLAPSE_, VALUE, "<value>");
+ r = object(b, l + 1);
+ if (!r) r = array(b, l + 1);
+ if (!r) r = literal(b, l + 1);
+ if (!r) r = reference_expression(b, l + 1);
+ exit_section_(b, l, m, r, false, null);
+ return r;
+ }
+
+ final static Parser not_brace_or_next_value_parser_ = new Parser() {
+ public boolean parse(PsiBuilder b, int l) {
+ return not_brace_or_next_value(b, l + 1);
+ }
+ };
+ final static Parser not_bracket_or_next_value_parser_ = new Parser() {
+ public boolean parse(PsiBuilder b, int l) {
+ return not_bracket_or_next_value(b, l + 1);
+ }
+ };
+}
diff --git a/json/gen/com/intellij/json/_JsonLexer.java b/json/gen/com/intellij/json/_JsonLexer.java
new file mode 100644
index 00000000..1f5df96f
--- /dev/null
+++ b/json/gen/com/intellij/json/_JsonLexer.java
@@ -0,0 +1,708 @@
+/* The following code was generated by JFlex 1.7.0 tweaked for IntelliJ platform */
+
+package com.intellij.json;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+
+/**
+ * This class is a scanner generated by
+ * <a href="http://www.jflex.de/">JFlex</a> 1.7.0
+ * from the specification file <tt>_JsonLexer.flex</tt>
+ */
+public class _JsonLexer implements FlexLexer {
+
+ /** This character denotes the end of file */
+ public static final int YYEOF = -1;
+
+ /** initial size of the lookahead buffer */
+ private static final int ZZ_BUFFERSIZE = 16384;
+
+ /** lexical states */
+ public static final int YYINITIAL = 0;
+
+ /**
+ * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
+ * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
+ * at the beginning of a line
+ * l is of the form l = 2*k, k a non negative integer
+ */
+ private static final int ZZ_LEXSTATE[] = {
+ 0, 0
+ };
+
+ /**
+ * Translates characters to character classes
+ * Chosen bits are [12, 6, 3]
+ * Total runtime size is 13376 bytes
+ */
+ public static int ZZ_CMAP(int ch) {
+ return ZZ_CMAP_A[(ZZ_CMAP_Y[(ZZ_CMAP_Z[ch>>9]<<6)|((ch>>3)&0x3f)]<<3)|(ch&0x7)];
+ }
+
+ /* The ZZ_CMAP_Z table has 2176 entries */
+ static final char ZZ_CMAP_Z[] = zzUnpackCMap(
+ "\1\0\1\1\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\11\1\12\1\13\1\14\1\15\1\16\1\17\1"+
+ "\20\5\21\1\22\1\23\1\24\1\21\14\25\1\26\50\25\1\27\2\25\1\30\1\31\1\32\1\33"+
+ "\25\25\1\34\20\21\1\35\1\36\1\37\1\40\1\41\1\42\1\43\1\21\1\44\1\45\1\46\1"+
+ "\21\1\47\2\21\1\50\4\21\1\25\1\51\1\52\5\21\2\25\1\53\31\21\1\25\1\54\1\21"+
+ "\1\55\40\21\1\56\17\21\1\57\1\60\1\61\1\62\13\21\1\63\10\21\123\25\1\64\7"+
+ "\25\1\65\1\66\37\21\1\25\1\66\u0582\21\1\67\u017f\21");
+
+ /* The ZZ_CMAP_Y table has 3584 entries */
+ static final char ZZ_CMAP_Y[] = zzUnpackCMap(
+ "\1\0\1\1\1\0\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\0\1\11\1\12\1\13\1\14\1\15\1\16"+
+ "\3\0\1\17\1\20\1\21\1\22\2\0\1\23\3\0\1\23\71\0\1\24\1\0\1\25\1\26\1\27\1"+
+ "\30\2\26\16\0\1\31\1\32\1\33\1\34\2\0\1\35\11\0\1\36\21\0\1\35\1\37\23\0\1"+
+ "\26\1\40\3\0\1\23\1\41\1\40\4\0\1\42\1\40\4\0\1\36\1\43\1\26\3\0\2\44\1\26"+
+ "\1\27\1\45\1\0\1\44\11\0\1\24\14\0\1\46\1\36\1\0\1\47\1\0\1\50\1\26\1\42\7"+
+ "\0\1\51\14\0\1\25\1\26\6\0\1\52\1\22\5\0\1\52\2\26\3\0\1\2\10\26\1\47\1\27"+
+ "\6\26\1\53\2\0\1\23\14\0\1\54\1\0\2\40\1\55\1\50\1\56\2\0\1\47\1\57\1\60\1"+
+ "\50\1\61\1\42\1\62\1\54\1\0\1\2\1\45\1\55\1\63\1\56\2\0\1\47\1\64\1\65\1\63"+
+ "\1\66\1\41\1\67\1\70\1\0\1\52\1\26\1\55\1\36\1\35\2\0\1\47\1\71\1\60\1\36"+
+ "\1\72\1\73\1\26\1\54\1\0\1\41\1\26\1\55\1\50\1\56\2\0\1\47\1\71\1\60\1\50"+
+ "\1\66\1\70\1\62\1\54\1\0\1\41\1\26\1\74\1\75\1\76\1\77\1\100\1\75\1\0\1\24"+
+ "\1\75\1\76\1\101\1\26\1\70\1\0\1\26\1\41\1\55\1\31\1\47\2\0\1\47\1\46\1\102"+
+ "\1\31\1\76\1\103\1\25\1\54\1\0\2\26\1\74\1\31\1\47\2\0\1\47\1\46\1\60\1\31"+
+ "\1\76\1\103\1\33\1\54\1\0\1\104\1\26\1\74\1\31\1\47\4\0\1\51\1\31\1\105\1"+
+ "\42\1\26\1\54\1\0\1\26\1\37\1\74\1\0\1\23\1\37\2\0\1\35\1\106\1\23\1\107\1"+
+ "\110\1\0\2\26\1\111\1\26\1\40\6\0\1\63\1\0\1\23\1\0\1\25\4\26\1\112\1\113"+
+ "\1\53\1\40\1\114\1\74\1\0\1\72\1\110\1\52\1\0\1\60\4\26\1\73\2\26\1\25\1\0"+
+ "\1\25\1\115\1\116\1\0\1\40\3\0\1\27\1\40\1\0\1\31\2\0\1\40\3\0\1\27\1\33\7"+
+ "\26\11\0\1\25\11\0\1\52\4\0\1\36\1\21\5\0\1\117\51\0\1\76\1\23\1\76\5\0\1"+
+ "\76\4\0\1\76\1\23\1\76\1\0\1\23\7\0\1\76\10\0\1\51\4\26\2\0\2\26\12\0\1\27"+
+ "\1\26\1\40\114\0\1\50\2\0\1\120\2\0\1\44\11\0\1\75\1\73\1\26\1\0\1\31\1\27"+
+ "\1\26\2\0\1\27\1\26\2\0\1\2\1\26\1\0\1\31\1\121\1\26\12\0\1\122\1\123\1\0"+
+ "\1\25\3\26\1\123\1\0\1\25\13\0\1\26\5\0\1\44\10\0\1\52\1\26\3\0\1\27\1\0\1"+
+ "\2\1\0\1\2\1\70\4\0\1\52\1\27\1\26\5\0\1\2\3\0\1\25\1\0\1\25\4\26\3\0\1\2"+
+ "\7\0\1\23\3\0\1\50\1\0\1\25\1\0\1\25\1\42\13\26\11\0\1\2\1\0\1\25\1\26\1\124"+
+ "\1\2\1\26\16\0\1\2\1\26\7\0\1\26\1\0\1\102\5\0\1\52\12\26\1\117\3\0\1\23\1"+
+ "\26\34\0\1\23\2\26\1\53\42\0\2\52\4\0\2\52\1\0\1\125\3\0\1\52\6\0\1\31\1\110"+
+ "\1\126\1\27\1\54\1\2\1\0\1\27\1\126\1\27\1\127\1\130\3\26\1\131\1\26\1\42"+
+ "\1\73\1\26\1\132\1\133\1\27\1\37\1\41\1\42\2\26\1\0\1\27\3\0\1\44\2\26\1\0"+
+ "\1\27\1\134\1\0\1\73\1\26\1\107\1\37\1\106\1\135\1\30\1\136\1\0\1\60\1\137"+
+ "\1\140\2\26\5\0\1\73\116\26\5\0\1\23\5\0\1\23\20\0\1\27\1\124\1\2\1\26\4\0"+
+ "\1\36\1\21\7\0\1\42\1\26\1\42\2\0\1\23\1\26\10\23\4\0\5\26\1\42\72\26\1\141"+
+ "\3\26\1\40\1\0\1\135\1\27\1\40\11\0\1\23\1\142\1\40\12\0\1\117\1\137\4\0\1"+
+ "\52\1\40\12\0\1\23\2\26\3\0\1\44\6\26\170\0\1\52\11\26\71\0\1\27\6\26\21\0"+
+ "\1\27\10\26\5\0\1\52\41\0\1\27\3\0\1\2\2\26\6\0\1\53\1\36\3\0\1\42\12\0\1"+
+ "\25\3\26\1\42\1\0\1\37\14\0\1\61\1\2\1\26\1\0\1\44\11\26\6\0\2\26\1\73\6\0"+
+ "\1\2\1\26\10\0\1\27\1\26\1\0\1\25\3\0\1\45\5\0\1\52\4\0\1\2\1\26\3\0\1\27"+
+ "\10\0\1\73\1\42\1\0\1\25\4\26\6\0\1\23\1\26\1\0\1\52\1\0\1\25\2\0\1\23\1\111"+
+ "\10\0\1\44\2\26\1\123\2\0\1\143\1\26\3\144\1\26\2\23\22\26\5\0\1\145\1\0\1"+
+ "\25\64\0\1\2\1\26\2\0\1\23\1\124\5\0\1\2\40\26\55\0\1\52\15\0\1\25\4\26\1"+
+ "\23\1\26\1\124\1\137\1\0\1\47\1\23\1\110\1\146\15\0\1\25\3\26\1\124\54\0\1"+
+ "\52\2\26\10\0\1\37\6\0\5\26\1\0\1\27\2\0\2\26\1\23\1\26\1\100\2\26\1\137\3"+
+ "\26\1\41\1\31\20\0\1\50\1\132\1\26\1\0\1\25\1\40\2\0\1\63\1\40\2\0\1\44\1"+
+ "\70\12\0\1\23\3\37\1\147\1\150\2\26\1\151\1\0\1\46\2\0\1\23\2\0\1\152\1\0"+
+ "\1\52\1\0\1\52\4\26\17\0\1\44\10\26\6\0\1\27\20\26\1\21\20\26\3\0\1\27\6\0"+
+ "\1\73\5\26\3\0\1\23\2\26\3\0\1\44\6\26\3\0\1\52\4\0\1\2\1\0\1\135\5\26\23"+
+ "\0\1\52\1\0\1\25\52\26\1\52\1\47\4\0\1\36\1\153\2\0\1\52\25\26\2\0\1\52\1"+
+ "\26\3\0\1\25\10\26\7\0\1\70\10\26\1\154\1\53\1\46\1\40\2\0\1\2\1\63\4\26\3"+
+ "\0\1\27\20\26\6\0\1\52\1\26\2\0\1\52\1\26\2\0\1\44\21\26\11\0\1\73\66\26\10"+
+ "\0\1\23\3\26\1\70\1\0\2\26\7\0\1\155\2\26\3\0\1\73\1\0\1\25\6\0\1\31\1\0\10"+
+ "\26\10\0\1\27\1\26\1\0\1\25\24\26\7\0\1\26\1\0\1\25\46\26\55\0\1\23\22\26"+
+ "\14\0\1\44\63\26\5\0\1\23\72\26\7\0\1\73\130\26\10\0\1\27\1\26\5\0\1\23\1"+
+ "\26\1\42\2\0\14\26\1\25\153\26\1\137\1\102\2\0\1\51\1\2\3\26\1\32\22\26\1"+
+ "\147\67\26\12\0\1\31\10\0\1\31\1\156\1\157\1\0\1\160\1\46\7\0\1\36\1\51\2"+
+ "\31\3\0\1\161\1\110\1\37\1\47\51\0\1\52\3\0\1\47\2\0\1\117\3\0\1\117\2\0\1"+
+ "\31\3\0\1\31\2\0\1\23\3\0\1\23\3\0\1\47\3\0\1\47\2\0\1\117\1\54\6\0\1\46\3"+
+ "\0\1\112\1\40\1\117\1\162\1\107\1\163\1\112\1\125\1\112\2\117\1\67\1\0\1\35"+
+ "\1\0\1\2\1\55\1\35\1\0\1\2\50\26\32\0\1\23\5\26\106\0\1\27\1\26\33\0\1\52"+
+ "\74\26\1\41\3\26\14\0\20\26\36\0\2\26");
+
+ /* The ZZ_CMAP_A table has 928 entries */
+ static final char ZZ_CMAP_A[] = zzUnpackCMap(
+ "\11\30\1\3\1\2\2\1\1\2\6\30\4\0\1\3\1\30\1\7\1\0\1\30\2\0\1\11\2\30\1\6\1"+
+ "\17\1\35\1\12\1\15\1\4\1\13\11\14\1\36\1\0\3\30\1\0\5\30\1\16\3\30\1\20\4"+
+ "\30\1\26\4\30\1\33\1\10\1\34\2\30\1\0\1\27\3\30\1\41\1\22\2\30\1\23\2\30\1"+
+ "\42\1\30\1\21\3\30\1\37\1\43\1\24\1\40\3\30\1\25\1\30\1\31\1\0\1\32\7\30\1"+
+ "\5\2\30\1\3\1\0\4\30\4\0\1\30\2\0\1\30\7\0\1\30\4\0\1\30\5\0\7\30\1\0\2\30"+
+ "\4\0\4\30\16\0\5\30\7\0\1\30\1\0\1\30\1\0\5\30\1\0\2\30\2\0\4\30\10\0\1\30"+
+ "\1\0\3\30\1\0\1\30\1\0\4\30\1\0\13\30\1\0\1\30\2\0\6\30\1\0\7\30\1\0\1\30"+
+ "\15\0\1\30\1\0\2\30\1\0\2\30\1\0\4\30\10\0\1\30\4\0\4\30\1\0\4\30\1\0\13\30"+
+ "\2\0\4\30\2\0\11\30\6\0\10\30\2\0\2\30\1\0\3\30\1\0\4\30\2\0\6\30\1\0\1\30"+
+ "\3\0\4\30\2\0\5\30\2\0\4\30\5\0\2\30\1\0\4\30\4\0\2\30\1\0\2\30\1\0\2\30\1"+
+ "\0\2\30\2\0\1\30\1\0\3\30\2\0\3\30\3\0\4\30\1\0\1\30\7\0\3\30\1\0\2\30\1\0"+
+ "\5\30\1\0\3\30\2\0\1\30\11\0\2\30\1\0\6\30\3\0\3\30\1\0\4\30\3\0\2\30\1\0"+
+ "\1\30\1\0\2\30\3\0\2\30\3\0\1\30\6\0\3\30\3\0\3\30\5\0\2\30\2\0\2\30\5\0\1"+
+ "\30\1\0\5\30\1\0\4\30\1\0\1\30\4\0\1\30\4\0\6\30\1\0\1\30\3\0\2\30\5\0\2\30"+
+ "\1\0\1\30\2\0\2\30\1\0\1\30\2\0\1\30\3\0\3\30\1\0\1\30\1\0\1\30\5\0\1\30\1"+
+ "\0\1\30\1\0\1\30\4\0\5\30\1\0\4\30\1\3\10\30\1\0\2\30\4\0\4\30\3\0\1\30\3"+
+ "\0\3\30\5\0\5\30\1\0\1\30\1\0\1\30\1\0\1\30\1\0\1\30\2\0\3\30\1\0\2\30\13"+
+ "\3\5\30\2\1\5\30\1\3\4\0\1\30\12\0\1\3\1\0\1\30\3\0\3\30\1\0\5\30\2\0\1\30"+
+ "\1\0\4\30\1\0\1\30\5\0\5\30\4\0\1\30\1\0\1\3\4\0\3\30\1\0\2\30\2\0\3\30\2"+
+ "\0\5\30\2\0\6\30\1\0\3\30\1\0\2\30\2\0\2\30\1\0\2\30\1\0\2\30\2\0\3\30\3\0"+
+ "\2\30\3\0\2\30\2\0\3\30\4\0\3\30\1\0\2\30\1\0\2\30\3\0\1\30\2\0\5\30\1\0\2"+
+ "\30\1\0\3\30\2\0\1\30\4\0\1\30\2\0\2\30\2\0\4\30\1\0\4\30\1\0\1\30\1\0\5\30"+
+ "\1\0\4\30\2\0\1\30\1\0\1\30\5\0\1\30\1\0\1\30\1\0\3\30");
+
+ /**
+ * Translates DFA states to action switch labels.
+ */
+ private static final int [] ZZ_ACTION = zzUnpackAction();
+
+ private static final String ZZ_ACTION_PACKED_0 =
+ "\1\0\1\1\1\2\1\3\1\2\1\3\1\4\1\5"+
+ "\1\3\2\6\5\3\1\7\1\10\1\11\1\12\1\13"+
+ "\1\14\1\15\1\16\1\4\2\0\1\5\1\3\1\6"+
+ "\5\3\1\15\1\16\1\3\3\6\4\3\1\6\1\0"+
+ "\1\16\1\3\1\17\1\3\1\20\1\16\1\3\1\21"+
+ "\2\3";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[57];
+ int offset = 0;
+ offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAction(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Translates a state to a row index in the transition table
+ */
+ private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
+
+ private static final String ZZ_ROWMAP_PACKED_0 =
+ "\0\0\0\44\0\110\0\154\0\220\0\264\0\330\0\374"+
+ "\0\u0120\0\u0144\0\u0168\0\u018c\0\u01b0\0\u01d4\0\u01f8\0\u021c"+
+ "\0\44\0\44\0\44\0\44\0\44\0\44\0\u0240\0\u0264"+
+ "\0\44\0\u0288\0\u02ac\0\44\0\u02d0\0\u02f4\0\u0318\0\u033c"+
+ "\0\u0360\0\u0384\0\u03a8\0\u03cc\0\u03f0\0\u0414\0\u0438\0\u045c"+
+ "\0\u0480\0\u04a4\0\u04c8\0\u04ec\0\u0510\0\264\0\u0534\0\264"+
+ "\0\u0558\0\264\0\u057c\0\264\0\44\0\u05a0\0\264\0\u05c4"+
+ "\0\u05e8";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[57];
+ int offset = 0;
+ offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int high = packed.charAt(i++) << 16;
+ result[j++] = high | packed.charAt(i++);
+ }
+ return j;
+ }
+
+ /**
+ * The transition table of the DFA
+ */
+ private static final int [] ZZ_TRANS = zzUnpackTrans();
+
+ private static final String ZZ_TRANS_PACKED_0 =
+ "\1\2\3\3\1\4\1\5\1\6\1\7\1\2\1\10"+
+ "\1\11\1\12\1\13\2\6\1\2\1\14\1\15\1\16"+
+ "\1\6\1\17\1\6\1\20\2\6\1\21\1\22\1\23"+
+ "\1\24\1\25\1\26\5\6\45\0\3\3\1\0\1\3"+
+ "\42\0\1\27\1\6\1\30\3\0\5\6\1\0\11\6"+
+ "\6\0\5\6\1\0\3\3\1\6\1\5\1\6\3\0"+
+ "\5\6\1\0\11\6\6\0\5\6\4\0\3\6\3\0"+
+ "\5\6\1\0\11\6\6\0\5\6\2\7\1\0\4\7"+
+ "\1\31\1\32\33\7\2\10\1\0\5\10\1\33\1\34"+
+ "\32\10\4\0\3\6\3\0\1\6\1\12\1\13\2\6"+
+ "\1\0\1\14\10\6\6\0\5\6\4\0\3\6\3\0"+
+ "\3\6\1\35\1\36\1\0\11\6\6\0\2\6\1\36"+
+ "\2\6\4\0\3\6\3\0\1\6\2\13\1\35\1\36"+
+ "\1\0\11\6\6\0\2\6\1\36\2\6\4\0\3\6"+
+ "\3\0\5\6\1\0\1\6\1\37\7\6\6\0\5\6"+
+ "\4\0\3\6\3\0\5\6\1\0\11\6\6\0\1\6"+
+ "\1\40\3\6\4\0\3\6\3\0\5\6\1\0\7\6"+
+ "\1\41\1\6\6\0\5\6\4\0\3\6\3\0\5\6"+
+ "\1\0\11\6\6\0\1\42\4\6\4\0\3\6\3\0"+
+ "\5\6\1\0\7\6\1\43\1\6\6\0\5\6\1\44"+
+ "\2\0\1\44\1\27\1\6\1\27\3\44\5\27\1\44"+
+ "\11\27\6\44\5\27\4\45\2\30\1\46\3\45\5\30"+
+ "\1\45\11\30\6\45\5\30\2\7\1\0\41\7\2\10"+
+ "\1\0\41\10\4\0\3\6\3\0\1\6\2\47\2\6"+
+ "\1\0\11\6\6\0\5\6\4\0\3\6\3\0\3\50"+
+ "\2\6\1\51\11\6\6\0\5\6\4\0\3\6\3\0"+
+ "\5\6\1\0\2\6\1\52\6\6\6\0\5\6\4\0"+
+ "\3\6\3\0\5\6\1\0\11\6\6\0\3\6\1\53"+
+ "\1\6\4\0\3\6\3\0\5\6\1\0\11\6\6\0"+
+ "\3\6\1\54\1\6\4\0\3\6\3\0\5\6\1\0"+
+ "\11\6\6\0\1\6\1\55\3\6\4\0\3\6\3\0"+
+ "\5\6\1\0\6\6\1\56\2\6\6\0\5\6\1\44"+
+ "\2\0\2\44\1\0\36\44\6\45\1\57\41\45\1\60"+
+ "\1\30\1\46\3\45\5\30\1\45\11\30\6\45\5\30"+
+ "\4\0\3\6\3\0\1\6\2\47\1\6\1\36\1\0"+
+ "\11\6\6\0\2\6\1\36\2\6\4\0\3\6\3\0"+
+ "\1\6\2\50\2\6\1\0\11\6\6\0\5\6\13\0"+
+ "\2\51\33\0\3\6\3\0\5\6\1\0\3\6\1\61"+
+ "\5\6\6\0\5\6\4\0\3\6\3\0\5\6\1\0"+
+ "\11\6\6\0\3\6\1\62\1\6\4\0\3\6\3\0"+
+ "\5\6\1\0\11\6\6\0\4\6\1\63\4\0\3\6"+
+ "\3\0\5\6\1\0\11\6\6\0\2\6\1\64\2\6"+
+ "\4\45\1\65\1\45\1\57\35\45\4\0\3\6\3\0"+
+ "\5\6\1\0\1\6\1\66\7\6\6\0\5\6\4\0"+
+ "\3\6\3\0\5\6\1\0\11\6\6\0\2\6\1\67"+
+ "\2\6\4\0\3\6\3\0\5\6\1\0\3\6\1\70"+
+ "\5\6\6\0\5\6\4\0\3\6\3\0\5\6\1\0"+
+ "\4\6\1\71\4\6\6\0\5\6\4\0\3\6\3\0"+
+ "\5\6\1\0\5\6\1\56\3\6\6\0\5\6";
+
+ private static int [] zzUnpackTrans() {
+ int [] result = new int[1548];
+ int offset = 0;
+ offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackTrans(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ value--;
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /* error codes */
+ private static final int ZZ_UNKNOWN_ERROR = 0;
+ private static final int ZZ_NO_MATCH = 1;
+ private static final int ZZ_PUSHBACK_2BIG = 2;
+
+ /* error messages for the codes above */
+ private static final String[] ZZ_ERROR_MSG = {
+ "Unknown internal scanner error",
+ "Error: could not match input",
+ "Error: pushback value was too large"
+ };
+
+ /**
+ * ZZ_ATTRIBUTE[aState] contains the attributes of state <code>aState</code>
+ */
+ private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
+
+ private static final String ZZ_ATTRIBUTE_PACKED_0 =
+ "\1\0\1\11\16\1\6\11\2\1\1\11\2\0\1\11"+
+ "\22\1\1\0\5\1\1\11\4\1";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[57];
+ int offset = 0;
+ offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAttribute(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /** the input device */
+ private java.io.Reader zzReader;
+
+ /** the current state of the DFA */
+ private int zzState;
+
+ /** the current lexical state */
+ private int zzLexicalState = YYINITIAL;
+
+ /** this buffer contains the current text to be matched and is
+ the source of the yytext() string */
+ private CharSequence zzBuffer = "";
+
+ /** the textposition at the last accepting state */
+ private int zzMarkedPos;
+
+ /** the current text position in the buffer */
+ private int zzCurrentPos;
+
+ /** startRead marks the beginning of the yytext() string in the buffer */
+ private int zzStartRead;
+
+ /** endRead marks the last character in the buffer, that has been read
+ from input */
+ private int zzEndRead;
+
+ /**
+ * zzAtBOL == true <=> the scanner is currently at the beginning of a line
+ */
+ private boolean zzAtBOL = true;
+
+ /** zzAtEOF == true <=> the scanner is at the EOF */
+ private boolean zzAtEOF;
+
+ /** denotes if the user-EOF-code has already been executed */
+ private boolean zzEOFDone;
+
+ /* user code: */
+ public _JsonLexer() {
+ this((java.io.Reader)null);
+ }
+
+
+ /**
+ * Creates a new scanner
+ *
+ * @param in the java.io.Reader to read input from.
+ */
+ public _JsonLexer(java.io.Reader in) {
+ this.zzReader = in;
+ }
+
+
+ /**
+ * Unpacks the compressed character translation table.
+ *
+ * @param packed the packed character translation table
+ * @return the unpacked character translation table
+ */
+ private static char [] zzUnpackCMap(String packed) {
+ int size = 0;
+ for (int i = 0, length = packed.length(); i < length; i += 2) {
+ size += packed.charAt(i);
+ }
+ char[] map = new char[size];
+ int i = 0; /* index in packed string */
+ int j = 0; /* index in unpacked array */
+ while (i < packed.length()) {
+ int count = packed.charAt(i++);
+ char value = packed.charAt(i++);
+ do map[j++] = value; while (--count > 0);
+ }
+ return map;
+ }
+
+ public final int getTokenStart() {
+ return zzStartRead;
+ }
+
+ public final int getTokenEnd() {
+ return getTokenStart() + yylength();
+ }
+
+ public void reset(CharSequence buffer, int start, int end, int initialState) {
+ zzBuffer = buffer;
+ zzCurrentPos = zzMarkedPos = zzStartRead = start;
+ zzAtEOF = false;
+ zzAtBOL = true;
+ zzEndRead = end;
+ yybegin(initialState);
+ }
+
+ /**
+ * Refills the input buffer.
+ *
+ * @return <code>false</code>, iff there was new input.
+ *
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ private boolean zzRefill() throws java.io.IOException {
+ return true;
+ }
+
+
+ /**
+ * Returns the current lexical state.
+ */
+ public final int yystate() {
+ return zzLexicalState;
+ }
+
+
+ /**
+ * Enters a new lexical state
+ *
+ * @param newState the new lexical state
+ */
+ public final void yybegin(int newState) {
+ zzLexicalState = newState;
+ }
+
+
+ /**
+ * Returns the text matched by the current regular expression.
+ */
+ public final CharSequence yytext() {
+ return zzBuffer.subSequence(zzStartRead, zzMarkedPos);
+ }
+
+
+ /**
+ * Returns the character at position <tt>pos</tt> from the
+ * matched text.
+ *
+ * It is equivalent to yytext().charAt(pos), but faster
+ *
+ * @param pos the position of the character to fetch.
+ * A value from 0 to yylength()-1.
+ *
+ * @return the character at position pos
+ */
+ public final char yycharat(int pos) {
+ return zzBuffer.charAt(zzStartRead+pos);
+ }
+
+
+ /**
+ * Returns the length of the matched text region.
+ */
+ public final int yylength() {
+ return zzMarkedPos-zzStartRead;
+ }
+
+
+ /**
+ * Reports an error that occured while scanning.
+ *
+ * In a wellformed scanner (no or only correct usage of
+ * yypushback(int) and a match-all fallback rule) this method
+ * will only be called with things that "Can't Possibly Happen".
+ * If this method is called, something is seriously wrong
+ * (e.g. a JFlex bug producing a faulty scanner etc.).
+ *
+ * Usual syntax/scanner level error handling should be done
+ * in error fallback rules.
+ *
+ * @param errorCode the code of the errormessage to display
+ */
+ private void zzScanError(int errorCode) {
+ String message;
+ try {
+ message = ZZ_ERROR_MSG[errorCode];
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
+ }
+
+ throw new Error(message);
+ }
+
+
+ /**
+ * Pushes the specified amount of characters back into the input stream.
+ *
+ * They will be read again by then next call of the scanning method
+ *
+ * @param number the number of characters to be read again.
+ * This number must not be greater than yylength()!
+ */
+ public void yypushback(int number) {
+ if ( number > yylength() )
+ zzScanError(ZZ_PUSHBACK_2BIG);
+
+ zzMarkedPos -= number;
+ }
+
+
+ /**
+ * Resumes scanning until the next regular expression is matched,
+ * the end of input is encountered or an I/O-Error occurs.
+ *
+ * @return the next token
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ public IElementType advance() throws java.io.IOException {
+ int zzInput;
+ int zzAction;
+
+ // cached fields:
+ int zzCurrentPosL;
+ int zzMarkedPosL;
+ int zzEndReadL = zzEndRead;
+ CharSequence zzBufferL = zzBuffer;
+
+ int [] zzTransL = ZZ_TRANS;
+ int [] zzRowMapL = ZZ_ROWMAP;
+ int [] zzAttrL = ZZ_ATTRIBUTE;
+
+ while (true) {
+ zzMarkedPosL = zzMarkedPos;
+
+ zzAction = -1;
+
+ zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
+
+ zzState = ZZ_LEXSTATE[zzLexicalState];
+
+ // set up zzAction for empty match case:
+ int zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ }
+
+
+ zzForAction: {
+ while (true) {
+
+ if (zzCurrentPosL < zzEndReadL) {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ else if (zzAtEOF) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ // store back cached positions
+ zzCurrentPos = zzCurrentPosL;
+ zzMarkedPos = zzMarkedPosL;
+ boolean eof = zzRefill();
+ // get translated positions and possibly new buffer
+ zzCurrentPosL = zzCurrentPos;
+ zzMarkedPosL = zzMarkedPos;
+ zzBufferL = zzBuffer;
+ zzEndReadL = zzEndRead;
+ if (eof) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ }
+ int zzNext = zzTransL[ zzRowMapL[zzState] + ZZ_CMAP(zzInput) ];
+ if (zzNext == -1) break zzForAction;
+ zzState = zzNext;
+
+ zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ zzMarkedPosL = zzCurrentPosL;
+ if ( (zzAttributes & 8) == 8 ) break zzForAction;
+ }
+
+ }
+ }
+
+ // store back cached position
+ zzMarkedPos = zzMarkedPosL;
+
+ if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
+ zzAtEOF = true;
+ return null;
+ }
+ else {
+ switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
+ case 1:
+ { return BAD_CHARACTER;
+ }
+ // fall through
+ case 18: break;
+ case 2:
+ { return WHITE_SPACE;
+ }
+ // fall through
+ case 19: break;
+ case 3:
+ { return IDENTIFIER;
+ }
+ // fall through
+ case 20: break;
+ case 4:
+ { return DOUBLE_QUOTED_STRING;
+ }
+ // fall through
+ case 21: break;
+ case 5:
+ { return SINGLE_QUOTED_STRING;
+ }
+ // fall through
+ case 22: break;
+ case 6:
+ { return NUMBER;
+ }
+ // fall through
+ case 23: break;
+ case 7:
+ { return L_CURLY;
+ }
+ // fall through
+ case 24: break;
+ case 8:
+ { return R_CURLY;
+ }
+ // fall through
+ case 25: break;
+ case 9:
+ { return L_BRACKET;
+ }
+ // fall through
+ case 26: break;
+ case 10:
+ { return R_BRACKET;
+ }
+ // fall through
+ case 27: break;
+ case 11:
+ { return COMMA;
+ }
+ // fall through
+ case 28: break;
+ case 12:
+ { return COLON;
+ }
+ // fall through
+ case 29: break;
+ case 13:
+ { return LINE_COMMENT;
+ }
+ // fall through
+ case 30: break;
+ case 14:
+ { return BLOCK_COMMENT;
+ }
+ // fall through
+ case 31: break;
+ case 15:
+ { return NULL;
+ }
+ // fall through
+ case 32: break;
+ case 16:
+ { return TRUE;
+ }
+ // fall through
+ case 33: break;
+ case 17:
+ { return FALSE;
+ }
+ // fall through
+ case 34: break;
+ default:
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/json/gen/com/intellij/json/json5/_Json5Lexer.java b/json/gen/com/intellij/json/json5/_Json5Lexer.java
new file mode 100644
index 00000000..f35cb96f
--- /dev/null
+++ b/json/gen/com/intellij/json/json5/_Json5Lexer.java
@@ -0,0 +1,730 @@
+/* The following code was generated by JFlex 1.7.0 tweaked for IntelliJ platform */
+
+package com.intellij.json.json5;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+
+/**
+ * This class is a scanner generated by
+ * <a href="http://www.jflex.de/">JFlex</a> 1.7.0
+ * from the specification file <tt>_Json5Lexer.flex</tt>
+ */
+public class _Json5Lexer implements FlexLexer {
+
+ /** This character denotes the end of file */
+ public static final int YYEOF = -1;
+
+ /** initial size of the lookahead buffer */
+ private static final int ZZ_BUFFERSIZE = 16384;
+
+ /** lexical states */
+ public static final int YYINITIAL = 0;
+
+ /**
+ * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
+ * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
+ * at the beginning of a line
+ * l is of the form l = 2*k, k a non negative integer
+ */
+ private static final int ZZ_LEXSTATE[] = {
+ 0, 0
+ };
+
+ /**
+ * Translates characters to character classes
+ * Chosen bits are [12, 6, 3]
+ * Total runtime size is 13376 bytes
+ */
+ public static int ZZ_CMAP(int ch) {
+ return ZZ_CMAP_A[(ZZ_CMAP_Y[(ZZ_CMAP_Z[ch>>9]<<6)|((ch>>3)&0x3f)]<<3)|(ch&0x7)];
+ }
+
+ /* The ZZ_CMAP_Z table has 2176 entries */
+ static final char ZZ_CMAP_Z[] = zzUnpackCMap(
+ "\1\0\1\1\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\11\1\12\1\13\1\14\1\15\1\16\1\17\1"+
+ "\20\5\21\1\22\1\23\1\24\1\21\14\25\1\26\50\25\1\27\2\25\1\30\1\31\1\32\1\33"+
+ "\25\25\1\34\20\21\1\35\1\36\1\37\1\40\1\41\1\42\1\43\1\21\1\44\1\45\1\46\1"+
+ "\21\1\47\2\21\1\50\4\21\1\25\1\51\1\52\5\21\2\25\1\53\31\21\1\25\1\54\1\21"+
+ "\1\55\40\21\1\56\17\21\1\57\1\60\1\61\1\62\13\21\1\63\10\21\123\25\1\64\7"+
+ "\25\1\65\1\66\37\21\1\25\1\66\u0582\21\1\67\u017f\21");
+
+ /* The ZZ_CMAP_Y table has 3584 entries */
+ static final char ZZ_CMAP_Y[] = zzUnpackCMap(
+ "\1\0\1\1\1\0\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\0\1\11\1\12\1\13\1\14\1\15\1\16"+
+ "\3\0\1\17\1\20\1\21\1\22\2\0\1\23\3\0\1\23\71\0\1\24\1\0\1\25\1\26\1\27\1"+
+ "\30\2\26\16\0\1\31\1\32\1\33\1\34\2\0\1\35\11\0\1\36\21\0\1\35\1\37\23\0\1"+
+ "\26\1\40\3\0\1\23\1\41\1\40\4\0\1\42\1\40\4\0\1\36\1\43\1\26\3\0\2\44\1\26"+
+ "\1\27\1\45\1\0\1\44\11\0\1\24\14\0\1\46\1\36\1\0\1\47\1\0\1\50\1\26\1\42\7"+
+ "\0\1\51\14\0\1\25\1\26\6\0\1\52\1\22\5\0\1\52\2\26\3\0\1\2\10\26\1\47\1\27"+
+ "\6\26\1\53\2\0\1\23\14\0\1\54\1\0\2\40\1\55\1\50\1\56\2\0\1\47\1\57\1\60\1"+
+ "\50\1\61\1\42\1\62\1\54\1\0\1\2\1\45\1\55\1\63\1\56\2\0\1\47\1\64\1\65\1\63"+
+ "\1\66\1\41\1\67\1\70\1\0\1\52\1\26\1\55\1\36\1\35\2\0\1\47\1\71\1\60\1\36"+
+ "\1\72\1\73\1\26\1\54\1\0\1\41\1\26\1\55\1\50\1\56\2\0\1\47\1\71\1\60\1\50"+
+ "\1\66\1\70\1\62\1\54\1\0\1\41\1\26\1\74\1\75\1\76\1\77\1\100\1\75\1\0\1\24"+
+ "\1\75\1\76\1\101\1\26\1\70\1\0\1\26\1\41\1\55\1\31\1\47\2\0\1\47\1\46\1\102"+
+ "\1\31\1\76\1\103\1\25\1\54\1\0\2\26\1\74\1\31\1\47\2\0\1\47\1\46\1\60\1\31"+
+ "\1\76\1\103\1\33\1\54\1\0\1\104\1\26\1\74\1\31\1\47\4\0\1\51\1\31\1\105\1"+
+ "\42\1\26\1\54\1\0\1\26\1\37\1\74\1\0\1\23\1\37\2\0\1\35\1\106\1\23\1\107\1"+
+ "\110\1\0\2\26\1\111\1\26\1\40\6\0\1\63\1\0\1\23\1\0\1\25\4\26\1\112\1\113"+
+ "\1\53\1\40\1\114\1\74\1\0\1\72\1\110\1\52\1\0\1\60\4\26\1\73\2\26\1\25\1\0"+
+ "\1\25\1\115\1\116\1\0\1\40\3\0\1\27\1\40\1\0\1\31\2\0\1\40\3\0\1\27\1\33\7"+
+ "\26\11\0\1\25\11\0\1\52\4\0\1\36\1\21\5\0\1\117\51\0\1\76\1\23\1\76\5\0\1"+
+ "\76\4\0\1\76\1\23\1\76\1\0\1\23\7\0\1\76\10\0\1\51\4\26\2\0\2\26\12\0\1\27"+
+ "\1\26\1\40\114\0\1\50\2\0\1\120\2\0\1\44\11\0\1\75\1\73\1\26\1\0\1\31\1\27"+
+ "\1\26\2\0\1\27\1\26\2\0\1\2\1\26\1\0\1\31\1\121\1\26\12\0\1\122\1\123\1\0"+
+ "\1\25\3\26\1\123\1\0\1\25\13\0\1\26\5\0\1\44\10\0\1\52\1\26\3\0\1\27\1\0\1"+
+ "\2\1\0\1\2\1\70\4\0\1\52\1\27\1\26\5\0\1\2\3\0\1\25\1\0\1\25\4\26\3\0\1\2"+
+ "\7\0\1\23\3\0\1\50\1\0\1\25\1\0\1\25\1\42\13\26\11\0\1\2\1\0\1\25\1\26\1\124"+
+ "\1\2\1\26\16\0\1\2\1\26\7\0\1\26\1\0\1\102\5\0\1\52\12\26\1\117\3\0\1\23\1"+
+ "\26\34\0\1\23\2\26\1\53\42\0\2\52\4\0\2\52\1\0\1\125\3\0\1\52\6\0\1\31\1\110"+
+ "\1\126\1\27\1\54\1\2\1\0\1\27\1\126\1\27\1\127\1\130\3\26\1\131\1\26\1\42"+
+ "\1\73\1\26\1\132\1\133\1\27\1\37\1\41\1\42\2\26\1\0\1\27\3\0\1\44\2\26\1\0"+
+ "\1\27\1\134\1\0\1\73\1\26\1\107\1\37\1\106\1\135\1\30\1\136\1\0\1\60\1\137"+
+ "\1\140\2\26\5\0\1\73\116\26\5\0\1\23\5\0\1\23\20\0\1\27\1\124\1\2\1\26\4\0"+
+ "\1\36\1\21\7\0\1\42\1\26\1\42\2\0\1\23\1\26\10\23\4\0\5\26\1\42\72\26\1\141"+
+ "\3\26\1\40\1\0\1\135\1\27\1\40\11\0\1\23\1\142\1\40\12\0\1\117\1\137\4\0\1"+
+ "\52\1\40\12\0\1\23\2\26\3\0\1\44\6\26\170\0\1\52\11\26\71\0\1\27\6\26\21\0"+
+ "\1\27\10\26\5\0\1\52\41\0\1\27\3\0\1\2\2\26\6\0\1\53\1\36\3\0\1\42\12\0\1"+
+ "\25\3\26\1\42\1\0\1\37\14\0\1\61\1\2\1\26\1\0\1\44\11\26\6\0\2\26\1\73\6\0"+
+ "\1\2\1\26\10\0\1\27\1\26\1\0\1\25\3\0\1\45\5\0\1\52\4\0\1\2\1\26\3\0\1\27"+
+ "\10\0\1\73\1\42\1\0\1\25\4\26\6\0\1\23\1\26\1\0\1\52\1\0\1\25\2\0\1\23\1\111"+
+ "\10\0\1\44\2\26\1\123\2\0\1\143\1\26\3\144\1\26\2\23\22\26\5\0\1\145\1\0\1"+
+ "\25\64\0\1\2\1\26\2\0\1\23\1\124\5\0\1\2\40\26\55\0\1\52\15\0\1\25\4\26\1"+
+ "\23\1\26\1\124\1\137\1\0\1\47\1\23\1\110\1\146\15\0\1\25\3\26\1\124\54\0\1"+
+ "\52\2\26\10\0\1\37\6\0\5\26\1\0\1\27\2\0\2\26\1\23\1\26\1\100\2\26\1\137\3"+
+ "\26\1\41\1\31\20\0\1\50\1\132\1\26\1\0\1\25\1\40\2\0\1\63\1\40\2\0\1\44\1"+
+ "\70\12\0\1\23\3\37\1\147\1\150\2\26\1\151\1\0\1\46\2\0\1\23\2\0\1\152\1\0"+
+ "\1\52\1\0\1\52\4\26\17\0\1\44\10\26\6\0\1\27\20\26\1\21\20\26\3\0\1\27\6\0"+
+ "\1\73\5\26\3\0\1\23\2\26\3\0\1\44\6\26\3\0\1\52\4\0\1\2\1\0\1\135\5\26\23"+
+ "\0\1\52\1\0\1\25\52\26\1\52\1\47\4\0\1\36\1\153\2\0\1\52\25\26\2\0\1\52\1"+
+ "\26\3\0\1\25\10\26\7\0\1\70\10\26\1\154\1\53\1\46\1\40\2\0\1\2\1\63\4\26\3"+
+ "\0\1\27\20\26\6\0\1\52\1\26\2\0\1\52\1\26\2\0\1\44\21\26\11\0\1\73\66\26\10"+
+ "\0\1\23\3\26\1\70\1\0\2\26\7\0\1\155\2\26\3\0\1\73\1\0\1\25\6\0\1\31\1\0\10"+
+ "\26\10\0\1\27\1\26\1\0\1\25\24\26\7\0\1\26\1\0\1\25\46\26\55\0\1\23\22\26"+
+ "\14\0\1\44\63\26\5\0\1\23\72\26\7\0\1\73\130\26\10\0\1\27\1\26\5\0\1\23\1"+
+ "\26\1\42\2\0\14\26\1\25\153\26\1\137\1\102\2\0\1\51\1\2\3\26\1\32\22\26\1"+
+ "\147\67\26\12\0\1\31\10\0\1\31\1\156\1\157\1\0\1\160\1\46\7\0\1\36\1\51\2"+
+ "\31\3\0\1\161\1\110\1\37\1\47\51\0\1\52\3\0\1\47\2\0\1\117\3\0\1\117\2\0\1"+
+ "\31\3\0\1\31\2\0\1\23\3\0\1\23\3\0\1\47\3\0\1\47\2\0\1\117\1\54\6\0\1\46\3"+
+ "\0\1\112\1\40\1\117\1\162\1\107\1\163\1\112\1\125\1\112\2\117\1\67\1\0\1\35"+
+ "\1\0\1\2\1\55\1\35\1\0\1\2\50\26\32\0\1\23\5\26\106\0\1\27\1\26\33\0\1\52"+
+ "\74\26\1\41\3\26\14\0\20\26\36\0\2\26");
+
+ /* The ZZ_CMAP_A table has 928 entries */
+ static final char ZZ_CMAP_A[] = zzUnpackCMap(
+ "\11\35\1\12\1\2\1\1\1\7\1\3\6\35\4\0\1\12\1\35\1\13\1\0\1\35\2\0\1\15\2\35"+
+ "\1\11\1\16\1\42\1\17\1\22\1\6\1\20\11\21\1\43\1\0\3\35\1\0\1\35\4\5\1\23\1"+
+ "\5\2\35\1\25\4\35\1\33\1\35\1\24\2\35\1\40\1\14\1\41\2\35\1\0\1\34\3\5\1\46"+
+ "\1\27\2\35\1\30\2\35\1\47\1\35\1\26\3\35\1\44\1\50\1\31\1\45\2\35\1\24\1\32"+
+ "\1\35\1\36\1\0\1\37\7\35\1\10\2\35\1\4\1\0\4\35\4\0\1\35\2\0\1\35\7\0\1\35"+
+ "\4\0\1\35\5\0\7\35\1\0\2\35\4\0\4\35\16\0\5\35\7\0\1\35\1\0\1\35\1\0\5\35"+
+ "\1\0\2\35\2\0\4\35\10\0\1\35\1\0\3\35\1\0\1\35\1\0\4\35\1\0\13\35\1\0\1\35"+
+ "\2\0\6\35\1\0\7\35\1\0\1\35\15\0\1\35\1\0\2\35\1\0\2\35\1\0\4\35\10\0\1\35"+
+ "\4\0\4\35\1\0\4\35\1\0\13\35\2\0\4\35\2\0\11\35\6\0\10\35\2\0\2\35\1\0\3\35"+
+ "\1\0\4\35\2\0\6\35\1\0\1\35\3\0\4\35\2\0\5\35\2\0\4\35\5\0\2\35\1\0\4\35\4"+
+ "\0\2\35\1\0\2\35\1\0\2\35\1\0\2\35\2\0\1\35\1\0\3\35\2\0\3\35\3\0\4\35\1\0"+
+ "\1\35\7\0\3\35\1\0\2\35\1\0\5\35\1\0\3\35\2\0\1\35\11\0\2\35\1\0\6\35\3\0"+
+ "\3\35\1\0\4\35\3\0\2\35\1\0\1\35\1\0\2\35\3\0\2\35\3\0\1\35\6\0\3\35\3\0\3"+
+ "\35\5\0\2\35\2\0\2\35\5\0\1\35\1\0\5\35\1\0\4\35\1\0\1\35\4\0\1\35\4\0\6\35"+
+ "\1\0\1\35\3\0\2\35\5\0\2\35\1\0\1\35\2\0\2\35\1\0\1\35\2\0\1\35\3\0\3\35\1"+
+ "\0\1\35\1\0\1\35\5\0\1\35\1\0\1\35\1\0\1\35\4\0\5\35\1\0\4\35\1\4\10\35\1"+
+ "\0\2\35\4\0\4\35\3\0\1\35\3\0\3\35\5\0\5\35\1\0\1\35\1\0\1\35\1\0\1\35\1\0"+
+ "\1\35\2\0\3\35\1\0\2\35\13\4\5\35\2\1\5\35\1\4\4\0\1\35\12\0\1\4\1\0\1\35"+
+ "\3\0\3\35\1\0\5\35\2\0\1\35\1\0\4\35\1\0\1\35\5\0\5\35\4\0\1\35\1\0\1\4\4"+
+ "\0\3\35\1\0\2\35\2\0\3\35\2\0\5\35\2\0\6\35\1\0\3\35\1\0\2\35\2\0\2\35\1\0"+
+ "\2\35\1\0\2\35\2\0\3\35\3\0\2\35\3\0\2\35\2\0\3\35\4\0\3\35\1\0\2\35\1\0\2"+
+ "\35\3\0\1\35\2\0\5\35\1\0\2\35\1\0\3\35\2\0\1\35\4\0\1\35\2\0\2\35\2\0\4\35"+
+ "\1\0\4\35\1\0\1\35\1\0\5\35\1\0\4\35\2\0\1\35\1\0\1\35\5\0\1\35\1\0\1\35\1"+
+ "\0\3\35");
+
+ /**
+ * Translates DFA states to action switch labels.
+ */
+ private static final int [] ZZ_ACTION = zzUnpackAction();
+
+ private static final String ZZ_ACTION_PACKED_0 =
+ "\1\1\1\2\1\3\2\4\1\3\1\5\1\6\6\1"+
+ "\5\4\1\7\1\10\1\11\1\12\1\13\1\14\1\15"+
+ "\1\16\1\5\2\0\1\6\4\1\2\0\1\4\2\1"+
+ "\5\4\1\15\1\16\1\4\2\5\2\6\3\0\1\1"+
+ "\4\4\1\1\1\0\1\16\1\1\1\0\1\1\1\4"+
+ "\1\17\1\4\1\20\1\16\1\0\1\4\1\21\1\0"+
+ "\1\4\1\0\1\4\1\0";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[79];
+ int offset = 0;
+ offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAction(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Translates a state to a row index in the transition table
+ */
+ private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
+
+ private static final String ZZ_ROWMAP_PACKED_0 =
+ "\0\0\0\51\0\122\0\173\0\244\0\315\0\366\0\u011f"+
+ "\0\u0148\0\u0171\0\u019a\0\u01c3\0\u01ec\0\u0215\0\u023e\0\u0267"+
+ "\0\u0290\0\u02b9\0\u02e2\0\51\0\51\0\51\0\51\0\51"+
+ "\0\51\0\u030b\0\u0334\0\51\0\u035d\0\u0386\0\51\0\u03af"+
+ "\0\u03d8\0\u0401\0\u042a\0\u0453\0\u047c\0\u04a5\0\u04ce\0\u04f7"+
+ "\0\u0520\0\u0549\0\u0572\0\u059b\0\u05c4\0\u05ed\0\u0616\0\u063f"+
+ "\0\u0668\0\u0691\0\u06ba\0\u06e3\0\u070c\0\u0735\0\u075e\0\u04a5"+
+ "\0\u0787\0\u07b0\0\u07d9\0\u0802\0\173\0\u082b\0\173\0\u070c"+
+ "\0\u0854\0\51\0\u087d\0\173\0\u08a6\0\173\0\51\0\u08cf"+
+ "\0\u08f8\0\173\0\u0921\0\u094a\0\u0973\0\u099c\0\u09c5";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[79];
+ int offset = 0;
+ offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int high = packed.charAt(i++) << 16;
+ result[j++] = high | packed.charAt(i++);
+ }
+ return j;
+ }
+
+ /**
+ * The transition table of the DFA
+ */
+ private static final int [] ZZ_TRANS = zzUnpackTrans();
+
+ private static final String ZZ_TRANS_PACKED_0 =
+ "\1\2\4\3\1\4\1\5\1\3\1\6\1\4\1\3"+
+ "\1\7\1\2\1\10\1\11\1\12\1\13\1\14\1\15"+
+ "\1\16\1\4\1\17\1\20\1\21\1\4\1\22\1\4"+
+ "\1\23\2\4\1\24\1\25\1\26\1\27\1\30\1\31"+
+ "\2\4\1\16\2\4\52\0\4\3\2\0\2\3\1\0"+
+ "\1\3\43\0\2\4\1\0\2\4\5\0\17\4\6\0"+
+ "\5\4\5\0\1\4\1\32\1\0\1\4\1\33\5\0"+
+ "\17\4\6\0\5\4\1\0\4\3\2\4\1\3\1\6"+
+ "\1\4\1\3\4\0\17\4\6\0\5\4\2\7\2\0"+
+ "\7\7\1\34\1\35\34\7\2\10\2\0\10\10\1\36"+
+ "\1\37\33\10\20\0\1\40\1\41\1\42\1\43\1\0"+
+ "\1\44\5\0\1\45\12\0\1\43\7\0\2\4\1\0"+
+ "\2\4\5\0\1\4\1\13\1\14\1\15\1\16\1\4"+
+ "\1\17\5\4\1\23\2\4\6\0\2\4\1\16\2\4"+
+ "\5\0\2\4\1\0\2\4\5\0\1\4\3\15\1\16"+
+ "\1\46\11\4\6\0\2\4\1\16\2\4\5\0\2\4"+
+ "\1\0\2\4\5\0\1\4\2\14\1\15\1\16\12\4"+
+ "\6\0\2\4\1\16\2\4\5\0\2\4\1\0\2\4"+
+ "\5\0\1\4\2\15\1\4\1\16\12\4\6\0\2\4"+
+ "\1\16\2\4\5\0\2\4\1\0\2\4\4\0\1\47"+
+ "\3\50\14\4\6\0\5\4\5\0\2\4\1\0\2\4"+
+ "\5\0\7\4\1\51\7\4\6\0\5\4\5\0\2\4"+
+ "\1\0\2\4\5\0\17\4\6\0\1\4\1\52\3\4"+
+ "\5\0\2\4\1\0\2\4\5\0\15\4\1\53\1\4"+
+ "\6\0\5\4\5\0\2\4\1\0\2\4\5\0\17\4"+
+ "\6\0\1\54\4\4\5\0\2\4\1\0\2\4\5\0"+
+ "\15\4\1\55\1\4\6\0\5\4\1\56\3\0\1\56"+
+ "\2\32\1\0\1\4\1\32\5\56\17\32\6\56\5\32"+
+ "\5\57\2\33\1\57\1\33\1\60\5\57\17\33\6\57"+
+ "\5\33\3\7\1\61\3\7\1\62\2\7\1\62\36\7"+
+ "\3\10\1\63\3\10\1\64\2\10\1\64\36\10\20\0"+
+ "\3\42\1\43\1\65\21\0\1\43\22\0\2\41\1\42"+
+ "\1\43\22\0\1\43\22\0\2\42\1\0\1\43\22\0"+
+ "\1\43\20\0\4\47\55\0\1\66\56\0\1\67\21\0"+
+ "\1\70\1\4\1\0\2\4\5\0\1\4\2\70\1\4"+
+ "\1\70\3\4\1\70\4\4\1\70\1\4\6\0\2\4"+
+ "\1\70\2\4\20\0\2\47\34\0\2\4\1\0\2\4"+
+ "\5\0\1\4\2\50\14\4\6\0\5\4\5\0\2\4"+
+ "\1\0\2\4\5\0\10\4\1\71\6\4\6\0\5\4"+
+ "\5\0\2\4\1\0\2\4\5\0\17\4\6\0\3\4"+
+ "\1\72\1\4\5\0\2\4\1\0\2\4\5\0\17\4"+
+ "\6\0\3\4\1\73\1\4\5\0\2\4\1\0\2\4"+
+ "\5\0\17\4\6\0\1\4\1\74\3\4\5\0\2\4"+
+ "\1\0\2\4\5\0\14\4\1\75\2\4\6\0\5\4"+
+ "\1\56\3\0\3\56\2\0\40\56\11\57\1\76\44\57"+
+ "\1\33\1\77\1\57\1\33\1\60\5\57\17\33\6\57"+
+ "\5\33\3\7\1\0\7\7\1\34\1\35\37\7\1\61"+
+ "\3\7\1\62\2\7\1\62\1\34\1\35\34\7\3\10"+
+ "\1\0\10\10\1\36\1\37\36\10\1\63\3\10\1\64"+
+ "\2\10\1\64\1\10\1\36\1\37\33\10\5\0\1\100"+
+ "\12\0\2\100\1\0\1\100\3\0\1\100\4\0\1\100"+
+ "\11\0\1\100\31\0\1\101\54\0\1\102\22\0\2\4"+
+ "\1\0\2\4\5\0\11\4\1\103\5\4\6\0\5\4"+
+ "\5\0\2\4\1\0\2\4\5\0\17\4\6\0\3\4"+
+ "\1\104\1\4\5\0\2\4\1\0\2\4\5\0\17\4"+
+ "\6\0\4\4\1\105\5\0\2\4\1\0\2\4\5\0"+
+ "\17\4\6\0\2\4\1\106\2\4\6\57\1\107\2\57"+
+ "\1\76\37\57\30\0\1\110\25\0\2\4\1\0\2\4"+
+ "\5\0\7\4\1\111\7\4\6\0\5\4\5\0\2\4"+
+ "\1\0\2\4\5\0\17\4\6\0\2\4\1\112\2\4"+
+ "\26\0\1\113\27\0\2\4\1\0\2\4\5\0\11\4"+
+ "\1\114\5\4\6\0\5\4\30\0\1\115\25\0\2\4"+
+ "\1\0\2\4\5\0\12\4\1\116\4\4\6\0\5\4"+
+ "\31\0\1\117\24\0\2\4\1\0\2\4\5\0\13\4"+
+ "\1\75\3\4\6\0\5\4\32\0\1\102\16\0";
+
+ private static int [] zzUnpackTrans() {
+ int [] result = new int[2542];
+ int offset = 0;
+ offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackTrans(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ value--;
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /* error codes */
+ private static final int ZZ_UNKNOWN_ERROR = 0;
+ private static final int ZZ_NO_MATCH = 1;
+ private static final int ZZ_PUSHBACK_2BIG = 2;
+
+ /* error messages for the codes above */
+ private static final String[] ZZ_ERROR_MSG = {
+ "Unknown internal scanner error",
+ "Error: could not match input",
+ "Error: pushback value was too large"
+ };
+
+ /**
+ * ZZ_ATTRIBUTE[aState] contains the attributes of state <code>aState</code>
+ */
+ private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
+
+ private static final String ZZ_ATTRIBUTE_PACKED_0 =
+ "\1\1\1\11\21\1\6\11\2\1\1\11\2\0\1\11"+
+ "\4\1\2\0\17\1\3\0\6\1\1\0\2\1\1\0"+
+ "\1\11\4\1\1\11\1\0\2\1\1\0\1\1\1\0"+
+ "\1\1\1\0";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[79];
+ int offset = 0;
+ offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAttribute(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /** the input device */
+ private java.io.Reader zzReader;
+
+ /** the current state of the DFA */
+ private int zzState;
+
+ /** the current lexical state */
+ private int zzLexicalState = YYINITIAL;
+
+ /** this buffer contains the current text to be matched and is
+ the source of the yytext() string */
+ private CharSequence zzBuffer = "";
+
+ /** the textposition at the last accepting state */
+ private int zzMarkedPos;
+
+ /** the current text position in the buffer */
+ private int zzCurrentPos;
+
+ /** startRead marks the beginning of the yytext() string in the buffer */
+ private int zzStartRead;
+
+ /** endRead marks the last character in the buffer, that has been read
+ from input */
+ private int zzEndRead;
+
+ /**
+ * zzAtBOL == true <=> the scanner is currently at the beginning of a line
+ */
+ private boolean zzAtBOL = true;
+
+ /** zzAtEOF == true <=> the scanner is at the EOF */
+ private boolean zzAtEOF;
+
+ /** denotes if the user-EOF-code has already been executed */
+ private boolean zzEOFDone;
+
+ /* user code: */
+ public _Json5Lexer() {
+ this((java.io.Reader)null);
+ }
+
+
+ /**
+ * Creates a new scanner
+ *
+ * @param in the java.io.Reader to read input from.
+ */
+ public _Json5Lexer(java.io.Reader in) {
+ this.zzReader = in;
+ }
+
+
+ /**
+ * Unpacks the compressed character translation table.
+ *
+ * @param packed the packed character translation table
+ * @return the unpacked character translation table
+ */
+ private static char [] zzUnpackCMap(String packed) {
+ int size = 0;
+ for (int i = 0, length = packed.length(); i < length; i += 2) {
+ size += packed.charAt(i);
+ }
+ char[] map = new char[size];
+ int i = 0; /* index in packed string */
+ int j = 0; /* index in unpacked array */
+ while (i < packed.length()) {
+ int count = packed.charAt(i++);
+ char value = packed.charAt(i++);
+ do map[j++] = value; while (--count > 0);
+ }
+ return map;
+ }
+
+ public final int getTokenStart() {
+ return zzStartRead;
+ }
+
+ public final int getTokenEnd() {
+ return getTokenStart() + yylength();
+ }
+
+ public void reset(CharSequence buffer, int start, int end, int initialState) {
+ zzBuffer = buffer;
+ zzCurrentPos = zzMarkedPos = zzStartRead = start;
+ zzAtEOF = false;
+ zzAtBOL = true;
+ zzEndRead = end;
+ yybegin(initialState);
+ }
+
+ /**
+ * Refills the input buffer.
+ *
+ * @return <code>false</code>, iff there was new input.
+ *
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ private boolean zzRefill() throws java.io.IOException {
+ return true;
+ }
+
+
+ /**
+ * Returns the current lexical state.
+ */
+ public final int yystate() {
+ return zzLexicalState;
+ }
+
+
+ /**
+ * Enters a new lexical state
+ *
+ * @param newState the new lexical state
+ */
+ public final void yybegin(int newState) {
+ zzLexicalState = newState;
+ }
+
+
+ /**
+ * Returns the text matched by the current regular expression.
+ */
+ public final CharSequence yytext() {
+ return zzBuffer.subSequence(zzStartRead, zzMarkedPos);
+ }
+
+
+ /**
+ * Returns the character at position <tt>pos</tt> from the
+ * matched text.
+ *
+ * It is equivalent to yytext().charAt(pos), but faster
+ *
+ * @param pos the position of the character to fetch.
+ * A value from 0 to yylength()-1.
+ *
+ * @return the character at position pos
+ */
+ public final char yycharat(int pos) {
+ return zzBuffer.charAt(zzStartRead+pos);
+ }
+
+
+ /**
+ * Returns the length of the matched text region.
+ */
+ public final int yylength() {
+ return zzMarkedPos-zzStartRead;
+ }
+
+
+ /**
+ * Reports an error that occured while scanning.
+ *
+ * In a wellformed scanner (no or only correct usage of
+ * yypushback(int) and a match-all fallback rule) this method
+ * will only be called with things that "Can't Possibly Happen".
+ * If this method is called, something is seriously wrong
+ * (e.g. a JFlex bug producing a faulty scanner etc.).
+ *
+ * Usual syntax/scanner level error handling should be done
+ * in error fallback rules.
+ *
+ * @param errorCode the code of the errormessage to display
+ */
+ private void zzScanError(int errorCode) {
+ String message;
+ try {
+ message = ZZ_ERROR_MSG[errorCode];
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
+ }
+
+ throw new Error(message);
+ }
+
+
+ /**
+ * Pushes the specified amount of characters back into the input stream.
+ *
+ * They will be read again by then next call of the scanning method
+ *
+ * @param number the number of characters to be read again.
+ * This number must not be greater than yylength()!
+ */
+ public void yypushback(int number) {
+ if ( number > yylength() )
+ zzScanError(ZZ_PUSHBACK_2BIG);
+
+ zzMarkedPos -= number;
+ }
+
+
+ /**
+ * Resumes scanning until the next regular expression is matched,
+ * the end of input is encountered or an I/O-Error occurs.
+ *
+ * @return the next token
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ public IElementType advance() throws java.io.IOException {
+ int zzInput;
+ int zzAction;
+
+ // cached fields:
+ int zzCurrentPosL;
+ int zzMarkedPosL;
+ int zzEndReadL = zzEndRead;
+ CharSequence zzBufferL = zzBuffer;
+
+ int [] zzTransL = ZZ_TRANS;
+ int [] zzRowMapL = ZZ_ROWMAP;
+ int [] zzAttrL = ZZ_ATTRIBUTE;
+
+ while (true) {
+ zzMarkedPosL = zzMarkedPos;
+
+ zzAction = -1;
+
+ zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
+
+ zzState = ZZ_LEXSTATE[zzLexicalState];
+
+ // set up zzAction for empty match case:
+ int zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ }
+
+
+ zzForAction: {
+ while (true) {
+
+ if (zzCurrentPosL < zzEndReadL) {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ else if (zzAtEOF) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ // store back cached positions
+ zzCurrentPos = zzCurrentPosL;
+ zzMarkedPos = zzMarkedPosL;
+ boolean eof = zzRefill();
+ // get translated positions and possibly new buffer
+ zzCurrentPosL = zzCurrentPos;
+ zzMarkedPosL = zzMarkedPos;
+ zzBufferL = zzBuffer;
+ zzEndReadL = zzEndRead;
+ if (eof) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ }
+ int zzNext = zzTransL[ zzRowMapL[zzState] + ZZ_CMAP(zzInput) ];
+ if (zzNext == -1) break zzForAction;
+ zzState = zzNext;
+
+ zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ zzMarkedPosL = zzCurrentPosL;
+ if ( (zzAttributes & 8) == 8 ) break zzForAction;
+ }
+
+ }
+ }
+
+ // store back cached position
+ zzMarkedPos = zzMarkedPosL;
+
+ if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
+ zzAtEOF = true;
+ return null;
+ }
+ else {
+ switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
+ case 1:
+ { return NUMBER;
+ }
+ // fall through
+ case 18: break;
+ case 2:
+ { return BAD_CHARACTER;
+ }
+ // fall through
+ case 19: break;
+ case 3:
+ { return WHITE_SPACE;
+ }
+ // fall through
+ case 20: break;
+ case 4:
+ { return IDENTIFIER;
+ }
+ // fall through
+ case 21: break;
+ case 5:
+ { return DOUBLE_QUOTED_STRING;
+ }
+ // fall through
+ case 22: break;
+ case 6:
+ { return SINGLE_QUOTED_STRING;
+ }
+ // fall through
+ case 23: break;
+ case 7:
+ { return L_CURLY;
+ }
+ // fall through
+ case 24: break;
+ case 8:
+ { return R_CURLY;
+ }
+ // fall through
+ case 25: break;
+ case 9:
+ { return L_BRACKET;
+ }
+ // fall through
+ case 26: break;
+ case 10:
+ { return R_BRACKET;
+ }
+ // fall through
+ case 27: break;
+ case 11:
+ { return COMMA;
+ }
+ // fall through
+ case 28: break;
+ case 12:
+ { return COLON;
+ }
+ // fall through
+ case 29: break;
+ case 13:
+ { return LINE_COMMENT;
+ }
+ // fall through
+ case 30: break;
+ case 14:
+ { return BLOCK_COMMENT;
+ }
+ // fall through
+ case 31: break;
+ case 15:
+ { return NULL;
+ }
+ // fall through
+ case 32: break;
+ case 16:
+ { return TRUE;
+ }
+ // fall through
+ case 33: break;
+ case 17:
+ { return FALSE;
+ }
+ // fall through
+ case 34: break;
+ default:
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonArray.java b/json/gen/com/intellij/json/psi/JsonArray.java
new file mode 100644
index 00000000..58263639
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonArray.java
@@ -0,0 +1,17 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+import com.intellij.navigation.ItemPresentation;
+
+public interface JsonArray extends JsonContainer {
+
+ @NotNull
+ List<JsonValue> getValueList();
+
+ @Nullable
+ ItemPresentation getPresentation();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java b/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java
new file mode 100644
index 00000000..cc0abeca
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java
@@ -0,0 +1,12 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonBooleanLiteral extends JsonLiteral {
+
+ boolean getValue();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonContainer.java b/json/gen/com/intellij/json/psi/JsonContainer.java
new file mode 100644
index 00000000..a69c0a8b
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonContainer.java
@@ -0,0 +1,10 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonContainer extends JsonValue {
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonElementVisitor.java b/json/gen/com/intellij/json/psi/JsonElementVisitor.java
new file mode 100644
index 00000000..836eeea6
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonElementVisitor.java
@@ -0,0 +1,64 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+
+public class JsonElementVisitor extends PsiElementVisitor {
+
+ public void visitArray(@NotNull JsonArray o) {
+ visitContainer(o);
+ }
+
+ public void visitBooleanLiteral(@NotNull JsonBooleanLiteral o) {
+ visitLiteral(o);
+ }
+
+ public void visitContainer(@NotNull JsonContainer o) {
+ visitValue(o);
+ }
+
+ public void visitLiteral(@NotNull JsonLiteral o) {
+ visitValue(o);
+ }
+
+ public void visitNullLiteral(@NotNull JsonNullLiteral o) {
+ visitLiteral(o);
+ }
+
+ public void visitNumberLiteral(@NotNull JsonNumberLiteral o) {
+ visitLiteral(o);
+ }
+
+ public void visitObject(@NotNull JsonObject o) {
+ visitContainer(o);
+ }
+
+ public void visitProperty(@NotNull JsonProperty o) {
+ visitElement(o);
+ // visitPsiNamedElement(o);
+ }
+
+ public void visitReferenceExpression(@NotNull JsonReferenceExpression o) {
+ visitValue(o);
+ }
+
+ public void visitStringLiteral(@NotNull JsonStringLiteral o) {
+ visitLiteral(o);
+ }
+
+ public void visitValue(@NotNull JsonValue o) {
+ visitElement(o);
+ }
+
+ public void visitElement(@NotNull JsonElement o) {
+ visitPsiElement(o);
+ }
+
+ public void visitPsiElement(@NotNull PsiElement o) {
+ visitElement(o);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonLiteral.java b/json/gen/com/intellij/json/psi/JsonLiteral.java
new file mode 100644
index 00000000..6e0eb7c7
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonLiteral.java
@@ -0,0 +1,12 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonLiteral extends JsonValue {
+
+ boolean isQuotedString();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonNullLiteral.java b/json/gen/com/intellij/json/psi/JsonNullLiteral.java
new file mode 100644
index 00000000..a57448e6
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonNullLiteral.java
@@ -0,0 +1,10 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonNullLiteral extends JsonLiteral {
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonNumberLiteral.java b/json/gen/com/intellij/json/psi/JsonNumberLiteral.java
new file mode 100644
index 00000000..14649f82
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonNumberLiteral.java
@@ -0,0 +1,12 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonNumberLiteral extends JsonLiteral {
+
+ double getValue();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonObject.java b/json/gen/com/intellij/json/psi/JsonObject.java
new file mode 100644
index 00000000..329a16f4
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonObject.java
@@ -0,0 +1,20 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+import com.intellij.navigation.ItemPresentation;
+
+public interface JsonObject extends JsonContainer {
+
+ @NotNull
+ List<JsonProperty> getPropertyList();
+
+ @Nullable
+ JsonProperty findProperty(@NotNull String name);
+
+ @Nullable
+ ItemPresentation getPresentation();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonProperty.java b/json/gen/com/intellij/json/psi/JsonProperty.java
new file mode 100644
index 00000000..eaf444c4
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonProperty.java
@@ -0,0 +1,24 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.navigation.ItemPresentation;
+
+public interface JsonProperty extends JsonElement, PsiNamedElement {
+
+ @NotNull
+ String getName();
+
+ @NotNull
+ JsonValue getNameElement();
+
+ @Nullable
+ JsonValue getValue();
+
+ @Nullable
+ ItemPresentation getPresentation();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonReferenceExpression.java b/json/gen/com/intellij/json/psi/JsonReferenceExpression.java
new file mode 100644
index 00000000..548fea1b
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonReferenceExpression.java
@@ -0,0 +1,13 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonReferenceExpression extends JsonValue {
+
+ @NotNull
+ PsiElement getIdentifier();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonStringLiteral.java b/json/gen/com/intellij/json/psi/JsonStringLiteral.java
new file mode 100644
index 00000000..260976c9
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonStringLiteral.java
@@ -0,0 +1,20 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+
+public interface JsonStringLiteral extends JsonLiteral {
+
+ @NotNull
+ List<Pair<TextRange, String>> getTextFragments();
+
+ @NotNull
+ String getValue();
+
+ boolean isPropertyName();
+
+}
diff --git a/json/gen/com/intellij/json/psi/JsonValue.java b/json/gen/com/intellij/json/psi/JsonValue.java
new file mode 100644
index 00000000..66c8d38b
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/JsonValue.java
@@ -0,0 +1,10 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface JsonValue extends JsonElement {
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java b/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java
new file mode 100644
index 00000000..e63eb97e
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java
@@ -0,0 +1,40 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+import com.intellij.navigation.ItemPresentation;
+
+public class JsonArrayImpl extends JsonContainerImpl implements JsonArray {
+
+ public JsonArrayImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitArray(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ @Override
+ @NotNull
+ public List<JsonValue> getValueList() {
+ return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonValue.class);
+ }
+
+ @Nullable
+ public ItemPresentation getPresentation() {
+ return JsonPsiImplUtils.getPresentation(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java
new file mode 100644
index 00000000..0dc43a31
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java
@@ -0,0 +1,32 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public class JsonBooleanLiteralImpl extends JsonLiteralImpl implements JsonBooleanLiteral {
+
+ public JsonBooleanLiteralImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitBooleanLiteral(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ public boolean getValue() {
+ return JsonPsiImplUtils.getValue(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java b/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java
new file mode 100644
index 00000000..3a8bc91a
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java
@@ -0,0 +1,28 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public class JsonContainerImpl extends JsonValueImpl implements JsonContainer {
+
+ public JsonContainerImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitContainer(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java
new file mode 100644
index 00000000..fa6759ef
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java
@@ -0,0 +1,32 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public abstract class JsonLiteralImpl extends JsonLiteralMixin implements JsonLiteral {
+
+ public JsonLiteralImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitLiteral(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ public boolean isQuotedString() {
+ return JsonPsiImplUtils.isQuotedString(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java
new file mode 100644
index 00000000..53499f8b
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java
@@ -0,0 +1,28 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public class JsonNullLiteralImpl extends JsonLiteralImpl implements JsonNullLiteral {
+
+ public JsonNullLiteralImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitNullLiteral(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java
new file mode 100644
index 00000000..a56f43a9
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java
@@ -0,0 +1,32 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public class JsonNumberLiteralImpl extends JsonLiteralImpl implements JsonNumberLiteral {
+
+ public JsonNumberLiteralImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitNumberLiteral(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ public double getValue() {
+ return JsonPsiImplUtils.getValue(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java b/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java
new file mode 100644
index 00000000..0f3d929f
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java
@@ -0,0 +1,40 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+import com.intellij.navigation.ItemPresentation;
+
+public class JsonObjectImpl extends JsonObjectMixin implements JsonObject {
+
+ public JsonObjectImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitObject(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ @Override
+ @NotNull
+ public List<JsonProperty> getPropertyList() {
+ return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonProperty.class);
+ }
+
+ @Nullable
+ public ItemPresentation getPresentation() {
+ return JsonPsiImplUtils.getPresentation(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java b/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java
new file mode 100644
index 00000000..395f686a
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java
@@ -0,0 +1,49 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+import com.intellij.navigation.ItemPresentation;
+
+public class JsonPropertyImpl extends JsonPropertyMixin implements JsonProperty {
+
+ public JsonPropertyImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitProperty(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ @NotNull
+ public String getName() {
+ return JsonPsiImplUtils.getName(this);
+ }
+
+ @NotNull
+ public JsonValue getNameElement() {
+ return JsonPsiImplUtils.getNameElement(this);
+ }
+
+ @Nullable
+ public JsonValue getValue() {
+ return JsonPsiImplUtils.getValue(this);
+ }
+
+ @Nullable
+ public ItemPresentation getPresentation() {
+ return JsonPsiImplUtils.getPresentation(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java b/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java
new file mode 100644
index 00000000..d9ca139c
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java
@@ -0,0 +1,34 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public class JsonReferenceExpressionImpl extends JsonValueImpl implements JsonReferenceExpression {
+
+ public JsonReferenceExpressionImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitReferenceExpression(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ @Override
+ @NotNull
+ public PsiElement getIdentifier() {
+ return findNotNullChildByType(IDENTIFIER);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java
new file mode 100644
index 00000000..365409b9
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java
@@ -0,0 +1,44 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+
+public class JsonStringLiteralImpl extends JsonStringLiteralMixin implements JsonStringLiteral {
+
+ public JsonStringLiteralImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitStringLiteral(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+ @NotNull
+ public List<Pair<TextRange, String>> getTextFragments() {
+ return JsonPsiImplUtils.getTextFragments(this);
+ }
+
+ @NotNull
+ public String getValue() {
+ return JsonPsiImplUtils.getValue(this);
+ }
+
+ public boolean isPropertyName() {
+ return JsonPsiImplUtils.isPropertyName(this);
+ }
+
+}
diff --git a/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java b/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java
new file mode 100644
index 00000000..7b7ce1de
--- /dev/null
+++ b/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java
@@ -0,0 +1,28 @@
+// This is a generated file. Not intended for manual editing.
+package com.intellij.json.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static com.intellij.json.JsonElementTypes.*;
+import com.intellij.json.psi.*;
+
+public abstract class JsonValueImpl extends JsonElementImpl implements JsonValue {
+
+ public JsonValueImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull JsonElementVisitor visitor) {
+ visitor.visitValue(this);
+ }
+
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+}
diff --git a/json/intellij.json.iml b/json/intellij.json.iml
new file mode 100644
index 00000000..b26c976a
--- /dev/null
+++ b/json/intellij.json.iml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="intellij.platform.core" />
+ <orderEntry type="module" module-name="intellij.platform.ide" />
+ <orderEntry type="module" module-name="intellij.platform.lang.impl" />
+ <orderEntry type="module" module-name="intellij.spellchecker" />
+ <orderEntry type="library" name="Guava" level="project" />
+ <orderEntry type="library" name="gson" level="project" />
+ <orderEntry type="module" module-name="intellij.regexp" />
+ </component>
+</module> \ No newline at end of file
diff --git a/json/json.bnf b/json/json.bnf
new file mode 100644
index 00000000..8036cf33
--- /dev/null
+++ b/json/json.bnf
@@ -0,0 +1,145 @@
+{
+ parserClass = 'com.intellij.json.JsonParser'
+ parserUtilClass = "com.intellij.json.psi.JsonParserUtil"
+ psiPackage = 'com.intellij.json.psi'
+ psiImplPackage = 'com.intellij.json.psi.impl'
+
+ elementTypeHolderClass = 'com.intellij.json.JsonElementTypes'
+ elementTypeClass = 'com.intellij.json.JsonElementType'
+ psiClassPrefix = "Json"
+ psiVisitorName = "JsonElementVisitor"
+
+ psiImplUtilClass = 'com.intellij.json.psi.impl.JsonPsiImplUtils'
+ tokenTypeClass = 'com.intellij.json.JsonTokenType'
+
+ implements("value") = "com.intellij.json.psi.JsonElement"
+ extends("value") = "com.intellij.json.psi.impl.JsonElementImpl"
+
+ tokens = [
+ L_CURLY='{'
+ R_CURLY='}'
+ L_BRACKET='['
+ R_BRACKET=']'
+
+ COMMA=','
+ COLON=':'
+ LINE_COMMENT='regexp://.*'
+ // "/*" ([^*]|\*+[^*/])* (\*+"/")?
+ BLOCK_COMMENT='regexp:/\*([^*]|\*+[^*/])*(\*+/)?'
+ // else /\*(?:[^*]|\*[^/])*\*+/
+
+ // unclosed string literal matches till the line's end
+ // any escape sequences included, illegal escapes are indicated by SyntaxHighlighter
+ // and JsonStringLiteralAnnotator
+ DOUBLE_QUOTED_STRING="regexp:\"([^\\\"\r\n]|\\[^\r\n])*\"?"
+ SINGLE_QUOTED_STRING="regexp:'([^\\\'\r\n]|\\[^\r\n])*'?"
+// STRING='regexp:"([^\\"\r\n]|\\([\\"/bfnrt]|u[a-fA-F0-9]{4}))*"?'
+
+ NUMBER='regexp:-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d*)?'
+ TRUE='true'
+ FALSE='false'
+ NULL='null'
+ // Actually not defined in RFC 4627, but may be used for JSON5 and helps with
+ // auto completion of keywords. Semantically, it represents "bad word" type
+ // of tokens
+ // Could be as loose as [^\s\[\]{}:,\"\']+, but is slightly more restricted
+ // for the time being to match most forms of npm package names and semver versions
+ // in package.json.
+ // See https://github.com/npm/validate-npm-package-name
+ IDENTIFIER="regexp:[[:jletterdigit:]~!()*\-./@\^<>=]+"
+ ]
+
+ extends("container|literal|reference_expression")=value
+ extends("array|object")=container
+ extends("string_literal|number_literal|boolean_literal|null_literal")=literal
+ implements("property")=[
+ "com.intellij.json.psi.JsonElement"
+ "com.intellij.psi.PsiNamedElement"
+ ]
+}
+
+// For compatibility we allow any value at root level (see JsonStandardComplianceAnnotator)
+json ::= value+
+
+object ::= '{' object_element* '}' {
+ pin=1
+ methods=[
+ findProperty
+ getPresentation
+ ]
+ mixin="com.intellij.json.psi.impl.JsonObjectMixin"
+}
+
+// Hackity-hack to parse array elements and properties even if separating commas are missing,
+// TODO: Find out if there is any simpler way to do so in GrammarKit
+private object_element ::= property (','|&'}') {
+ recoverWhile = not_brace_or_next_value
+ pin = 1
+}
+
+property ::= property_name (':' value) {
+ methods=[
+ getName
+ getNameElement
+ getValue
+ // suppress getValueList() accessor
+ value=""
+ getPresentation
+ ]
+ mixin="com.intellij.json.psi.impl.JsonPropertyMixin"
+ pin(".*")=1
+}
+
+private property_name ::= literal | reference_expression
+
+array ::= '[' array_element* ']' {
+ methods=[
+ getPresentation
+ ]
+ pin=1
+}
+
+private array_element ::= value (','|&']') {
+ recoverWhile = not_bracket_or_next_value
+ pin=1
+}
+
+string_literal ::= SINGLE_QUOTED_STRING | DOUBLE_QUOTED_STRING {
+ methods=[
+ getTextFragments
+ getValue
+ isPropertyName
+ SINGLE_QUOTED_STRING=""
+ DOUBLE_QUOTED_STRING=""
+ ]
+ mixin="com.intellij.json.psi.impl.JsonStringLiteralMixin"
+}
+number_literal ::= NUMBER {
+ methods=[
+ NUMBER=""
+ getValue
+ ]
+}
+boolean_literal ::= TRUE | FALSE {
+ methods=[
+ getValue
+ ]
+}
+null_literal ::= NULL
+
+literal ::= string_literal | number_literal | boolean_literal | null_literal {
+ methods=[
+ isQuotedString
+ ]
+ mixin="com.intellij.json.psi.impl.JsonLiteralMixin"
+}
+
+fake container ::=
+
+reference_expression ::= IDENTIFIER
+
+value ::= object | array | literal | reference_expression
+
+// Recoveries
+private not_bracket_or_next_value ::= !(']'|value)
+private not_brace_or_next_value ::= !('}'|value) \ No newline at end of file
diff --git a/json/resources/inspectionDescriptions/Json5StandardCompliance.html b/json/resources/inspectionDescriptions/Json5StandardCompliance.html
new file mode 100644
index 00000000..8171150e
--- /dev/null
+++ b/json/resources/inspectionDescriptions/Json5StandardCompliance.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection checks that JSON5 files conform to language specification (http://json5.org/).<br>
+</body>
+</html> \ No newline at end of file
diff --git a/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html b/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html
new file mode 100644
index 00000000..51dead15
--- /dev/null
+++ b/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection checks that object literals don't contain duplicate keys.<br>
+</body>
+</html> \ No newline at end of file
diff --git a/json/resources/inspectionDescriptions/JsonSchemaCompliance.html b/json/resources/inspectionDescriptions/JsonSchemaCompliance.html
new file mode 100644
index 00000000..11c402d7
--- /dev/null
+++ b/json/resources/inspectionDescriptions/JsonSchemaCompliance.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection checks that JSON files conform to JSON Schemas assigned to them<br>
+</body>
+</html> \ No newline at end of file
diff --git a/json/resources/inspectionDescriptions/JsonSchemaRefReference.html b/json/resources/inspectionDescriptions/JsonSchemaRefReference.html
new file mode 100644
index 00000000..714ecb28
--- /dev/null
+++ b/json/resources/inspectionDescriptions/JsonSchemaRefReference.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection checks that '$ref' and '$schema' paths are valid<br>
+</body>
+</html> \ No newline at end of file
diff --git a/json/resources/inspectionDescriptions/JsonStandardCompliance.html b/json/resources/inspectionDescriptions/JsonStandardCompliance.html
new file mode 100644
index 00000000..12ca8a17
--- /dev/null
+++ b/json/resources/inspectionDescriptions/JsonStandardCompliance.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection checks that JSON files conform to language specification (RFC-7159).<br>
+</body>
+</html> \ No newline at end of file
diff --git a/json/src/com/intellij/json/JsonBraceMatcher.java b/json/src/com/intellij/json/JsonBraceMatcher.java
new file mode 100644
index 00000000..d4a8aad3
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBraceMatcher.java
@@ -0,0 +1,34 @@
+package com.intellij.json;
+
+import com.intellij.lang.BracePair;
+import com.intellij.lang.PairedBraceMatcher;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBraceMatcher implements PairedBraceMatcher {
+ private static final BracePair[] PAIRS = {
+ new BracePair(JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET, true),
+ new BracePair(JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY, true)
+ };
+
+ @NotNull
+ @Override
+ public BracePair[] getPairs() {
+ return PAIRS;
+ }
+
+ @Override
+ public boolean isPairedBracesAllowedBeforeType(@NotNull IElementType lbraceType, @Nullable IElementType contextType) {
+ return true;
+ }
+
+ @Override
+ public int getCodeConstructStart(PsiFile file, int openingBraceOffset) {
+ return openingBraceOffset;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonBundle.java b/json/src/com/intellij/json/JsonBundle.java
new file mode 100644
index 00000000..9ccab90f
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBundle.java
@@ -0,0 +1,36 @@
+package com.intellij.json;
+
+import com.intellij.CommonBundle;
+import com.intellij.reference.SoftReference;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.PropertyKey;
+
+import java.lang.ref.Reference;
+import java.util.ResourceBundle;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBundle {
+
+ private static Reference<ResourceBundle> ourBundle;
+ @NonNls public static final String BUNDLE = "com.intellij.json.JsonBundle";
+
+ private JsonBundle() {
+ // empty
+ }
+
+ public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) {
+ return CommonBundle.message(getBundle(), key, params);
+ }
+
+ private static ResourceBundle getBundle() {
+ ResourceBundle bundle = SoftReference.dereference(ourBundle);
+ if (bundle == null) {
+ bundle = ResourceBundle.getBundle(BUNDLE);
+ ourBundle = new SoftReference<>(bundle);
+ }
+ return bundle;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonBundle.properties b/json/src/com/intellij/json/JsonBundle.properties
new file mode 100644
index 00000000..2089ac9a
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBundle.properties
@@ -0,0 +1,66 @@
+json.array=array
+json.object=object
+json.property=property
+
+syntax.error.missing.closing.quote=Missing closing quote
+syntax.error.illegal.escape.sequence=Illegal escape sequence
+syntax.error.illegal.unicode.escape.sequence=Illegal unicode escape sequence
+syntax.error.illegal.floating.point.literal=Illegal floating point literal
+syntax.error.control.char.in.string=Control character ''{0}'' is not allowed in string literals
+
+# Inspections
+json.inspection.group=JSON and JSON5
+
+inspection.compliance.name=Compliance with JSON standard
+inspection.compliance5.name=Compliance with JSON5 standard
+inspection.compliance.msg.comments=JSON standard does not allow comments. Use JSMin or similar tool to remove comments before parsing.
+inspection.compliance.msg.single.quoted.strings=JSON standard does not allow single quoted strings
+inspection.compliance.msg.bad.token=JSON standard does not allow such tokens
+inspection.compliance.msg.illegal.property.key=JSON standard allows only double quoted string as property key
+inspection.compliance.msg.trailing.comma=JSON standard does not allow trailing comma
+inspection.compliance.msg.multiple.top.level.values=JSON standard allows only one top-level value
+
+inspection.compliance.option.comments=Warn about comments
+inspection.compliance.option.multiple.top.level.values=Warn about multiple top-level values
+inspection.compliance.option.trailing.comma=Warn about trailing commas
+inspection.compliance.option.nan.infinity=Warn about NaN and Infinity/-Infinity numeric values
+
+inspection.duplicate.keys.name=Duplicate keys in object literals
+inspection.duplicate.keys.msg.duplicate.keys=Object contains duplicate keys ''{0}''
+
+# Formatter
+formatter.align.properties.caption=Align
+
+formatter.align.properties.none=Do not align
+formatter.align.properties.on.colon=On colon
+formatter.align.properties.on.value=On value
+
+# Quickfixes and editor actions
+quickfix.add.double.quotes.desc=Wrap with double quotes
+
+surround.with.object.literal.desc=object literal
+surround.with.array.literal.desc=array literal
+surround.with.quotes.desc=quotes
+json.template.context.type=JSON
+
+json.copy.to.clipboard=Copy {0} to clipboard
+
+#json schema
+json.schema.add.schema.chooser.title=Select JSON Schema File
+json.schema.annotation.not.allowed.property=Property ''{0}'' is not allowed
+json.schema.conflicting.mappings=Warning: conflicting mappings. <a href="#">Show details</a>
+json.schema.file.selector.title=Schema file or URL:
+json.schema.file.not.found=File not found
+json.schema.inspection.compliance.name=Compliance with JSON schema
+json.schema.inspection.case.insensitive.enum=Case insensitive matching for enum values
+
+json.schema.ref.refs.inspection.name=Unresolved '$ref' and '$schema' references
+json.schema.ref.file.not.found=File ''{0}'' not found
+json.schema.ref.cannot.resolve.path=Cannot resolve path ''{0}''
+json.schema.ref.cannot.resolve.id=Cannot resolve id ''{0}''
+json.schema.ref.no.array.element=Array doesn''t contain element with index '{0}'
+json.schema.ref.no.property=Property ''{0}'' not found
+
+settings.json.schema.add.mapping=Add mapping
+settings.json.schema.edit.mapping=Edit mapping
+settings.json.schema.remove.mapping=Remove mapping \ No newline at end of file
diff --git a/json/src/com/intellij/json/JsonDialectUtil.java b/json/src/com/intellij/json/JsonDialectUtil.java
new file mode 100644
index 00000000..610e3f99
--- /dev/null
+++ b/json/src/com/intellij/json/JsonDialectUtil.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonDialectUtil {
+ public static boolean isStandardJson(@NotNull PsiElement element) {
+ return isStandardJson(getLanguage(element));
+ }
+
+ public static Language getLanguage(@NotNull PsiElement element) {
+ PsiFile file = element.getContainingFile();
+ if (file == null) return JsonLanguage.INSTANCE;
+ Language language = file.getLanguage();
+ return language instanceof JsonLanguage ? language : JsonLanguage.INSTANCE;
+ }
+
+ public static boolean isStandardJson(@Nullable Language language) {
+ return language == JsonLanguage.INSTANCE;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonElementType.java b/json/src/com/intellij/json/JsonElementType.java
new file mode 100644
index 00000000..54e05dfc
--- /dev/null
+++ b/json/src/com/intellij/json/JsonElementType.java
@@ -0,0 +1,11 @@
+package com.intellij.json;
+
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonElementType extends IElementType {
+ public JsonElementType(@NotNull @NonNls String debugName) {
+ super(debugName, JsonLanguage.INSTANCE);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonFileType.java b/json/src/com/intellij/json/JsonFileType.java
new file mode 100644
index 00000000..9b891800
--- /dev/null
+++ b/json/src/com/intellij/json/JsonFileType.java
@@ -0,0 +1,50 @@
+package com.intellij.json;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.lang.Language;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFileType extends LanguageFileType{
+ public static final JsonFileType INSTANCE = new JsonFileType();
+ public static final String DEFAULT_EXTENSION = "json";
+
+ protected JsonFileType(Language language) {
+ super(language);
+ }
+
+ public JsonFileType() {
+ super(JsonLanguage.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JSON";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JSON";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return DEFAULT_EXTENSION;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ // TODO: add JSON icon instead
+ return AllIcons.FileTypes.Json;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonFileTypeFactory.java b/json/src/com/intellij/json/JsonFileTypeFactory.java
new file mode 100644
index 00000000..2cfaa2f1
--- /dev/null
+++ b/json/src/com/intellij/json/JsonFileTypeFactory.java
@@ -0,0 +1,15 @@
+package com.intellij.json;
+
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFileTypeFactory extends FileTypeFactory {
+ @Override
+ public void createFileTypes(@NotNull FileTypeConsumer consumer) {
+ consumer.consume(JsonFileType.INSTANCE, JsonFileType.DEFAULT_EXTENSION);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonLanguage.java b/json/src/com/intellij/json/JsonLanguage.java
new file mode 100644
index 00000000..e43ac474
--- /dev/null
+++ b/json/src/com/intellij/json/JsonLanguage.java
@@ -0,0 +1,22 @@
+package com.intellij.json;
+
+import com.intellij.lang.Language;
+
+public class JsonLanguage extends Language {
+ public static final JsonLanguage INSTANCE = new JsonLanguage();
+
+ protected JsonLanguage(String ID, String... mimeTypes) {
+ super(INSTANCE, ID, mimeTypes);
+ }
+
+ private JsonLanguage() {
+ super("JSON", "application/json", "application/vnd.api+json", "application/hal+json");
+ }
+
+ @Override
+ public boolean isCaseSensitive() {
+ return true;
+ }
+
+ public boolean hasPermissiveStrings() { return false; }
+}
diff --git a/json/src/com/intellij/json/JsonLexer.java b/json/src/com/intellij/json/JsonLexer.java
new file mode 100644
index 00000000..ce771b9d
--- /dev/null
+++ b/json/src/com/intellij/json/JsonLexer.java
@@ -0,0 +1,12 @@
+package com.intellij.json;
+
+import com.intellij.lexer.FlexAdapter;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLexer extends FlexAdapter {
+ public JsonLexer() {
+ super(new _JsonLexer());
+ }
+}
diff --git a/json/src/com/intellij/json/JsonNamesValidator.java b/json/src/com/intellij/json/JsonNamesValidator.java
new file mode 100644
index 00000000..98a5bb34
--- /dev/null
+++ b/json/src/com/intellij/json/JsonNamesValidator.java
@@ -0,0 +1,38 @@
+/*
+ * @author max
+ */
+package com.intellij.json;
+
+import com.intellij.lang.refactoring.NamesValidator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonNamesValidator implements NamesValidator {
+
+ private final JsonLexer myLexer = new JsonLexer();
+
+ @Override
+ public synchronized boolean isKeyword(@NotNull String name, Project project) {
+ myLexer.start(name);
+ return JsonParserDefinition.JSON_KEYWORDS.contains( myLexer.getTokenType() ) && myLexer.getTokenEnd() == name.length();
+ }
+ @Override
+ public synchronized boolean isIdentifier(@NotNull String name, final Project project) {
+ if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) {
+ name = "\"" + name;
+ }
+
+ if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) {
+ name += "\"";
+ }
+
+ myLexer.start(name);
+ IElementType type = myLexer.getTokenType();
+
+ return myLexer.getTokenEnd() == name.length() && (type == JsonElementTypes.DOUBLE_QUOTED_STRING ||
+ type == JsonElementTypes.SINGLE_QUOTED_STRING);
+ }
+
+}
diff --git a/json/src/com/intellij/json/JsonParserDefinition.java b/json/src/com/intellij/json/JsonParserDefinition.java
new file mode 100644
index 00000000..06ecf6cd
--- /dev/null
+++ b/json/src/com/intellij/json/JsonParserDefinition.java
@@ -0,0 +1,83 @@
+package com.intellij.json;
+
+import com.intellij.json.psi.impl.JsonFileImpl;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.json.JsonElementTypes.*;
+
+public class JsonParserDefinition implements ParserDefinition {
+ public static final TokenSet WHITE_SPACES = TokenSet.WHITE_SPACE;
+ public static final TokenSet STRING_LITERALS = TokenSet.create(SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING);
+
+ public static final IFileElementType FILE = new IFileElementType(JsonLanguage.INSTANCE);
+
+ public static final TokenSet JSON_BRACES = TokenSet.create(L_CURLY, R_CURLY);
+ public static final TokenSet JSON_BRACKETS = TokenSet.create(L_BRACKET, R_BRACKET);
+ public static final TokenSet JSON_CONTAINERS = TokenSet.create(OBJECT, ARRAY);
+ public static final TokenSet JSON_BOOLEANS = TokenSet.create(TRUE, FALSE);
+ public static final TokenSet JSON_KEYWORDS = TokenSet.create(TRUE, FALSE, NULL);
+ public static final TokenSet JSON_LITERALS = TokenSet.create(STRING_LITERAL, NUMBER_LITERAL, NULL_LITERAL, TRUE, FALSE);
+ public static final TokenSet JSON_VALUES = TokenSet.orSet(JSON_CONTAINERS, JSON_LITERALS);
+ public static final TokenSet JSON_COMMENTARIES = TokenSet.create(BLOCK_COMMENT, LINE_COMMENT);
+
+
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new JsonLexer();
+ }
+
+ @Override
+ public PsiParser createParser(Project project) {
+ return new JsonParser();
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return FILE;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getWhitespaceTokens() {
+ return WHITE_SPACES;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getCommentTokens() {
+ return JSON_COMMENTARIES;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getStringLiteralElements() {
+ return STRING_LITERALS;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createElement(ASTNode astNode) {
+ return Factory.createElement(astNode);
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new JsonFileImpl(fileViewProvider, JsonLanguage.INSTANCE);
+ }
+
+ @Override
+ public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode astNode, ASTNode astNode2) {
+ return SpaceRequirements.MAY;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonQuoteHandler.java b/json/src/com/intellij/json/JsonQuoteHandler.java
new file mode 100644
index 00000000..d2ff4bcf
--- /dev/null
+++ b/json/src/com/intellij/json/JsonQuoteHandler.java
@@ -0,0 +1,40 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler;
+import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler;
+import com.intellij.json.editor.JsonTypedHandler;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.highlighter.HighlighterIterator;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonQuoteHandler extends SimpleTokenSetQuoteHandler implements MultiCharQuoteHandler {
+ public JsonQuoteHandler() {
+ super(JsonParserDefinition.STRING_LITERALS);
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getClosingQuote(@NotNull HighlighterIterator iterator, int offset) {
+ final IElementType tokenType = iterator.getTokenType();
+ if (tokenType == TokenType.WHITE_SPACE) {
+ final int index = iterator.getStart() - 1;
+ if (index >= 0) {
+ return String.valueOf(iterator.getDocument().getCharsSequence().charAt(index));
+ }
+ }
+ return tokenType == JsonElementTypes.SINGLE_QUOTED_STRING ? "'" : "\"";
+ }
+
+ @Override
+ public void insertClosingQuote(@NotNull Editor editor, int offset, PsiFile file, @NotNull CharSequence closingQuote) {
+ editor.getDocument().insertString(offset, closingQuote);
+ JsonTypedHandler.processPairedBracesComma(closingQuote.charAt(0), editor, file);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonSpellcheckerStrategy.java b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java
new file mode 100644
index 00000000..1463baae
--- /dev/null
+++ b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java
@@ -0,0 +1,87 @@
+package com.intellij.json;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+import com.intellij.spellchecker.inspections.PlainTextSplitter;
+import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
+import com.intellij.spellchecker.tokenizer.TokenConsumer;
+import com.intellij.spellchecker.tokenizer.Tokenizer;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaResolver;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSpellcheckerStrategy extends SpellcheckingStrategy {
+ private final Tokenizer<JsonStringLiteral> ourStringLiteralTokenizer = new Tokenizer<JsonStringLiteral>() {
+ @Override
+ public void tokenize(@NotNull JsonStringLiteral element, TokenConsumer consumer) {
+ final PlainTextSplitter textSplitter = PlainTextSplitter.getInstance();
+ if (element.textContains('\\')) {
+ final List<Pair<TextRange, String>> fragments = element.getTextFragments();
+ for (Pair<TextRange, String> fragment : fragments) {
+ final TextRange fragmentRange = fragment.getFirst();
+ final String escaped = fragment.getSecond();
+ // Fragment without escaping, also not a broken escape sequence or a unicode code point
+ if (escaped.length() == fragmentRange.getLength() && !escaped.startsWith("\\")) {
+ consumer.consumeToken(element, escaped, false, fragmentRange.getStartOffset(), TextRange.allOf(escaped), textSplitter);
+ }
+ }
+ }
+ else {
+ consumer.consumeToken(element, textSplitter);
+ }
+ }
+ };
+
+ private static boolean matchesNameFromSchema(@NotNull JsonStringLiteral element) {
+ final VirtualFile file = PsiUtilCore.getVirtualFile(element);
+ if (file == null) return false;
+
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ if (!service.isApplicableToFile(file)) return false;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(file);
+ if (rootSchema == null) return false;
+
+ String value = element.getValue();
+ if (StringUtil.isEmpty(value)) return false;
+
+ JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER;
+ final PsiElement checkable = walker.goUpToCheckable(element);
+ if (checkable == null) return false;
+ final ThreeState isName = walker.isName(checkable);
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, isName == ThreeState.NO);
+ if (position == null || position.isEmpty() && isName == ThreeState.NO) return false;
+
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, false, position).resolve();
+ if (schemas.isEmpty()) return false;
+
+ return schemas.stream().anyMatch(s -> s.getProperties().keySet().contains(value)
+ || s.getMatchingPatternPropertySchema(value) != null);
+ }
+
+ @NotNull
+ @Override
+ public Tokenizer getTokenizer(PsiElement element) {
+ if (element instanceof JsonStringLiteral) {
+ return matchesNameFromSchema((JsonStringLiteral)element)
+ ? EMPTY_TOKENIZER
+ : ourStringLiteralTokenizer;
+ }
+ return super.getTokenizer(element);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonTokenType.java b/json/src/com/intellij/json/JsonTokenType.java
new file mode 100644
index 00000000..a7752cd8
--- /dev/null
+++ b/json/src/com/intellij/json/JsonTokenType.java
@@ -0,0 +1,11 @@
+package com.intellij.json;
+
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTokenType extends IElementType {
+ public JsonTokenType(@NotNull @NonNls String debugName) {
+ super(debugName, JsonLanguage.INSTANCE);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonUtil.java b/json/src/com/intellij/json/JsonUtil.java
new file mode 100644
index 00000000..51f751b9
--- /dev/null
+++ b/json/src/com/intellij/json/JsonUtil.java
@@ -0,0 +1,94 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.json.psi.*;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonUtil {
+ private JsonUtil() {
+ // empty
+ }
+
+ /**
+ * Clone of C# "as" operator.
+ * Checks if expression has correct type and casts it if it has. Returns null otherwise.
+ * It saves coder from "instanceof / cast" chains.
+ *
+ * Copied from PyCharm's {@code PyUtil}.
+ *
+ * @param expression expression to check
+ * @param cls class to cast
+ * @param <T> class to cast
+ * @return expression casted to appropriate type (if could be casted). Null otherwise.
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public static <T> T as(@Nullable final Object expression, @NotNull final Class<T> cls) {
+ if (expression == null) {
+ return null;
+ }
+ if (cls.isAssignableFrom(expression.getClass())) {
+ return (T)expression;
+ }
+ return null;
+ }
+
+ @Nullable
+ public static <T extends JsonElement> T getPropertyValueOfType(@NotNull final JsonObject object, @NotNull final String name,
+ @NotNull final Class<T> clazz) {
+ final JsonProperty property = object.findProperty(name);
+ if (property == null) return null;
+ return ObjectUtils.tryCast(property.getValue(), clazz);
+ }
+
+ @Nullable
+ public static List<String> getChildAsStringList(@NotNull final JsonObject object, @NotNull final String name) {
+ final JsonArray array = getPropertyValueOfType(object, name, JsonArray.class);
+ if (array != null) return array.getValueList().stream().filter(value -> value instanceof JsonStringLiteral)
+ .map(value -> StringUtil.unquoteString(value.getText())).collect(Collectors.toList());
+ return null;
+ }
+
+ @Nullable
+ public static List<String> getChildAsSingleStringOrList(@NotNull final JsonObject object, @NotNull final String name) {
+ final List<String> list = getChildAsStringList(object, name);
+ if (list != null) return list;
+ final JsonStringLiteral literal = getPropertyValueOfType(object, name, JsonStringLiteral.class);
+ return literal == null ? null : Collections.singletonList(StringUtil.unquoteString(literal.getText()));
+ }
+
+ public static boolean isArrayElement(@NotNull PsiElement element) {
+ return element instanceof JsonValue && element.getParent() instanceof JsonArray;
+ }
+
+ public static int getArrayIndexOfItem(@NotNull PsiElement e) {
+ PsiElement parent = e.getParent();
+ if (!(parent instanceof JsonArray)) return -1;
+ List<JsonValue> elements = ((JsonArray)parent).getValueList();
+ for (int i = 0; i < elements.size(); i++) {
+ if (e == elements.get(i)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static boolean isJsonFile(@NotNull VirtualFile file) {
+ FileType type = file.getFileType();
+ return type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage() instanceof JsonLanguage;
+ }
+}
diff --git a/json/src/com/intellij/json/_JsonLexer.flex b/json/src/com/intellij/json/_JsonLexer.flex
new file mode 100644
index 00000000..e48e9b8d
--- /dev/null
+++ b/json/src/com/intellij/json/_JsonLexer.flex
@@ -0,0 +1,58 @@
+package com.intellij.json;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+%%
+
+%{
+ public _JsonLexer() {
+ this((java.io.Reader)null);
+ }
+%}
+
+%public
+%class _JsonLexer
+%implements FlexLexer
+%function advance
+%type IElementType
+%unicode
+
+EOL=\R
+WHITE_SPACE=\s+
+
+LINE_COMMENT="//".*
+BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")?
+DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n])*\"?
+SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n])*'?
+NUMBER=(-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]*)?)|Infinity|-Infinity|NaN
+IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+
+
+%%
+<YYINITIAL> {
+ {WHITE_SPACE} { return WHITE_SPACE; }
+
+ "{" { return L_CURLY; }
+ "}" { return R_CURLY; }
+ "[" { return L_BRACKET; }
+ "]" { return R_BRACKET; }
+ "," { return COMMA; }
+ ":" { return COLON; }
+ "true" { return TRUE; }
+ "false" { return FALSE; }
+ "null" { return NULL; }
+
+ {LINE_COMMENT} { return LINE_COMMENT; }
+ {BLOCK_COMMENT} { return BLOCK_COMMENT; }
+ {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; }
+ {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; }
+ {NUMBER} { return NUMBER; }
+ {IDENTIFIER} { return IDENTIFIER; }
+
+}
+
+[^] { return BAD_CHARACTER; }
diff --git a/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java
new file mode 100644
index 00000000..7ab178f8
--- /dev/null
+++ b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java
@@ -0,0 +1,73 @@
+package com.intellij.json.breadcrumbs;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonUtil;
+import com.intellij.json.navigation.JsonQualifiedNameKind;
+import com.intellij.json.navigation.JsonQualifiedNameProvider;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.Language;
+import com.intellij.openapi.ide.CopyPasteManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.ui.breadcrumbs.BreadcrumbsProvider;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.impl.JsonSchemaDocumentationProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBreadcrumbsProvider implements BreadcrumbsProvider {
+ private static final Language[] LANGUAGES = new Language[]{JsonLanguage.INSTANCE};
+
+ @Override
+ public Language[] getLanguages() {
+ return LANGUAGES;
+ }
+
+ @Override
+ public boolean acceptElement(@NotNull PsiElement e) {
+ return e instanceof JsonProperty || JsonUtil.isArrayElement(e);
+ }
+
+ @NotNull
+ @Override
+ public String getElementInfo(@NotNull PsiElement e) {
+ if (e instanceof JsonProperty) {
+ return ((JsonProperty)e).getName();
+ }
+ else if (JsonUtil.isArrayElement(e)) {
+ int i = JsonUtil.getArrayIndexOfItem(e);
+ if (i != -1) return String.valueOf(i);
+ }
+ throw new AssertionError("Breadcrumbs can be extracted only from JsonProperty elements or JsonArray child items");
+ }
+
+ @Nullable
+ @Override
+ public String getElementTooltip(@NotNull PsiElement e) {
+ return JsonSchemaDocumentationProvider.findSchemaAndGenerateDoc(e, null, true, null);
+ }
+
+ @NotNull
+ @Override
+ public List<? extends Action> getContextActions(@NotNull PsiElement element) {
+ JsonQualifiedNameKind[] values = JsonQualifiedNameKind.values();
+ List<Action> actions = ContainerUtil.newArrayListWithCapacity(values.length);
+ for (JsonQualifiedNameKind kind: values) {
+ actions.add(new AbstractAction(JsonBundle.message("json.copy.to.clipboard", kind.toString())) {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ CopyPasteManager.getInstance().setContents(new StringSelection(JsonQualifiedNameProvider.generateQualifiedName(element, kind)));
+ }
+ });
+ }
+ return actions;
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java
new file mode 100644
index 00000000..4d05bf51
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.patterns.PsiElementPattern;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCompletionContributor extends CompletionContributor {
+
+ private static final PsiElementPattern.Capture<PsiElement> AFTER_COLON_IN_PROPERTY = psiElement()
+ .afterLeaf(":").withSuperParent(2, JsonProperty.class)
+ .andNot(psiElement().withParent(JsonStringLiteral.class));
+
+ private static final PsiElementPattern.Capture<PsiElement> AFTER_COMMA_OR_BRACKET_IN_ARRAY = psiElement()
+ .afterLeaf("[", ",").withSuperParent(2, JsonArray.class)
+ .andNot(psiElement().withParent(JsonStringLiteral.class));
+
+ public JsonCompletionContributor() {
+ extend(CompletionType.BASIC, AFTER_COLON_IN_PROPERTY, MyKeywordsCompletionProvider.INSTANCE);
+ extend(CompletionType.BASIC, AFTER_COMMA_OR_BRACKET_IN_ARRAY, MyKeywordsCompletionProvider.INSTANCE);
+ }
+
+ private static class MyKeywordsCompletionProvider extends CompletionProvider<CompletionParameters> {
+ private static final MyKeywordsCompletionProvider INSTANCE = new MyKeywordsCompletionProvider();
+ private static final String[] KEYWORDS = new String[]{"null", "true", "false"};
+
+ @Override
+ protected void addCompletions(@NotNull CompletionParameters parameters,
+ @NotNull ProcessingContext context,
+ @NotNull CompletionResultSet result) {
+ for (String keyword : KEYWORDS) {
+ result.addElement(LookupElementBuilder.create(keyword).bold());
+ }
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java
new file mode 100644
index 00000000..c9747679
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalQuickFixBase;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ScrollType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.util.PsiEditorUtil;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonDuplicatePropertyKeysInspection extends LocalInspectionTool {
+ @Nls
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.duplicate.keys.name");
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ return new JsonElementVisitor() {
+ @Override
+ public void visitObject(@NotNull JsonObject o) {
+ final MultiMap<String, PsiElement> keys = new MultiMap<>();
+ for (JsonProperty property : o.getPropertyList()) {
+ keys.putValue(property.getName(), property.getNameElement());
+ }
+ for (Map.Entry<String, Collection<PsiElement>> entry : keys.entrySet()) {
+ final Collection<PsiElement> sameNamedKeys = entry.getValue();
+ final String entryKey = entry.getKey();
+ if (sameNamedKeys.size() > 1) {
+ for (PsiElement element : sameNamedKeys) {
+ holder.registerProblem(element, JsonBundle.message("inspection.duplicate.keys.msg.duplicate.keys", entryKey),
+ new NavigateToDuplicatesFix(sameNamedKeys, element, entryKey));
+ }
+ }
+ }
+ }
+ };
+ }
+
+ private static class NavigateToDuplicatesFix extends LocalQuickFixBase {
+ @NotNull private final Collection<SmartPsiElementPointer> mySameNamedKeys;
+ @NotNull private final SmartPsiElementPointer myElement;
+ @NotNull private final String myEntryKey;
+
+ private NavigateToDuplicatesFix(@NotNull Collection<PsiElement> sameNamedKeys, @NotNull PsiElement element, @NotNull String entryKey) {
+ super("Navigate to duplicates");
+ mySameNamedKeys = sameNamedKeys.stream().map(k -> SmartPointerManager.createPointer(k)).collect(Collectors.toList());
+ myElement = SmartPointerManager.createPointer(element);
+ myEntryKey = entryKey;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ final Editor editor =
+ PsiEditorUtil.Service.getInstance().findEditorByPsiElement(descriptor.getPsiElement());
+ if (editor == null) return;
+ applyFix(editor);
+ }
+
+
+ private void applyFix(@NotNull Editor editor) {
+ final PsiElement currentElement = myElement.getElement();
+ if (mySameNamedKeys.size() == 2) {
+ final Iterator<SmartPsiElementPointer> iterator = mySameNamedKeys.iterator();
+ final PsiElement next = iterator.next().getElement();
+ PsiElement toNavigate = next != currentElement ? next : iterator.next().getElement();
+ if (toNavigate == null) return;
+ navigateTo(editor, toNavigate);
+ }
+ else {
+ final List<PsiElement> allElements =
+ mySameNamedKeys.stream().map(k -> k.getElement()).filter(k -> k != currentElement).collect(Collectors.toList());
+ JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<PsiElement>("Duplicates of '" + myEntryKey + "'", allElements) {
+ @NotNull
+ @Override
+ public Icon getIconFor(PsiElement aValue) {
+ return AllIcons.Nodes.Property;
+ }
+
+ @NotNull
+ @Override
+ public String getTextFor(PsiElement value) {
+ return "'" + myEntryKey + "' at line #" + editor.getDocument().getLineNumber(value.getTextOffset());
+ }
+
+ @Override
+ public int getDefaultOptionIndex() {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ public PopupStep onChosen(PsiElement selectedValue, boolean finalChoice) {
+ navigateTo(editor, selectedValue);
+ return PopupStep.FINAL_CHOICE;
+ }
+
+ @Override
+ public boolean isSpeedSearchEnabled() {
+ return true;
+ }
+ }).showInBestPositionFor(editor);
+ }
+ }
+
+ private static void navigateTo(@NotNull Editor editor, @NotNull PsiElement toNavigate) {
+ editor.getCaretModel().moveToOffset(toNavigate.getTextOffset());
+ editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java
new file mode 100644
index 00000000..4ad034d8
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.codeinsight;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory;
+import com.intellij.json.psi.JsonNumberLiteral;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonReferenceExpression;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLiteralAnnotator implements Annotator {
+
+ private static class Holder {
+ private static final boolean DEBUG = ApplicationManager.getApplication().isUnitTestMode();
+ }
+
+ @Override
+ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ JsonLiteralChecker[] extensions = JsonLiteralChecker.EP_NAME.getExtensions();
+ if (element instanceof JsonReferenceExpression) {
+ highlightPropertyKey(element, holder);
+ }
+ else if (element instanceof JsonStringLiteral) {
+ final JsonStringLiteral stringLiteral = (JsonStringLiteral)element;
+ final int elementOffset = element.getTextOffset();
+ highlightPropertyKey(element, holder);
+ final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(element);
+ final int length = text.length();
+
+ // Check that string literal is closed properly
+ if (length <= 1 || text.charAt(0) != text.charAt(length - 1) || JsonPsiUtil.isEscapedChar(text, length - 1)) {
+ holder.createErrorAnnotation(element, JsonBundle.message("syntax.error.missing.closing.quote"));
+ }
+
+ // Check escapes
+ final List<Pair<TextRange, String>> fragments = stringLiteral.getTextFragments();
+ for (Pair<TextRange, String> fragment: fragments) {
+ for (JsonLiteralChecker checker: extensions) {
+ if (!checker.isApplicable(element)) continue;
+ Pair<TextRange, String> error = checker.getErrorForStringFragment(fragment, stringLiteral);
+ if (error != null) {
+ holder.createErrorAnnotation(error.getFirst().shiftRight(elementOffset), error.second);
+ }
+ }
+ }
+ }
+ else if (element instanceof JsonNumberLiteral) {
+ String text = null;
+ for (JsonLiteralChecker checker: extensions) {
+ if (!checker.isApplicable(element)) continue;
+ if (text == null) {
+ text = JsonPsiUtil.getElementTextWithoutHostEscaping(element);
+ }
+ String error = checker.getErrorForNumericLiteral(text);
+ if (error != null) {
+ holder.createErrorAnnotation(element, error);
+ }
+ }
+ }
+ }
+
+ private static void highlightPropertyKey(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ if (JsonPsiUtil.isPropertyKey(element)) {
+ holder.createInfoAnnotation(element, Holder.DEBUG ? "property key" : null).setTextAttributes(JsonSyntaxHighlighterFactory.JSON_PROPERTY_KEY);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java
new file mode 100644
index 00000000..6b8fe130
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java
@@ -0,0 +1,21 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+public interface JsonLiteralChecker {
+ ExtensionPointName<JsonLiteralChecker> EP_NAME = ExtensionPointName.create("com.intellij.json.jsonLiteralChecker");
+
+ @Nullable
+ String getErrorForNumericLiteral(String literalText);
+
+ @Nullable
+ Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragmentText, JsonStringLiteral stringLiteral);
+
+ boolean isApplicable(PsiElement element);
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java
new file mode 100644
index 00000000..564c2d85
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java
@@ -0,0 +1,234 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeHighlighting.HighlightDisplayLevel;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Compliance checks include
+ * <ul>
+ * <li>Usage of line and block commentaries</li>
+ * <li>Usage of single quoted strings</li>
+ * <li>Usage of identifiers (unqouted words)</li>
+ * <li>Not double quoted string literal is used as property key</li>
+ * <li>Multiple top-level values</li>
+ * </ul>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonStandardComplianceInspection extends LocalInspectionTool {
+ private static final Logger LOG = Logger.getInstance(JsonStandardComplianceInspection.class);
+
+ public boolean myWarnAboutComments = true;
+ public boolean myWarnAboutNanInfinity = true;
+ public boolean myWarnAboutTrailingCommas = true;
+ public boolean myWarnAboutMultipleTopLevelValues = true;
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.compliance.name");
+ }
+
+ @NotNull
+ @Override
+ public HighlightDisplayLevel getDefaultLevel() {
+ return HighlightDisplayLevel.ERROR;
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ if (!JsonDialectUtil.isStandardJson(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
+ return new StandardJsonValidatingElementVisitor(holder);
+ }
+
+ @Nullable
+ private static PsiElement findTrailingComma(@NotNull JsonContainer container, @NotNull IElementType ending) {
+ final PsiElement lastChild = container.getLastChild();
+ if (lastChild.getNode().getElementType() != ending) {
+ return null;
+ }
+ final PsiElement beforeEnding = PsiTreeUtil.skipWhitespacesAndCommentsBackward(lastChild);
+ if (beforeEnding != null && beforeEnding.getNode().getElementType() == JsonElementTypes.COMMA) {
+ return beforeEnding;
+ }
+ return null;
+ }
+
+
+ @Override
+ public JComponent createOptionsPanel() {
+ final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this);
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.comments"), "myWarnAboutComments");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.multiple.top.level.values"), "myWarnAboutMultipleTopLevelValues");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.trailing.comma"), "myWarnAboutTrailingCommas");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.nan.infinity"), "myWarnAboutNanInfinity");
+ return optionsPanel;
+ }
+
+ private static class AddDoubleQuotesFix implements LocalQuickFix {
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return JsonBundle.message("quickfix.add.double.quotes.desc");
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ final PsiElement element = descriptor.getPsiElement();
+ final String rawText = element.getText();
+ if (element instanceof JsonLiteral || element instanceof JsonReferenceExpression) {
+ String content = JsonPsiUtil.stripQuotes(rawText);
+ if (element instanceof JsonStringLiteral && rawText.startsWith("'")) {
+ content = escapeSingleQuotedStringContent(content);
+ }
+ final PsiElement replacement = new JsonElementGenerator(project).createValue("\"" + content + "\"");
+ CodeStyleManager.getInstance(project).performActionWithFormatterDisabled((Runnable)() -> element.replace(replacement));
+ }
+ else {
+ LOG.error("Quick fix was applied to unexpected element", rawText, element.getParent().getText());
+ }
+ }
+
+ @NotNull
+ private static String escapeSingleQuotedStringContent(@NotNull String content) {
+ final StringBuilder result = new StringBuilder();
+ boolean nextCharEscaped = false;
+ for (int i = 0; i < content.length(); i++) {
+ final char c = content.charAt(i);
+ if ((nextCharEscaped && c != '\'') || (!nextCharEscaped && c == '"')) {
+ result.append('\\');
+ }
+ if (c != '\\' || nextCharEscaped) {
+ result.append(c);
+ nextCharEscaped = false;
+ }
+ else {
+ nextCharEscaped = true;
+ }
+ }
+ if (nextCharEscaped) {
+ result.append('\\');
+ }
+ return result.toString();
+ }
+ }
+
+ protected class StandardJsonValidatingElementVisitor extends JsonElementVisitor {
+ private final ProblemsHolder myHolder;
+
+ public StandardJsonValidatingElementVisitor(ProblemsHolder holder) {myHolder = holder;}
+
+ protected boolean allowComments() { return false; }
+ protected boolean allowSingleQuotes() { return false; }
+ protected boolean allowIdentifierPropertyNames() { return false; }
+ protected boolean allowTrailingCommas() { return false; }
+
+ protected boolean isValidPropertyName(@NotNull PsiElement literal) {
+ return literal instanceof JsonLiteral && JsonPsiUtil.getElementTextWithoutHostEscaping(literal).startsWith("\"");
+ }
+
+ @Override
+ public void visitComment(PsiComment comment) {
+ if (!allowComments() && myWarnAboutComments) {
+ if (JsonStandardComplianceProvider.shouldWarnAboutComment(comment)) {
+ myHolder.registerProblem(comment, JsonBundle.message("inspection.compliance.msg.comments"));
+ }
+ }
+ }
+
+ @Override
+ public void visitStringLiteral(@NotNull JsonStringLiteral stringLiteral) {
+ if (!allowSingleQuotes() && JsonPsiUtil.getElementTextWithoutHostEscaping(stringLiteral).startsWith("'")) {
+ myHolder.registerProblem(stringLiteral, JsonBundle.message("inspection.compliance.msg.single.quoted.strings"),
+ new AddDoubleQuotesFix());
+ }
+ // May be illegal property key as well
+ super.visitStringLiteral(stringLiteral);
+ }
+
+ @Override
+ public void visitLiteral(@NotNull JsonLiteral literal) {
+ if (JsonPsiUtil.isPropertyKey(literal) && !isValidPropertyName(literal)) {
+ myHolder.registerProblem(literal, JsonBundle.message("inspection.compliance.msg.illegal.property.key"), new AddDoubleQuotesFix());
+ }
+
+ // for standard JSON, the inspection for NaN, Infinity and -Infinity is now configurable
+ if (!allowNanInfinity() && literal instanceof JsonNumberLiteral && myWarnAboutNanInfinity) {
+ final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(literal);
+ if (StandardJsonLiteralChecker.INF.equals(text) ||
+ StandardJsonLiteralChecker.MINUS_INF.equals(text) ||
+ StandardJsonLiteralChecker.NAN.equals(text)) {
+ myHolder.registerProblem(literal, JsonBundle.message("syntax.error.illegal.floating.point.literal"));
+ }
+ }
+ super.visitLiteral(literal);
+ }
+
+ protected boolean allowNanInfinity() {
+ return false;
+ }
+
+ @Override
+ public void visitReferenceExpression(@NotNull JsonReferenceExpression reference) {
+ if (!allowIdentifierPropertyNames() || !JsonPsiUtil.isPropertyKey(reference) || !isValidPropertyName(reference)) {
+ myHolder.registerProblem(reference, JsonBundle.message("inspection.compliance.msg.bad.token"), new AddDoubleQuotesFix());
+ }
+ // May be illegal property key as well
+ super.visitReferenceExpression(reference);
+ }
+
+ @Override
+ public void visitArray(@NotNull JsonArray array) {
+ if (myWarnAboutTrailingCommas && !allowTrailingCommas()) {
+ final PsiElement trailingComma = findTrailingComma(array, JsonElementTypes.R_BRACKET);
+ if (trailingComma != null) {
+ myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma"));
+ }
+ }
+ super.visitArray(array);
+ }
+
+ @Override
+ public void visitObject(@NotNull JsonObject object) {
+ if (myWarnAboutTrailingCommas && !allowTrailingCommas()) {
+ final PsiElement trailingComma = findTrailingComma(object, JsonElementTypes.R_CURLY);
+ if (trailingComma != null) {
+ myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma"));
+ }
+ }
+ super.visitObject(object);
+ }
+
+ @Override
+ public void visitValue(@NotNull JsonValue value) {
+ if (value.getContainingFile() instanceof JsonFile) {
+ final JsonFile jsonFile = (JsonFile)value.getContainingFile();
+ if (myWarnAboutMultipleTopLevelValues && value.getParent() == jsonFile && value != jsonFile.getTopLevelValue()) {
+ myHolder.registerProblem(value, JsonBundle.message("inspection.compliance.msg.multiple.top.level.values"));
+ }
+ }
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java
new file mode 100644
index 00000000..996cc82a
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.codeinsight;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiComment;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Allows to configure a compliance level for JSON.
+ * For example, some tools ignore comments in JSON silently when parsing, so there is no need to warn users about it.
+ */
+public abstract class JsonStandardComplianceProvider {
+ public static final ExtensionPointName<JsonStandardComplianceProvider> EP_NAME =
+ ExtensionPointName.create("com.intellij.json.jsonStandardComplianceProvider");
+
+ public abstract boolean isCommentAllowed(@NotNull PsiComment comment);
+
+ public static boolean shouldWarnAboutComment(@NotNull PsiComment comment) {
+ JsonStandardComplianceProvider[] providers = EP_NAME.getExtensions();
+ if (providers.length == 0) {
+ return true;
+ }
+ for (JsonStandardComplianceProvider provider : providers) {
+ if (provider.isCommentAllowed(comment)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java
new file mode 100644
index 00000000..ebb20bf5
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java
@@ -0,0 +1,84 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeInsight.AutoPopupController;
+import com.intellij.codeInsight.completion.InsertHandler;
+import com.intellij.codeInsight.completion.InsertionContext;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonStringPropertyInsertHandler implements InsertHandler<LookupElement> {
+
+ private final String myNewValue;
+
+ public JsonStringPropertyInsertHandler(@NotNull String newValue) {
+ myNewValue = newValue;
+ }
+
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ PsiElement element = context.getFile().findElementAt(context.getStartOffset());
+ JsonStringLiteral literal = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class, false);
+ if (literal == null) return;
+ JsonProperty property = ObjectUtils.tryCast(literal.getParent(), JsonProperty.class);
+ if (property == null) return;
+ final TextRange toDelete;
+ String textToInsert = "";
+ TextRange literalRange = literal.getTextRange();
+ if (literal.getValue().equals(myNewValue)) {
+ toDelete = new TextRange(literalRange.getEndOffset(), literalRange.getEndOffset());
+ }
+ else {
+ toDelete = literalRange;
+ textToInsert = StringUtil.wrapWithDoubleQuote(myNewValue);
+ }
+ int newCaretOffset = literalRange.getStartOffset() + 1 + myNewValue.length();
+ boolean showAutoPopup = false;
+ if (property.getNameElement().equals(literal)) {
+ if (property.getValue() == null) {
+ textToInsert += ":\"\"";
+ newCaretOffset += 3; // "package<caret offset>":"<new caret offset>"
+ if (needCommaAfter(property)) {
+ textToInsert += ",";
+ }
+ showAutoPopup = true;
+ }
+ }
+ context.getDocument().replaceString(toDelete.getStartOffset(), toDelete.getEndOffset(), textToInsert);
+ context.getEditor().getCaretModel().moveToOffset(newCaretOffset);
+ reformat(context, toDelete.getStartOffset(), toDelete.getStartOffset() + textToInsert.length());
+ if (showAutoPopup) {
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ }
+
+ private static boolean needCommaAfter(@NotNull JsonProperty property) {
+ PsiElement element = property.getNextSibling();
+ while (element != null) {
+ if (element instanceof JsonProperty) {
+ return true;
+ }
+ if (element.getNode().getElementType() == JsonElementTypes.COMMA) {
+ return false;
+ }
+ element = element.getNextSibling();
+ }
+ return false;
+ }
+
+ private static void reformat(@NotNull InsertionContext context, int startOffset, int endOffset) {
+ PsiDocumentManager.getInstance(context.getProject()).commitDocument(context.getDocument());
+ CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(context.getProject());
+ codeStyleManager.reformatText(context.getFile(), startOffset, endOffset);
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java
new file mode 100644
index 00000000..06b87816
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java
@@ -0,0 +1,73 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public class StandardJsonLiteralChecker implements JsonLiteralChecker {
+ public static final Pattern VALID_ESCAPE = Pattern.compile("\\\\([\"\\\\/bfnrt]|u[0-9a-fA-F]{4})");
+ private static final Pattern VALID_NUMBER_LITERAL = Pattern.compile("-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?");
+ public static final String INF = "Infinity";
+ public static final String MINUS_INF = "-Infinity";
+ public static final String NAN = "NaN";
+
+ @Nullable
+ @Override
+ public String getErrorForNumericLiteral(String literalText) {
+ if (!INF.equals(literalText) &&
+ !MINUS_INF.equals(literalText) &&
+ !NAN.equals(literalText) &&
+ !VALID_NUMBER_LITERAL.matcher(literalText).matches()) {
+ return JsonBundle.message("syntax.error.illegal.floating.point.literal");
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) {
+ if (fragment.getSecond().chars().anyMatch(c -> c <= '\u001F')) { // fragments are cached, string values - aren't; go inside only if we encountered a potentially 'wrong' char
+ final String text = stringLiteral.getText();
+ if (new TextRange(0, text.length()).contains(fragment.first)) {
+ final int startOffset = fragment.first.getStartOffset();
+ final String part = text.substring(startOffset, fragment.first.getEndOffset());
+ char[] array = part.toCharArray();
+ for (int i = 0; i < array.length; i++) {
+ char c = array[i];
+ if (c <= '\u001F') {
+ return Pair.create(new TextRange(startOffset + i, startOffset + i + 1),
+ JsonBundle
+ .message("syntax.error.control.char.in.string", "\\u" + Integer.toHexString(c | 0x10000).substring(1)));
+ }
+ }
+ }
+ }
+ final String error = getStringError(fragment.second);
+ return error == null ? null : Pair.create(fragment.first, error);
+ }
+
+ @Nullable
+ public static String getStringError(String fragmentText) {
+ if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && !VALID_ESCAPE.matcher(fragmentText).matches()) {
+ if (fragmentText.startsWith("\\u")) {
+ return JsonBundle.message("syntax.error.illegal.unicode.escape.sequence");
+ }
+ else {
+ return JsonBundle.message("syntax.error.illegal.escape.sequence");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isApplicable(PsiElement element) {
+ return JsonDialectUtil.isStandardJson(element);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCommenter.java b/json/src/com/intellij/json/editor/JsonCommenter.java
new file mode 100644
index 00000000..699f7006
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCommenter.java
@@ -0,0 +1,41 @@
+package com.intellij.json.editor;
+
+import com.intellij.lang.Commenter;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * JSON standard (RFC 4627) doesn't allow comments in documents, but they are added for compatibility with legacy JSON integration.
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonCommenter implements Commenter {
+ @Nullable
+ @Override
+ public String getLineCommentPrefix() {
+ return "//";
+ }
+
+ @Nullable
+ @Override
+ public String getBlockCommentPrefix() {
+ return "/*";
+ }
+
+ @Nullable
+ @Override
+ public String getBlockCommentSuffix() {
+ return "*/";
+ }
+
+ @Nullable
+ @Override
+ public String getCommentedBlockCommentPrefix() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getCommentedBlockCommentSuffix() {
+ return null;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java
new file mode 100644
index 00000000..feb7068b
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java
@@ -0,0 +1,169 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
+import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
+import com.intellij.ide.scratch.ScratchFileType;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.RangeMarker;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.util.Collections;
+import java.util.List;
+
+public class JsonCopyPastePostProcessor extends CopyPastePostProcessor<TextBlockTransferableData> {
+ static final List<TextBlockTransferableData> DATA_LIST = Collections.singletonList(new DumbData());
+ static class DumbData implements TextBlockTransferableData {
+ private static final DataFlavor DATA_FLAVOR = new DataFlavor(JsonCopyPastePostProcessor.class, "class: JsonCopyPastePostProcessor");
+ @Override
+ public DataFlavor getFlavor() {
+ return DATA_FLAVOR;
+ }
+
+ @Override
+ public int getOffsetCount() {
+ return 0;
+ }
+
+ @Override
+ public int getOffsets(int[] offsets, int index) {
+ return index;
+ }
+
+ @Override
+ public int setOffsets(int[] offsets, int index) {
+ return index;
+ }
+ }
+
+ @NotNull
+ @Override
+ public List<TextBlockTransferableData> collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) {
+ return ContainerUtil.emptyList();
+ }
+
+ @NotNull
+ @Override
+ public List<TextBlockTransferableData> extractTransferableData(Transferable content) {
+ // if this list is empty, processTransferableData won't be called
+ return DATA_LIST;
+ }
+
+ @Override
+ public void processTransferableData(Project project,
+ Editor editor,
+ RangeMarker bounds,
+ int caretOffset,
+ Ref<Boolean> indented,
+ List<TextBlockTransferableData> values) {
+ fixCommasOnPaste(project, editor, bounds);
+ }
+
+ private static void fixCommasOnPaste(@NotNull Project project, @NotNull Editor editor, @NotNull RangeMarker bounds) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_PASTE) return;
+
+ if (!isJsonEditor(project, editor)) return;
+
+ final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+ manager.commitDocument(editor.getDocument());
+ final PsiFile psiFile = manager.getPsiFile(editor.getDocument());
+ if (psiFile == null) return;
+ fixTrailingComma(bounds, psiFile, manager);
+ fixLeadingComma(bounds, psiFile, manager);
+ }
+
+ private static boolean isJsonEditor(@NotNull Project project,
+ @NotNull Editor editor) {
+ final VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
+ if (file == null) return false;
+ final FileType fileType = file.getFileType();
+ if (fileType instanceof JsonFileType) return true;
+ if (!(fileType instanceof ScratchFileType)) return false;
+ return PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()) instanceof JsonFile;
+ }
+
+ private static void fixLeadingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) {
+ final PsiElement startElement = skipWhitespaces(psiFile.findElementAt(bounds.getStartOffset()));
+ PsiElement propertyOrArrayItem = startElement instanceof JsonProperty ? startElement : getParentPropertyOrArrayItem(startElement);
+
+ if (propertyOrArrayItem == null) return;
+
+ PsiElement prevSibling = PsiTreeUtil.skipWhitespacesBackward(propertyOrArrayItem);
+ if (prevSibling instanceof PsiErrorElement) {
+ final int offset = prevSibling.getTextRange().getEndOffset();
+ ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(offset, ","));
+ manager.commitDocument(bounds.getDocument());
+ }
+ }
+
+ @Nullable
+ private static PsiElement getParentPropertyOrArrayItem(@Nullable PsiElement startElement) {
+ PsiElement propertyOrArrayItem = PsiTreeUtil.getParentOfType(startElement, JsonProperty.class, JsonArray.class);
+ if (propertyOrArrayItem instanceof JsonArray) {
+ for (JsonValue value : ((JsonArray)propertyOrArrayItem).getValueList()) {
+ if (PsiTreeUtil.isAncestor(value, startElement, false)) {
+ return value;
+ }
+ }
+ return null;
+ }
+ return propertyOrArrayItem;
+ }
+
+ private static void fixTrailingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) {
+ PsiElement endElement = skipWhitespaces(psiFile.findElementAt(bounds.getEndOffset() - 1));
+ if (endElement != null && endElement.getTextOffset() >= bounds.getEndOffset()) {
+ endElement = PsiTreeUtil.skipWhitespacesBackward(endElement);
+ }
+
+ if (endElement instanceof LeafPsiElement && ((LeafPsiElement)endElement).getElementType() == JsonElementTypes.COMMA) {
+ final PsiElement nextNext = skipWhitespaces(endElement.getNextSibling());
+ if (nextNext instanceof LeafPsiElement && (((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_CURLY ||
+ ((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_BRACKET)) {
+ PsiElement finalEndElement = endElement;
+ ApplicationManager.getApplication().runWriteAction(() -> finalEndElement.delete());
+ }
+ }
+ else {
+ final PsiElement property = getParentPropertyOrArrayItem(endElement);
+ if (endElement instanceof PsiErrorElement || property != null && skipWhitespaces(property.getNextSibling()) instanceof PsiErrorElement) {
+ PsiElement finalEndElement1 = endElement;
+ ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(getOffset(property, finalEndElement1), ","));
+ manager.commitDocument(bounds.getDocument());
+ }
+ }
+ }
+
+ private static int getOffset(@Nullable PsiElement property, @Nullable PsiElement finalEndElement1) {
+ if (finalEndElement1 instanceof PsiErrorElement) return finalEndElement1.getTextOffset();
+ assert finalEndElement1 != null;
+ return property != null ? property.getTextRange().getEndOffset() : finalEndElement1.getTextOffset();
+ }
+
+ @Nullable
+ private static PsiElement skipWhitespaces(@Nullable PsiElement element) {
+ while (element instanceof PsiWhiteSpace) {
+ element = element.getNextSibling();
+ }
+ return element;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java
new file mode 100644
index 00000000..e85e7167
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java
@@ -0,0 +1,72 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.CopyPastePreProcessor;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.RawText;
+import com.intellij.openapi.editor.SelectionModel;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonCopyPasteProcessor implements CopyPastePreProcessor {
+ @Nullable
+ @Override
+ public String preprocessOnCopy(PsiFile file, int[] startOffsets, int[] endOffsets, String text) {
+ if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) {
+ return null;
+ }
+ if (!file.isPhysical() || startOffsets.length > 1 || endOffsets.length > 1) {
+ return null;
+ }
+ final int selectionStart = startOffsets[0];
+ final int selectionEnd = endOffsets[0];
+ final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd);
+
+ if (literalExpression == null) {
+ return null;
+ }
+
+ return StringUtil.unescapeStringCharacters(StringUtil.replaceUnicodeEscapeSequences(text));
+ }
+
+ @Nullable
+ private static JsonStringLiteral getSingleElementFromSelectionOrNull(PsiFile file, int start, int end) {
+ final PsiElement element = file.findElementAt(start);
+ final JsonStringLiteral literalExpression = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class);
+ if (literalExpression == null) return null;
+ TextRange textRange = literalExpression.getTextRange();
+ if (start <= textRange.getStartOffset() || end >= textRange.getEndOffset()) return null;
+ String text = literalExpression.getText();
+ if (!text.startsWith("\"") || !text.endsWith("\"")) return null;
+ return literalExpression;
+ }
+
+ @NotNull
+ @Override
+ public String preprocessOnPaste(Project project, PsiFile file, Editor editor, String text, RawText rawText) {
+ if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) {
+ return text;
+ }
+ if (!file.isPhysical()) {
+ return text;
+ }
+
+ final SelectionModel selectionModel = editor.getSelectionModel();
+ final int selectionStart = selectionModel.getSelectionStart();
+ final int selectionEnd = selectionModel.getSelectionEnd();
+
+ final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd);
+ if (literalExpression == null) {
+ return text;
+ }
+
+ return StringUtil.escapeStringCharacters(text);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonEditorOptions.java b/json/src/com/intellij/json/editor/JsonEditorOptions.java
new file mode 100644
index 00000000..53726fc9
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonEditorOptions.java
@@ -0,0 +1,38 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@State(
+ name = "JsonEditorOptions",
+ storages = @Storage("editor.xml")
+)
+public class JsonEditorOptions implements PersistentStateComponent<JsonEditorOptions> {
+ public boolean COMMA_ON_ENTER = true;
+ public boolean COMMA_ON_MATCHING_BRACES = true;
+ public boolean COMMA_ON_PASTE = true;
+ public boolean AUTO_QUOTE_PROP_NAME = true;
+ public boolean AUTO_WHITESPACE_AFTER_COLON = true;
+ public boolean ESCAPE_PASTED_TEXT = true;
+
+ @Nullable
+ @Override
+ public JsonEditorOptions getState() {
+ return this;
+ }
+
+ @Override
+ public void loadState(@NotNull JsonEditorOptions state) {
+ XmlSerializerUtil.copyBean(state, this);
+ }
+
+ public static JsonEditorOptions getInstance() {
+ return ServiceManager.getService(JsonEditorOptions.class);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonEnterHandler.java b/json/src/com/intellij/json/editor/JsonEnterHandler.java
new file mode 100644
index 00000000..53a2a047
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonEnterHandler.java
@@ -0,0 +1,147 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.EnterHandler;
+import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.*;
+import com.intellij.lang.Language;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiErrorElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonEnterHandler extends EnterHandlerDelegateAdapter {
+ @Override
+ public Result preprocessEnter(@NotNull PsiFile file,
+ @NotNull Editor editor,
+ @NotNull Ref<Integer> caretOffsetRef,
+ @NotNull Ref<Integer> caretAdvanceRef,
+ @NotNull DataContext dataContext,
+ EditorActionHandler originalHandler) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_ENTER) {
+ return Result.Continue;
+ }
+
+ Language language = EnterHandler.getLanguage(dataContext);
+ if (!(language instanceof JsonLanguage)) {
+ return Result.Continue;
+ }
+
+ int caretOffset = caretOffsetRef.get().intValue();
+ PsiElement psiAtOffset = file.findElementAt(caretOffset);
+
+ if (psiAtOffset == null) {
+ return Result.Continue;
+ }
+
+ if (psiAtOffset instanceof LeafPsiElement && handleComma(caretOffsetRef, psiAtOffset, editor)) {
+ return Result.Continue;
+ }
+
+ JsonValue literal = ObjectUtils.tryCast(psiAtOffset.getParent(), JsonValue.class);
+ if (literal != null && (!(literal instanceof JsonStringLiteral) || !((JsonLanguage)language).hasPermissiveStrings())) {
+ handleJsonValue(literal, editor, caretOffsetRef);
+ }
+
+ return Result.Continue;
+ }
+
+ private static boolean handleComma(@NotNull Ref<Integer> caretOffsetRef, @NotNull PsiElement psiAtOffset, @NotNull Editor editor) {
+ PsiElement nextSibling = psiAtOffset;
+ while (nextSibling instanceof PsiWhiteSpace) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+
+ LeafPsiElement leafPsiElement = ObjectUtils.tryCast(nextSibling, LeafPsiElement.class);
+ IElementType elementType = leafPsiElement == null ? null : leafPsiElement.getElementType();
+ if (elementType == JsonElementTypes.COMMA || elementType == JsonElementTypes.R_CURLY) {
+ PsiElement prevSibling = nextSibling.getPrevSibling();
+ while (prevSibling instanceof PsiWhiteSpace) {
+ prevSibling = prevSibling.getPrevSibling();
+ }
+
+ if (prevSibling instanceof JsonProperty && ((JsonProperty)prevSibling).getValue() != null) {
+ int offset = elementType == JsonElementTypes.COMMA ? nextSibling.getTextRange().getEndOffset() : prevSibling.getTextRange().getEndOffset();
+ if (offset < editor.getDocument().getTextLength()) {
+ if (elementType == JsonElementTypes.R_CURLY) {
+ editor.getDocument().insertString(offset, ",");
+ offset++;
+ }
+ caretOffsetRef.set(offset);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ if (nextSibling instanceof JsonProperty) {
+ PsiElement prevSibling = nextSibling.getPrevSibling();
+ while (prevSibling instanceof PsiWhiteSpace || prevSibling instanceof PsiErrorElement) {
+ prevSibling = prevSibling.getPrevSibling();
+ }
+
+ if (prevSibling instanceof JsonProperty) {
+ int offset = prevSibling.getTextRange().getEndOffset();
+ if (offset < editor.getDocument().getTextLength()) {
+ editor.getDocument().insertString(offset, ",");
+ caretOffsetRef.set(offset + 1);
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void handleJsonValue(@NotNull JsonValue literal, @NotNull Editor editor, @NotNull Ref<Integer> caretOffsetRef) {
+ PsiElement parent = literal.getParent();
+ if (!(parent instanceof JsonProperty) || ((JsonProperty)parent).getValue() != literal) {
+ return;
+ }
+
+ PsiElement nextSibling = parent.getNextSibling();
+ while (nextSibling instanceof PsiWhiteSpace || nextSibling instanceof PsiErrorElement) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+
+ int offset = literal.getTextRange().getEndOffset();
+
+ if (literal instanceof JsonObject || literal instanceof JsonArray) {
+ if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA
+ || !(nextSibling instanceof JsonProperty)) {
+ return;
+ }
+ Document document = editor.getDocument();
+ if (offset < document.getTextLength()) {
+ document.insertString(offset, ",");
+ }
+ return;
+ }
+
+ if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA) {
+ offset = nextSibling.getTextRange().getEndOffset();
+ }
+ else {
+ Document document = editor.getDocument();
+ if (offset < document.getTextLength()) {
+ document.insertString(offset, ",");
+ }
+ offset++;
+ }
+
+ if (offset < editor.getDocument().getTextLength()) {
+ caretOffsetRef.set(offset);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java
new file mode 100644
index 00000000..83ac5b2f
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java
@@ -0,0 +1,42 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.openapi.options.BeanConfigurable;
+import com.intellij.openapi.options.UnnamedConfigurable;
+import com.intellij.ui.IdeBorderFactory;
+
+import javax.swing.*;
+
+public class JsonSmartKeysConfigurable extends BeanConfigurable<JsonEditorOptions> implements UnnamedConfigurable {
+ public JsonSmartKeysConfigurable() {
+ super(JsonEditorOptions.getInstance());
+ JsonEditorOptions settings = getInstance();
+
+ checkBox("Insert missing comma on enter",
+ () -> settings.COMMA_ON_ENTER,
+ v -> settings.COMMA_ON_ENTER = v);
+ checkBox("Insert missing comma after matching braces and quotes",
+ () -> settings.COMMA_ON_MATCHING_BRACES,
+ v -> settings.COMMA_ON_MATCHING_BRACES = v);
+ checkBox("Automatically manage commas when pasting JSON fragments",
+ () -> settings.COMMA_ON_PASTE,
+ v -> settings.COMMA_ON_PASTE = v);
+ checkBox("Escape text on paste in string literals",
+ () -> settings.ESCAPE_PASTED_TEXT,
+ v -> settings.ESCAPE_PASTED_TEXT = v);
+ checkBox("Automatically add quotes to property names when typing ':'",
+ () -> settings.AUTO_QUOTE_PROP_NAME,
+ v -> settings.AUTO_QUOTE_PROP_NAME = v);
+ checkBox("Automatically add whitespace when typing ':' after property namess",
+ () -> settings.AUTO_WHITESPACE_AFTER_COLON,
+ v -> settings.AUTO_WHITESPACE_AFTER_COLON = v);
+ }
+
+ @Override
+ public JComponent createComponent() {
+ JComponent result = super.createComponent();
+ assert result != null;
+ result.setBorder(IdeBorderFactory.createTitledBorder("JSON"));
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonTypedHandler.java b/json/src/com/intellij/json/editor/JsonTypedHandler.java
new file mode 100644
index 00000000..7214f238
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonTypedHandler.java
@@ -0,0 +1,142 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
+import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.*;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTypedHandler extends TypedHandlerDelegate {
+
+ private boolean myWhitespaceAdded;
+
+ @NotNull
+ @Override
+ public Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+ if (file instanceof JsonFile) {
+ processPairedBracesComma(c, editor, file);
+ addWhiteSpaceAfterColonIfNeeded(c, editor, file);
+ removeRedundantWhitespaceIfAfterColon(c, editor, file);
+ }
+ return Result.CONTINUE;
+ }
+
+ private void removeRedundantWhitespaceIfAfterColon(char c, Editor editor, PsiFile file) {
+ if (!myWhitespaceAdded || c != ' ' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) {
+ if (c != ':') {
+ myWhitespaceAdded = false;
+ }
+ return;
+ }
+ int offset = editor.getCaretModel().getOffset();
+ PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
+ final PsiElement element = file.findElementAt(offset);
+ if (element instanceof PsiWhiteSpace) {
+ editor.getDocument().deleteString(offset - 1, offset);
+ }
+ myWhitespaceAdded = false;
+ }
+
+ @NotNull
+ @Override
+ public Result beforeCharTyped(char c,
+ @NotNull Project project,
+ @NotNull Editor editor,
+ @NotNull PsiFile file,
+ @NotNull FileType fileType) {
+ if (file instanceof JsonFile) {
+ addPropertyNameQuotesIfNeeded(c, editor, file);
+ }
+ return Result.CONTINUE;
+ }
+
+ private void addWhiteSpaceAfterColonIfNeeded(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (c != ':' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) {
+ if (c != ' ') {
+ myWhitespaceAdded = false;
+ }
+ return;
+ }
+ int offset = editor.getCaretModel().getOffset();
+ PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
+ PsiElement element = PsiTreeUtil.getParentOfType(PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset)), JsonProperty.class, false);
+ if (element == null) {
+ myWhitespaceAdded = false;
+ return;
+ }
+ final ASTNode[] children = element.getNode().getChildren(TokenSet.create(JsonElementTypes.COLON));
+ if (children.length == 0) {
+ myWhitespaceAdded = false;
+ return;
+ }
+ final ASTNode colon = children[0];
+ final ASTNode next = colon.getTreeNext();
+ final String text = next.getText();
+ if (text.length() == 0 || !StringUtil.isEmptyOrSpaces(text) || StringUtil.isLineBreak(text.charAt(0))) {
+ final int insOffset = colon.getStartOffset() + 1;
+ editor.getDocument().insertString(insOffset, " ");
+ editor.getCaretModel().moveToOffset(insOffset + 1);
+ myWhitespaceAdded = true;
+ }
+ else {
+ myWhitespaceAdded = false;
+ }
+ }
+
+ private static void addPropertyNameQuotesIfNeeded(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (c != ':' || !JsonDialectUtil.isStandardJson(file) || !JsonEditorOptions.getInstance().AUTO_QUOTE_PROP_NAME) return;
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset));
+ if (!(element instanceof JsonProperty)) return;
+ final JsonValue nameElement = ((JsonProperty)element).getNameElement();
+ if (nameElement instanceof JsonReferenceExpression) {
+ ((JsonProperty)element).setName(nameElement.getText());
+ PsiDocumentManager.getInstance(file.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument());
+ }
+ }
+
+ public static void processPairedBracesComma(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_MATCHING_BRACES) return;
+ if (c != '[' && c != '{' && c != '"' && c != '\'') return;
+ SmartEnterProcessor.commitDocument(editor);
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = file.findElementAt(offset);
+ if (element == null) return;
+ PsiElement parent = element.getParent();
+ if (c == '[' && parent instanceof JsonArray
+ || c == '{' && parent instanceof JsonObject
+ || (c == '"' || c == '\'') && parent instanceof JsonStringLiteral) {
+ if (shouldAddCommaInParentContainer((JsonValue)parent)) {
+ editor.getDocument().insertString(parent.getTextRange().getEndOffset(), ",");
+ }
+ }
+ }
+
+ private static boolean shouldAddCommaInParentContainer(@NotNull JsonValue item) {
+ PsiElement parent = item.getParent();
+ if (parent instanceof JsonArray || parent instanceof JsonProperty) {
+ PsiElement nextElement = PsiTreeUtil.skipWhitespacesForward(parent instanceof JsonProperty ? parent : item);
+ if (nextElement instanceof PsiErrorElement) {
+ PsiElement forward = PsiTreeUtil.skipWhitespacesForward(nextElement);
+ return parent instanceof JsonProperty ? forward instanceof JsonProperty : forward instanceof JsonValue;
+ }
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java
new file mode 100644
index 00000000..9bfc3103
--- /dev/null
+++ b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java
@@ -0,0 +1,110 @@
+package com.intellij.json.editor.folding;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.folding.FoldingBuilder;
+import com.intellij.lang.folding.FoldingDescriptor;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.util.Couple;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFoldingBuilder implements FoldingBuilder, DumbAware {
+ @NotNull
+ @Override
+ public FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
+ final List<FoldingDescriptor> descriptors = new ArrayList<>();
+ collectDescriptorsRecursively(node, document, descriptors);
+ return descriptors.toArray(FoldingDescriptor.EMPTY);
+ }
+
+ private static void collectDescriptorsRecursively(@NotNull ASTNode node,
+ @NotNull Document document,
+ @NotNull List<FoldingDescriptor> descriptors) {
+ final IElementType type = node.getElementType();
+ if ((type == JsonElementTypes.OBJECT || type == JsonElementTypes.ARRAY) && spanMultipleLines(node, document)) {
+ descriptors.add(new FoldingDescriptor(node, node.getTextRange()));
+ }
+ else if (type == JsonElementTypes.BLOCK_COMMENT) {
+ descriptors.add(new FoldingDescriptor(node, node.getTextRange()));
+ }
+ else if (type == JsonElementTypes.LINE_COMMENT) {
+ final Couple<PsiElement> commentRange = expandLineCommentsRange(node.getPsi());
+ final int startOffset = commentRange.getFirst().getTextRange().getStartOffset();
+ final int endOffset = commentRange.getSecond().getTextRange().getEndOffset();
+ if (document.getLineNumber(startOffset) != document.getLineNumber(endOffset)) {
+ descriptors.add(new FoldingDescriptor(node, new TextRange(startOffset, endOffset)));
+ }
+ }
+
+ for (ASTNode child : node.getChildren(null)) {
+ collectDescriptorsRecursively(child, document, descriptors);
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getPlaceholderText(@NotNull ASTNode node) {
+ final IElementType type = node.getElementType();
+ if (type == JsonElementTypes.OBJECT) {
+ final JsonObject object = node.getPsi(JsonObject.class);
+ final List<JsonProperty> properties = object.getPropertyList();
+ JsonProperty candidate = null;
+ for (JsonProperty property : properties) {
+ final String name = property.getName();
+ final JsonValue value = property.getValue();
+ if (value instanceof JsonLiteral) {
+ if ("id".equals(name) || "name".equals(name)) {
+ candidate = property;
+ break;
+ }
+ if (candidate == null) {
+ candidate = property;
+ }
+ }
+ }
+ if (candidate != null) {
+ return "{\"" + candidate.getName() + "\": " + candidate.getValue().getText() + "...}";
+ }
+ return "{...}";
+ }
+ else if (type == JsonElementTypes.ARRAY) {
+ return "[...]";
+ }
+ else if (type == JsonElementTypes.LINE_COMMENT) {
+ return "//...";
+ }
+ else if (type == JsonElementTypes.BLOCK_COMMENT) {
+ return "/*...*/";
+ }
+ return "...";
+ }
+
+ @Override
+ public boolean isCollapsedByDefault(@NotNull ASTNode node) {
+ return false;
+ }
+
+ @NotNull
+ public static Couple<PsiElement> expandLineCommentsRange(@NotNull PsiElement anchor) {
+ return Couple.of(JsonPsiUtil.findFurthestSiblingOfSameType(anchor, false), JsonPsiUtil.findFurthestSiblingOfSameType(anchor, true));
+ }
+
+ private static boolean spanMultipleLines(@NotNull ASTNode node, @NotNull Document document) {
+ final TextRange range = node.getTextRange();
+ int endOffset = range.getEndOffset();
+ return document.getLineNumber(range.getStartOffset())
+ < (endOffset < document.getTextLength() ? document.getLineNumber(endOffset) : document.getLineCount() - 1);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java
new file mode 100644
index 00000000..ff82d1c8
--- /dev/null
+++ b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java
@@ -0,0 +1,197 @@
+package com.intellij.json.editor.lineMover;
+
+import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
+import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonLineMover extends LineMover {
+ private enum Direction {
+ Same,
+ Inside,
+ Outside
+ }
+
+ private Direction myDirection = Direction.Same;
+
+ @Override
+ public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
+ myDirection = Direction.Same;
+
+ if (!(file instanceof JsonFile) || !super.checkAvailable(editor, file, info, down)) {
+ return false;
+ }
+
+ Pair<PsiElement, PsiElement> movedElementRange = getElementRange(editor, file, info.toMove);
+ if (!isValidElementRange(movedElementRange)) {
+ return false;
+ }
+
+ // Tweak range to move if it's necessary
+ movedElementRange = expandCommentsInRange(movedElementRange);
+
+ PsiElement movedSecond = movedElementRange.getSecond();
+ PsiElement movedFirst = movedElementRange.getFirst();
+
+ info.toMove = new LineRange(movedFirst, movedSecond);
+
+ // Adjust destination range to prevent illegal offsets
+ final int lineCount = editor.getDocument().getLineCount();
+ if (down) {
+ info.toMove2 = new LineRange(info.toMove.endLine, Math.min(info.toMove.endLine + 1, lineCount));
+ }
+ else {
+ info.toMove2 = new LineRange(Math.max(info.toMove.startLine - 1, 0), info.toMove.startLine);
+ }
+
+ if (movedFirst instanceof PsiComment && movedSecond instanceof PsiComment) {
+ return true;
+ }
+
+ // Check whether additional comma is needed
+ final Pair<PsiElement, PsiElement> destElementRange = getElementRange(editor, file, info.toMove2);
+
+ if (destElementRange != null) {
+ PsiElement destFirst = destElementRange.getFirst();
+ PsiElement destSecond = destElementRange.getSecond();
+
+ if (destFirst == destSecond && !(destFirst instanceof JsonProperty) && !(destFirst instanceof JsonValue)) {
+ PsiElement parent = destFirst.getParent();
+ if (((JsonFile)parent.getContainingFile()).getTopLevelValue() == parent) {
+ info.prohibitMove();
+ return true;
+ }
+ }
+
+ PsiElement firstParent = destFirst.getParent();
+ PsiElement secondParent = destSecond.getParent();
+
+ JsonValue firstParentParent = PsiTreeUtil.getParentOfType(firstParent, JsonObject.class, JsonArray.class);
+ if (firstParentParent == secondParent) {
+ myDirection = down ? Direction.Outside : Direction.Inside;
+ }
+ JsonValue secondParentParent = PsiTreeUtil.getParentOfType(secondParent, JsonObject.class, JsonArray.class);
+ if (firstParent == secondParentParent) {
+ myDirection = down ? Direction.Inside : Direction.Outside;
+ }
+ }
+ return true;
+ }
+
+ @NotNull
+ private static Pair<PsiElement, PsiElement> expandCommentsInRange(@NotNull Pair<PsiElement, PsiElement> range) {
+ final PsiElement upper = JsonPsiUtil.findFurthestSiblingOfSameType(range.getFirst(), false);
+ final PsiElement lower = JsonPsiUtil.findFurthestSiblingOfSameType(range.getSecond(), true);
+ return Pair.create(upper, lower);
+ }
+
+ @Override
+ public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
+
+ }
+
+ @Override
+ public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
+ int diff = (info.toMove.endLine - info.toMove.startLine) - (info.toMove2.endLine - info.toMove2.startLine);
+ switch (myDirection) {
+ case Same:
+ addCommaIfNeeded(editor.getDocument(), down ? info.toMove.endLine - 1 - diff : info.toMove2.endLine - 1 + diff);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ break;
+ case Inside:
+ if (!down) {
+ addCommaIfNeeded(editor.getDocument(), info.toMove2.startLine - 1);
+ }
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.startLine : info.toMove2.startLine);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ break;
+ case Outside:
+ addCommaIfNeeded(editor.getDocument(), down ? info.toMove.startLine : info.toMove2.startLine);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ if (down) {
+ trimCommaIfNeeded(editor.getDocument(), file, info.toMove.startLine - 1);
+ addCommaIfNeeded(editor.getDocument(), info.toMove.endLine);
+ trimCommaIfNeeded(editor.getDocument(), file, info.toMove.endLine);
+ }
+ break;
+ }
+ }
+
+ private static int getForwardLineNumber(Document document, PsiElement element) {
+ while (element instanceof PsiWhiteSpace || element instanceof PsiComment) {
+ element = element.getNextSibling();
+ }
+ if (element == null) return -1;
+
+ TextRange range = element.getTextRange();
+ return document.getLineNumber(range.getEndOffset());
+ }
+
+ private static int getBackwardLineNumber(Document document, PsiElement element) {
+ while (element instanceof PsiWhiteSpace || element instanceof PsiComment) {
+ element = element.getPrevSibling();
+ }
+ if (element == null) return -1;
+
+ TextRange range = element.getTextRange();
+ return document.getLineNumber(range.getEndOffset());
+ }
+
+ private static void trimCommaIfNeeded(Document document, PsiFile file, int line) {
+ int offset = document.getLineEndOffset(line);
+ if (doTrimComma(document, offset + 1, offset)) return;
+
+ PsiElement element = file.findElementAt(offset - 1);
+ int forward = getForwardLineNumber(document, element);
+ int backward = getBackwardLineNumber(document, element);
+ if (forward < 0 || backward < 0) return;
+ doTrimComma(document, document.getLineEndOffset(forward) - 1, document.getLineEndOffset(backward));
+ }
+
+ private static boolean doTrimComma(Document document, int forwardOffset, int backwardOffset) {
+ CharSequence charSequence = document.getCharsSequence();
+ if (backwardOffset <= 0) return true;
+ if (charSequence.charAt(backwardOffset - 1) == ',') {
+ int offsetAfter = skipWhitespaces(charSequence, forwardOffset);
+ if (offsetAfter >= charSequence.length()) return true;
+ char ch = charSequence.charAt(offsetAfter);
+
+ if (ch == ']' || ch == '}') {
+ document.deleteString(backwardOffset - 1, backwardOffset);
+ }
+ if (ch != '/') return true;
+ }
+ return false;
+ }
+
+ private static int skipWhitespaces(CharSequence charSequence, int offset2) {
+ while (offset2 < charSequence.length() && Character.isWhitespace(charSequence.charAt(offset2))) {
+ offset2++;
+ }
+ return offset2;
+ }
+
+ private static void addCommaIfNeeded(Document document, int line) {
+ int offset = document.getLineEndOffset(line);
+ if (offset > 0 && document.getCharsSequence().charAt(offset - 1) != ',') {
+ document.insertString(offset, ",");
+ }
+ }
+
+ private static boolean isValidElementRange(@Nullable Pair<PsiElement, PsiElement> elementRange) {
+ if (elementRange == null) {
+ return false;
+ }
+ return elementRange.getFirst().getParent() == elementRange.getSecond().getParent();
+ }
+}
diff --git a/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java
new file mode 100644
index 00000000..ada84f24
--- /dev/null
+++ b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java
@@ -0,0 +1,16 @@
+package com.intellij.json.editor.selection;
+
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.openapi.util.Condition;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBasicWordSelectionFilter implements Condition<PsiElement> {
+ @Override
+ public boolean value(PsiElement element) {
+ return !(JsonParserDefinition.STRING_LITERALS.contains(PsiUtilCore.getElementType(element)));
+ }
+}
diff --git a/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java
new file mode 100644
index 00000000..aa0fc8f3
--- /dev/null
+++ b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java
@@ -0,0 +1,43 @@
+package com.intellij.json.editor.selection;
+
+import com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase;
+import com.intellij.codeInsight.editorActions.SelectWordUtil;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.lexer.StringLiteralLexer;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.SINGLE_QUOTED_STRING;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStringLiteralSelectionHandler extends ExtendWordSelectionHandlerBase {
+ @Override
+ public boolean canSelect(@NotNull PsiElement e) {
+ if (!(e.getParent() instanceof JsonStringLiteral)) {
+ return false;
+ }
+ return !InjectedLanguageManager.getInstance(e.getProject()).isInjectedFragment(e.getContainingFile());
+ }
+
+ @Override
+ public List<TextRange> select(@NotNull PsiElement e, @NotNull CharSequence editorText, int cursorOffset, @NotNull Editor editor) {
+ final IElementType type = e.getNode().getElementType();
+ final StringLiteralLexer lexer = new StringLiteralLexer(type == SINGLE_QUOTED_STRING ? '\'' : '"', type, false, "/", false, false);
+ final List<TextRange> result = new ArrayList<>();
+ SelectWordUtil.addWordHonoringEscapeSequences(editorText, e.getTextRange(), cursorOffset, lexer, result);
+
+ final PsiElement parent = e.getParent();
+ result.add(ElementManipulators.getValueTextRange(parent).shiftRight(parent.getTextOffset()));
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java
new file mode 100644
index 00000000..ed695d74
--- /dev/null
+++ b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java
@@ -0,0 +1,123 @@
+package com.intellij.json.editor.smartEnter;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.SmartEnterProcessorWithFixers;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.COLON;
+import static com.intellij.json.JsonElementTypes.COMMA;
+
+/**
+ * This processor allows
+ * <ul>
+ * <li>Insert colon after key inside object property</li>
+ * <li>Insert comma after array element or object property</li>
+ * </ul>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonSmartEnterProcessor extends SmartEnterProcessorWithFixers {
+ public static final Logger LOG = Logger.getInstance(JsonSmartEnterProcessor.class);
+
+ private boolean myShouldAddNewline = false;
+
+ public JsonSmartEnterProcessor() {
+ addFixers(new JsonObjectPropertyFixer(), new JsonArrayElementFixer());
+ addEnterProcessors(new JsonEnterProcessor());
+ }
+
+ @Override
+ protected void collectAdditionalElements(@NotNull PsiElement element, @NotNull List<PsiElement> result) {
+ // include all parents as well
+ PsiElement parent = element.getParent();
+ while (parent != null && !(parent instanceof JsonFile)) {
+ result.add(parent);
+ parent = parent.getParent();
+ }
+ }
+
+ private static boolean terminatedOnCurrentLine(@NotNull Editor editor, @NotNull PsiElement element) {
+ final Document document = editor.getDocument();
+ final int caretOffset = editor.getCaretModel().getCurrentCaret().getOffset();
+ final int elementEndOffset = element.getTextRange().getEndOffset();
+ if (document.getLineNumber(elementEndOffset) != document.getLineNumber(caretOffset)) {
+ return false;
+ }
+ // Skip empty PsiError elements if comma is missing
+ PsiElement nextLeaf = PsiTreeUtil.nextLeaf(element, true);
+ return nextLeaf == null || (nextLeaf instanceof PsiWhiteSpace && nextLeaf.getText().contains("\n"));
+ }
+
+ private static boolean isFollowedByTerminal(@NotNull PsiElement element, IElementType type) {
+ final PsiElement nextLeaf = PsiTreeUtil.nextVisibleLeaf(element);
+ return nextLeaf != null && nextLeaf.getNode().getElementType() == type;
+ }
+
+ private static class JsonArrayElementFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> {
+ @Override
+ public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element)
+ throws IncorrectOperationException {
+ if (element instanceof JsonValue && element.getParent() instanceof JsonArray) {
+ final JsonValue arrayElement = (JsonValue)element;
+ if (terminatedOnCurrentLine(editor, arrayElement) && !isFollowedByTerminal(element, COMMA)) {
+ editor.getDocument().insertString(arrayElement.getTextRange().getEndOffset(), ",");
+ processor.myShouldAddNewline = true;
+ }
+ }
+ }
+ }
+
+ private class JsonObjectPropertyFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> {
+ @Override
+ public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element)
+ throws IncorrectOperationException {
+ if (element instanceof JsonProperty) {
+ final JsonValue propertyValue = ((JsonProperty)element).getValue();
+ if (propertyValue != null) {
+ if (terminatedOnCurrentLine(editor, propertyValue) && !isFollowedByTerminal(propertyValue, COMMA)) {
+ editor.getDocument().insertString(propertyValue.getTextRange().getEndOffset(), ",");
+ processor.myShouldAddNewline = true;
+ }
+ }
+ else {
+ final JsonValue propertyKey = ((JsonProperty)element).getNameElement();
+ final int keyEndOffset = propertyKey.getTextRange().getEndOffset();
+ //processor.myFirstErrorOffset = keyEndOffset;
+ if (terminatedOnCurrentLine(editor, propertyKey) && !isFollowedByTerminal(propertyKey, COLON)) {
+ processor.myFirstErrorOffset = keyEndOffset + 2;
+ editor.getDocument().insertString(keyEndOffset, ": ");
+ }
+ }
+ }
+ }
+ }
+
+ private class JsonEnterProcessor extends SmartEnterProcessorWithFixers.FixEnterProcessor {
+ @Override
+ public boolean doEnter(PsiElement atCaret, PsiFile file, @NotNull Editor editor, boolean modified) {
+ if (myShouldAddNewline) {
+ try {
+ plainEnter(editor);
+ }
+ finally {
+ myShouldAddNewline = false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java
new file mode 100644
index 00000000..fb7dfd3c
--- /dev/null
+++ b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java
@@ -0,0 +1,54 @@
+package com.intellij.json.findUsages;
+
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.HelpID;
+import com.intellij.lang.cacheBuilder.WordsScanner;
+import com.intellij.lang.findUsages.FindUsagesProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFindUsagesProvider implements FindUsagesProvider {
+ @Nullable
+ @Override
+ public WordsScanner getWordsScanner() {
+ return new JsonWordScanner();
+ }
+
+ @Override
+ public boolean canFindUsagesFor(@NotNull PsiElement psiElement) {
+ return psiElement instanceof PsiNamedElement;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpId(@NotNull PsiElement psiElement) {
+ return HelpID.FIND_OTHER_USAGES;
+ }
+
+ @NotNull
+ @Override
+ public String getType(@NotNull PsiElement element) {
+ if (element instanceof JsonProperty) {
+ return "property";
+ }
+ return "";
+ }
+
+ @NotNull
+ @Override
+ public String getDescriptiveName(@NotNull PsiElement element) {
+ final String name = element instanceof PsiNamedElement ? ((PsiNamedElement)element).getName() : null;
+ return name != null ? name : "<unnamed>";
+ }
+
+ @NotNull
+ @Override
+ public String getNodeText(@NotNull PsiElement element, boolean useFullName) {
+ return getDescriptiveName(element);
+ }
+}
diff --git a/json/src/com/intellij/json/findUsages/JsonWordScanner.java b/json/src/com/intellij/json/findUsages/JsonWordScanner.java
new file mode 100644
index 00000000..df570922
--- /dev/null
+++ b/json/src/com/intellij/json/findUsages/JsonWordScanner.java
@@ -0,0 +1,19 @@
+package com.intellij.json.findUsages;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLexer;
+import com.intellij.lang.cacheBuilder.DefaultWordsScanner;
+import com.intellij.psi.tree.TokenSet;
+
+import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES;
+import static com.intellij.json.JsonParserDefinition.JSON_LITERALS;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonWordScanner extends DefaultWordsScanner {
+ public JsonWordScanner() {
+ super(new JsonLexer(), TokenSet.create(JsonElementTypes.IDENTIFIER), JSON_COMMENTARIES, JSON_LITERALS);
+ setMayHaveFileRefsInLiterals(true);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonBlock.java b/json/src/com/intellij/json/formatter/JsonBlock.java
new file mode 100644
index 00000000..992e0001
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonBlock.java
@@ -0,0 +1,221 @@
+package com.intellij.json.formatter;
+
+import com.intellij.formatting.*;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.*;
+import static com.intellij.json.JsonParserDefinition.JSON_CONTAINERS;
+import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_COLON;
+import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_VALUE;
+import static com.intellij.json.psi.JsonPsiUtil.hasElementType;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBlock implements ASTBlock {
+ private static final TokenSet JSON_OPEN_BRACES = TokenSet.create(L_BRACKET, L_CURLY);
+ private static final TokenSet JSON_CLOSE_BRACES = TokenSet.create(R_BRACKET, R_CURLY);
+ private static final TokenSet JSON_ALL_BRACES = TokenSet.orSet(JSON_OPEN_BRACES, JSON_CLOSE_BRACES);
+
+ private final JsonBlock myParent;
+
+ private final ASTNode myNode;
+ private final PsiElement myPsiElement;
+ private final Alignment myAlignment;
+ private final Indent myIndent;
+ private final Wrap myWrap;
+ private final JsonCodeStyleSettings myCustomSettings;
+ private final SpacingBuilder mySpacingBuilder;
+ // lazy initialized on first call to #getSubBlocks()
+ private List<Block> mySubBlocks = null;
+
+ private final Alignment myPropertyValueAlignment;
+ private final Wrap myChildWrap;
+
+ /**
+ * @deprecated Please use overload with settings JsonCodeStyleSettings and spacingBuilder.
+ * Getting settings should be done only for the root block.
+ */
+ @Deprecated
+ @SuppressWarnings("unused") //used externally
+ public JsonBlock(@Nullable JsonBlock parent,
+ @NotNull ASTNode node,
+ @NotNull CodeStyleSettings settings,
+ @Nullable Alignment alignment,
+ @NotNull Indent indent,
+ @Nullable Wrap wrap) {
+ this(parent, node, settings.getCustomSettings(JsonCodeStyleSettings.class), alignment, indent, wrap,
+ JsonFormattingBuilderModel.createSpacingBuilder(settings));
+ }
+
+ public JsonBlock(@Nullable JsonBlock parent,
+ @NotNull ASTNode node,
+ @NotNull JsonCodeStyleSettings customSettings,
+ @Nullable Alignment alignment,
+ @NotNull Indent indent,
+ @Nullable Wrap wrap,
+ @NotNull SpacingBuilder spacingBuilder) {
+ myParent = parent;
+ myNode = node;
+ myPsiElement = node.getPsi();
+ myAlignment = alignment;
+ myIndent = indent;
+ myWrap = wrap;
+ mySpacingBuilder = spacingBuilder;
+ myCustomSettings = customSettings;
+
+ if (myPsiElement instanceof JsonObject) {
+ myChildWrap = Wrap.createWrap(myCustomSettings.OBJECT_WRAPPING, true);
+ }
+ else if (myPsiElement instanceof JsonArray) {
+ myChildWrap = Wrap.createWrap(myCustomSettings.ARRAY_WRAPPING, true);
+ }
+ else {
+ myChildWrap = null;
+ }
+
+ myPropertyValueAlignment = myPsiElement instanceof JsonObject ? Alignment.createAlignment(true) : null;
+ }
+
+ @Override
+ public ASTNode getNode() {
+ return myNode;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getTextRange() {
+ return myNode.getTextRange();
+ }
+
+ @NotNull
+ @Override
+ public List<Block> getSubBlocks() {
+ if (mySubBlocks == null) {
+ int propertyAlignment = myCustomSettings.PROPERTY_ALIGNMENT;
+ ASTNode[] children = myNode.getChildren(null);
+ mySubBlocks = ContainerUtil.newArrayListWithCapacity(children.length);
+ for (ASTNode child: children) {
+ if (isWhitespaceOrEmpty(child)) continue;
+ mySubBlocks.add(makeSubBlock(child, propertyAlignment));
+ }
+ }
+ return mySubBlocks;
+ }
+
+ private Block makeSubBlock(@NotNull ASTNode childNode, int propertyAlignment) {
+ Indent indent = Indent.getNoneIndent();
+ Alignment alignment = null;
+ Wrap wrap = null;
+
+ if (hasElementType(myNode, JSON_CONTAINERS)) {
+ if (hasElementType(childNode, COMMA)) {
+ wrap = Wrap.createWrap(WrapType.NONE, true);
+ }
+ else if (!hasElementType(childNode, JSON_ALL_BRACES)) {
+ assert myChildWrap != null;
+ wrap = myChildWrap;
+ indent = Indent.getNormalIndent();
+ }
+ else if (hasElementType(childNode, JSON_OPEN_BRACES)) {
+ if (JsonPsiUtil.isPropertyValue(myPsiElement) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) {
+ // WEB-13587 Align compound values on opening brace/bracket, not the whole block
+ assert myParent != null && myParent.myParent != null && myParent.myParent.myPropertyValueAlignment != null;
+ alignment = myParent.myParent.myPropertyValueAlignment;
+ }
+ }
+ }
+ // Handle properties alignment
+ else if (hasElementType(myNode, PROPERTY) ) {
+ assert myParent != null && myParent.myPropertyValueAlignment != null;
+ if (hasElementType(childNode, COLON) && propertyAlignment == ALIGN_PROPERTY_ON_COLON) {
+ alignment = myParent.myPropertyValueAlignment;
+ }
+ else if (JsonPsiUtil.isPropertyValue(childNode.getPsi()) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) {
+ if (!hasElementType(childNode, JSON_CONTAINERS)) {
+ alignment = myParent.myPropertyValueAlignment;
+ }
+ }
+ }
+ return new JsonBlock(this, childNode, myCustomSettings, alignment, indent, wrap, mySpacingBuilder);
+ }
+
+ @Nullable
+ @Override
+ public Wrap getWrap() {
+ return myWrap;
+ }
+
+ @Nullable
+ @Override
+ public Indent getIndent() {
+ return myIndent;
+ }
+
+ @Nullable
+ @Override
+ public Alignment getAlignment() {
+ return myAlignment;
+ }
+
+ @Nullable
+ @Override
+ public Spacing getSpacing(@Nullable Block child1, @NotNull Block child2) {
+ return mySpacingBuilder.getSpacing(this, child1, child2);
+ }
+
+ @NotNull
+ @Override
+ public ChildAttributes getChildAttributes(int newChildIndex) {
+ if (hasElementType(myNode, JSON_CONTAINERS)) {
+ // WEB-13675: For some reason including alignment in child attributes causes
+ // indents to consist solely of spaces when both USE_TABS and SMART_TAB
+ // options are enabled.
+ return new ChildAttributes(Indent.getNormalIndent(), null);
+ }
+ else if (myNode.getPsi() instanceof PsiFile) {
+ return new ChildAttributes(Indent.getNoneIndent(), null);
+ }
+ // Will use continuation indent for cases like { "foo"<caret> }
+ return new ChildAttributes(null, null);
+ }
+
+ @Override
+ public boolean isIncomplete() {
+ final ASTNode lastChildNode = myNode.getLastChildNode();
+ if (hasElementType(myNode, OBJECT)) {
+ return lastChildNode != null && lastChildNode.getElementType() != R_CURLY;
+ }
+ else if (hasElementType(myNode, ARRAY)) {
+ return lastChildNode != null && lastChildNode.getElementType() != R_BRACKET;
+ }
+ else if (hasElementType(myNode, PROPERTY)) {
+ return ((JsonProperty)myPsiElement).getValue() == null;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return myNode.getFirstChildNode() == null;
+ }
+
+ private static boolean isWhitespaceOrEmpty(ASTNode node) {
+ return node.getElementType() == TokenType.WHITE_SPACE || node.getTextLength() == 0;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java
new file mode 100644
index 00000000..fa19c5e6
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java
@@ -0,0 +1,78 @@
+package com.intellij.json.formatter;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
+import org.intellij.lang.annotations.MagicConstant;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCodeStyleSettings extends CustomCodeStyleSettings {
+
+ public static int DO_NOT_ALIGN_PROPERTY = PropertyAlignment.DO_NOT_ALIGN.getId();
+ public static int ALIGN_PROPERTY_ON_VALUE = PropertyAlignment.ALIGN_ON_VALUE.getId();
+ public static int ALIGN_PROPERTY_ON_COLON = PropertyAlignment.ALIGN_ON_COLON.getId();
+
+ public boolean SPACE_AFTER_COLON = true;
+ public boolean SPACE_BEFORE_COLON = false;
+ public boolean KEEP_TRAILING_COMMA = false;
+
+ // TODO: check whether it's possible to migrate CustomCodeStyleSettings to newer com.intellij.util.xmlb.XmlSerializer
+ /**
+ * Contains value of {@link com.intellij.json.formatter.JsonCodeStyleSettings.PropertyAlignment#getId()}
+ *
+ * @see #DO_NOT_ALIGN_PROPERTY
+ * @see #ALIGN_PROPERTY_ON_VALUE
+ * @see #ALIGN_PROPERTY_ON_COLON
+ */
+ public int PROPERTY_ALIGNMENT = PropertyAlignment.DO_NOT_ALIGN.getId();
+
+ @MagicConstant(flags = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
+ public int OBJECT_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS;
+
+ // This was default policy for array elements wrapping in JavaScript's JSON.
+ // CHOP_DOWN_IF_LONG seems more appropriate however for short arrays.
+ @MagicConstant(flags = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
+ public int ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS;
+
+ public JsonCodeStyleSettings(CodeStyleSettings container) {
+ super(JsonLanguage.INSTANCE.getID(), container);
+ }
+
+ public enum PropertyAlignment {
+ DO_NOT_ALIGN(JsonBundle.message("formatter.align.properties.none"), 0),
+ ALIGN_ON_VALUE(JsonBundle.message("formatter.align.properties.on.value"), 1),
+ ALIGN_ON_COLON(JsonBundle.message("formatter.align.properties.on.colon"), 2);
+
+ private final String myDescription;
+ private final int myId;
+
+ PropertyAlignment(@NotNull String description, int id) {
+ myDescription = description;
+ myId = id;
+ }
+
+ @NotNull
+ public String getDescription() {
+ return myDescription;
+ }
+
+ public int getId() {
+ return myId;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java
new file mode 100644
index 00000000..6a54a233
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java
@@ -0,0 +1,57 @@
+package com.intellij.json.formatter;
+
+import com.intellij.application.options.CodeStyleAbstractConfigurable;
+import com.intellij.application.options.CodeStyleAbstractPanel;
+import com.intellij.application.options.TabbedLanguageCodeStylePanel;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsProvider;
+import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCodeStyleSettingsProvider extends CodeStyleSettingsProvider {
+ @NotNull
+ @Override
+ public Configurable createSettingsPage(CodeStyleSettings settings, CodeStyleSettings originalSettings) {
+ return new CodeStyleAbstractConfigurable(settings, originalSettings, "JSON") {
+ @Override
+ protected CodeStyleAbstractPanel createPanel(CodeStyleSettings settings) {
+ final Language language = JsonLanguage.INSTANCE;
+ final CodeStyleSettings currentSettings = getCurrentSettings();
+ return new TabbedLanguageCodeStylePanel(language, currentSettings, settings) {
+ @Override
+ protected void initTabs(CodeStyleSettings settings) {
+ addIndentOptionsTab(settings);
+ addSpacesTab(settings);
+ addBlankLinesTab(settings);
+ addWrappingAndBracesTab(settings);
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return "reference.settingsdialog.codestyle.json";
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public String getConfigurableDisplayName() {
+ return JsonLanguage.INSTANCE.getDisplayName();
+ }
+
+ @Nullable
+ @Override
+ public CustomCodeStyleSettings createCustomSettings(CodeStyleSettings settings) {
+ return new JsonCodeStyleSettings(settings);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java
new file mode 100644
index 00000000..ed8d7503
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java
@@ -0,0 +1,42 @@
+package com.intellij.json.formatter;
+
+import com.intellij.formatting.*;
+import com.intellij.json.JsonLanguage;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.json.JsonElementTypes.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFormattingBuilderModel implements FormattingModelBuilder {
+ @NotNull
+ @Override
+ public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) {
+ JsonCodeStyleSettings customSettings = settings.getCustomSettings(JsonCodeStyleSettings.class);
+ SpacingBuilder spacingBuilder = createSpacingBuilder(settings);
+ final JsonBlock block = new JsonBlock(null, element.getNode(), customSettings, null, Indent.getSmartIndent(Indent.Type.CONTINUATION), null, spacingBuilder);
+ return FormattingModelProvider.createFormattingModelForPsiFile(element.getContainingFile(), block, settings);
+ }
+
+ @NotNull
+ static SpacingBuilder createSpacingBuilder(CodeStyleSettings settings) {
+ final JsonCodeStyleSettings jsonSettings = settings.getCustomSettings(JsonCodeStyleSettings.class);
+ final CommonCodeStyleSettings commonSettings = settings.getCommonSettings(JsonLanguage.INSTANCE);
+
+ final int spacesBeforeComma = commonSettings.SPACE_BEFORE_COMMA ? 1 : 0;
+ final int spacesBeforeColon = jsonSettings.SPACE_BEFORE_COLON ? 1 : 0;
+ final int spacesAfterColon = jsonSettings.SPACE_AFTER_COLON ? 1 : 0;
+
+ return new SpacingBuilder(settings, JsonLanguage.INSTANCE)
+ .before(COLON).spacing(spacesBeforeColon, spacesBeforeColon, 0, false, 0)
+ .after(COLON).spacing(spacesAfterColon, spacesAfterColon, 0, false, 0)
+ .withinPair(L_BRACKET, R_BRACKET).spaceIf(commonSettings.SPACE_WITHIN_BRACKETS, true)
+ .withinPair(L_CURLY, R_CURLY).spaceIf(commonSettings.SPACE_WITHIN_BRACES, true)
+ .before(COMMA).spacing(spacesBeforeComma, spacesBeforeComma, 0, false, 0)
+ .after(COMMA).spaceIf(commonSettings.SPACE_AFTER_COMMA);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java
new file mode 100644
index 00000000..eb727926
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java
@@ -0,0 +1,116 @@
+package com.intellij.json.formatter;
+
+import com.intellij.application.options.IndentOptionsEditor;
+import com.intellij.application.options.SmartIndentOptionsEditor;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import com.intellij.util.ArrayUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import static com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.SPACES_OTHER;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
+ private static final String[] ALIGN_OPTIONS = Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values())
+ .map(alignment -> alignment.getDescription())
+ .toArray(value -> new String[value]);
+
+ private static final int[] ALIGN_VALUES =
+ ArrayUtil.toIntArray(Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values())
+ .map(alignment -> alignment.getId())
+ .collect(Collectors.toList()));
+
+ private static final String SAMPLE = "{\n" +
+ " \"json literals are\": {\n" +
+ " \"strings\": [\"foo\", \"bar\", \"\\u0062\\u0061\\u0072\"],\n" +
+ " \"numbers\": [42, 6.62606975e-34],\n" +
+ " \"boolean values\": [true, false,],\n" +
+ " \"objects\": {\"null\": null,\"another\": null,}\n" +
+ " }\n" +
+ "}";
+
+
+ @Override
+ public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, @NotNull SettingsType settingsType) {
+ if (settingsType == SettingsType.SPACING_SETTINGS) {
+ consumer.showStandardOptions("SPACE_WITHIN_BRACKETS",
+ "SPACE_WITHIN_BRACES",
+ "SPACE_AFTER_COMMA",
+ "SPACE_BEFORE_COMMA");
+ consumer.renameStandardOption("SPACE_WITHIN_BRACES", "Braces");
+ consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_BEFORE_COLON", "Before ':'", SPACES_OTHER);
+ consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_AFTER_COLON", "After ':'", SPACES_OTHER);
+ }
+ else if (settingsType == SettingsType.BLANK_LINES_SETTINGS) {
+ consumer.showStandardOptions("KEEP_BLANK_LINES_IN_CODE");
+ }
+ else if (settingsType == SettingsType.WRAPPING_AND_BRACES_SETTINGS) {
+ consumer.showStandardOptions("RIGHT_MARGIN",
+ "WRAP_ON_TYPING",
+ "KEEP_LINE_BREAKS",
+ "WRAP_LONG_LINES");
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "KEEP_TRAILING_COMMA",
+ "Trailing comma",
+ CodeStyleSettingsCustomizable.WRAPPING_KEEP);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "ARRAY_WRAPPING",
+ "Arrays",
+ null,
+ CodeStyleSettingsCustomizable.WRAP_OPTIONS,
+ CodeStyleSettingsCustomizable.WRAP_VALUES);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "OBJECT_WRAPPING",
+ "Objects",
+ null,
+ CodeStyleSettingsCustomizable.WRAP_OPTIONS,
+ CodeStyleSettingsCustomizable.WRAP_VALUES);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "PROPERTY_ALIGNMENT",
+ JsonBundle.message("formatter.align.properties.caption"),
+ "Objects",
+ ALIGN_OPTIONS,
+ ALIGN_VALUES);
+
+ }
+ }
+
+ @NotNull
+ @Override
+ public Language getLanguage() {
+ return JsonLanguage.INSTANCE;
+ }
+
+ @Nullable
+ @Override
+ public IndentOptionsEditor getIndentOptionsEditor() {
+ return new SmartIndentOptionsEditor();
+ }
+
+ @Override
+ public String getCodeSample(@NotNull SettingsType settingsType) {
+ return SAMPLE;
+ }
+
+ @Override
+ protected void customizeDefaults(@NotNull CommonCodeStyleSettings commonSettings,
+ @NotNull CommonCodeStyleSettings.IndentOptions indentOptions) {
+ indentOptions.INDENT_SIZE = 2;
+ // strip all blank lines by default
+ commonSettings.KEEP_BLANK_LINES_IN_CODE = 0;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java
new file mode 100644
index 00000000..3116dc52
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java
@@ -0,0 +1,70 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.formatter;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.openapi.editor.DefaultLineWrapPositionStrategy;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtilCore;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonLineWrapPositionStrategy extends DefaultLineWrapPositionStrategy {
+ @Override
+ public int calculateWrapPosition(@NotNull Document document,
+ @Nullable Project project,
+ int startOffset,
+ int endOffset,
+ int maxPreferredOffset,
+ boolean allowToBeyondMaxPreferredOffset,
+ boolean isSoftWrap) {
+ if (isSoftWrap) {
+ return super.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset,
+ true);
+ }
+ if (project == null) return -1;
+ final int wrapPosition = getMinWrapPosition(document, project, maxPreferredOffset);
+ if (wrapPosition == SKIP_WRAPPING) return -1;
+ int minWrapPosition = Math.max(startOffset, wrapPosition);
+ return super
+ .calculateWrapPosition(document, project, minWrapPosition, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, isSoftWrap);
+ }
+
+ private static final int SKIP_WRAPPING = -2;
+ private static int getMinWrapPosition(@NotNull Document document, @NotNull Project project, int offset) {
+ PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+ if (manager.isUncommited(document)) manager.commitDocument(document);
+ PsiFile psiFile = manager.getPsiFile(document);
+ if (psiFile != null) {
+ PsiElement currElement = psiFile.findElementAt(offset);
+ final IElementType elementType = PsiUtilCore.getElementType(currElement);
+ if (elementType == JsonElementTypes.DOUBLE_QUOTED_STRING
+ || elementType == JsonElementTypes.SINGLE_QUOTED_STRING
+ || elementType == JsonElementTypes.LITERAL
+ || elementType == JsonElementTypes.BOOLEAN_LITERAL
+ || elementType == JsonElementTypes.TRUE
+ || elementType == JsonElementTypes.FALSE
+ || elementType == JsonElementTypes.IDENTIFIER
+ || elementType == JsonElementTypes.NULL_LITERAL
+ || elementType == JsonElementTypes.NUMBER_LITERAL) {
+ return currElement.getTextRange().getEndOffset();
+ }
+ if (elementType == JsonElementTypes.COLON) {
+ return SKIP_WRAPPING;
+ }
+ if (currElement != null) {
+ if (currElement instanceof PsiComment ||
+ PsiUtilCore.getElementType(PsiTreeUtil.skipWhitespacesForward(currElement)) == JsonElementTypes.COMMA) {
+ return SKIP_WRAPPING;
+ }
+ }
+ }
+ return -1;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java
new file mode 100644
index 00000000..b3423fd9
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.formatter;
+
+import com.intellij.application.options.CodeStyle;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.impl.JsonRecursiveElementVisitor;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor;
+import com.intellij.util.DocumentUtil;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonTrailingCommaRemover implements PreFormatProcessor {
+
+ @NotNull
+ @Override
+ public TextRange process(@NotNull ASTNode element, @NotNull TextRange range) {
+ PsiElement rootPsi = element.getPsi();
+ if (rootPsi.getLanguage() != JsonLanguage.INSTANCE) {
+ return range;
+ }
+ JsonCodeStyleSettings settings = CodeStyle.getCustomSettings(rootPsi.getContainingFile(), JsonCodeStyleSettings.class);
+ if (settings.KEEP_TRAILING_COMMA) {
+ return range;
+ }
+ PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(rootPsi.getProject());
+ Document document = psiDocumentManager.getDocument(rootPsi.getContainingFile());
+ if (document == null) {
+ return range;
+ }
+ DocumentUtil.executeInBulk(document, true, () -> {
+ psiDocumentManager.doPostponedOperationsAndUnblockDocument(document);
+ PsiElementVisitor visitor = new Visitor(document);
+ rootPsi.accept(visitor);
+ psiDocumentManager.commitDocument(document);
+ });
+ return range;
+ }
+
+ private static class Visitor extends JsonRecursiveElementVisitor {
+ private final Document myDocument;
+ private int myOffsetDelta;
+
+ Visitor(Document document) {
+ myDocument = document;
+ }
+
+ @Override
+ public void visitArray(@NotNull JsonArray o) {
+ super.visitArray(o);
+ PsiElement lastChild = o.getLastChild();
+ if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_BRACKET) {
+ return;
+ }
+ deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getValueList()), o.getFirstChild()));
+ }
+
+ @Override
+ public void visitObject(@NotNull JsonObject o) {
+ super.visitObject(o);
+ PsiElement lastChild = o.getLastChild();
+ if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_CURLY) {
+ return;
+ }
+ deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getPropertyList()), o.getFirstChild()));
+ }
+
+ private void deleteTrailingCommas(@Nullable PsiElement lastElementOrOpeningBrace) {
+ PsiElement element = lastElementOrOpeningBrace != null ? lastElementOrOpeningBrace.getNextSibling() : null;
+
+ while (element != null) {
+ if (element.getNode().getElementType() == JsonElementTypes.COMMA ||
+ element instanceof PsiErrorElement && ",".equals(element.getText())) {
+ deleteNode(element.getNode());
+ }
+ else if (!(element instanceof PsiComment || element instanceof PsiWhiteSpace)) {
+ break;
+ }
+ element = element.getNextSibling();
+ }
+ }
+
+ private void deleteNode(@NotNull ASTNode node) {
+ int length = node.getTextLength();
+ myDocument.deleteString(node.getStartOffset() + myOffsetDelta, node.getStartOffset() + length + myOffsetDelta);
+ myOffsetDelta -= length;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/highlighting/JsonColorsPage.java b/json/src/com/intellij/json/highlighting/JsonColorsPage.java
new file mode 100644
index 00000000..69075e6c
--- /dev/null
+++ b/json/src/com/intellij/json/highlighting/JsonColorsPage.java
@@ -0,0 +1,105 @@
+package com.intellij.json.highlighting;
+
+import com.google.common.collect.ImmutableMap;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.options.colors.AttributesDescriptor;
+import com.intellij.openapi.options.colors.ColorDescriptor;
+import com.intellij.openapi.options.colors.ColorSettingsPage;
+import com.intellij.psi.codeStyle.DisplayPriority;
+import com.intellij.psi.codeStyle.DisplayPrioritySortable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Map;
+
+import static com.intellij.json.highlighting.JsonSyntaxHighlighterFactory.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonColorsPage implements ColorSettingsPage, DisplayPrioritySortable {
+ private static final Map<String, TextAttributesKey> ourAdditionalHighlighting = ImmutableMap.of("propertyKey", JSON_PROPERTY_KEY);
+
+ private static final AttributesDescriptor[] ourAttributeDescriptors = new AttributesDescriptor[]{
+ new AttributesDescriptor("Property key", JSON_PROPERTY_KEY),
+
+ new AttributesDescriptor("Braces", JSON_BRACES),
+ new AttributesDescriptor("Brackets", JSON_BRACKETS),
+ new AttributesDescriptor("Comma", JSON_COMMA),
+ new AttributesDescriptor("Colon", JSON_COLON),
+ new AttributesDescriptor("Number", JSON_NUMBER),
+ new AttributesDescriptor("String", JSON_STRING),
+ new AttributesDescriptor("Keyword", JSON_KEYWORD),
+ new AttributesDescriptor("Line comment", JSON_LINE_COMMENT),
+ new AttributesDescriptor("Block comment", JSON_BLOCK_COMMENT),
+ //new AttributesDescriptor("", JSON_IDENTIFIER),
+ new AttributesDescriptor("Valid escape sequence", JSON_VALID_ESCAPE),
+ new AttributesDescriptor("Invalid escape sequence", JSON_INVALID_ESCAPE),
+ };
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ return AllIcons.FileTypes.Json;
+ }
+
+ @NotNull
+ @Override
+ public SyntaxHighlighter getHighlighter() {
+ return SyntaxHighlighterFactory.getSyntaxHighlighter(JsonLanguage.INSTANCE, null, null);
+ }
+
+ @NotNull
+ @Override
+ public String getDemoText() {
+ return "{\n" +
+ " // Line comments are not included in standard but nonetheless allowed.\n" +
+ " /* As well as block comments. */\n" +
+ " <propertyKey>\"the only keywords are\"</propertyKey>: [true, false, null],\n" +
+ " <propertyKey>\"strings with\"</propertyKey>: {\n" +
+ " <propertyKey>\"no escapes\"</propertyKey>: \"pseudopolinomiality\"\n" +
+ " <propertyKey>\"valid escapes\"</propertyKey>: \"C-style\\r\\n and unicode\\u0021\",\n" +
+ " <propertyKey>\"illegal escapes\"</propertyKey>: \"\\0377\\x\\\"\n" +
+ " },\n" +
+ " <propertyKey>\"some numbers\"</propertyKey>: [\n" +
+ " 42,\n" +
+ " -0.0e-0,\n" +
+ " 6.626e-34\n" +
+ " ] \n" +
+ "}";
+ }
+
+ @Nullable
+ @Override
+ public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
+ return ourAdditionalHighlighting;
+ }
+
+ @NotNull
+ @Override
+ public AttributesDescriptor[] getAttributeDescriptors() {
+ return ourAttributeDescriptors;
+ }
+
+ @NotNull
+ @Override
+ public ColorDescriptor[] getColorDescriptors() {
+ return ColorDescriptor.EMPTY_ARRAY;
+ }
+
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return "JSON";
+ }
+
+ @Override
+ public DisplayPriority getPriority() {
+ return DisplayPriority.LANGUAGE_SETTINGS;
+ }
+}
diff --git a/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java
new file mode 100644
index 00000000..fd80e8cb
--- /dev/null
+++ b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java
@@ -0,0 +1,164 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.highlighting;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonLexer;
+import com.intellij.lang.Language;
+import com.intellij.lexer.LayeredLexer;
+import com.intellij.lexer.Lexer;
+import com.intellij.lexer.StringLiteralLexer;
+import com.intellij.openapi.editor.HighlighterColors;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.StringEscapesTokenTypes;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*;
+
+public class JsonSyntaxHighlighterFactory extends SyntaxHighlighterFactory {
+
+ private static final String PERMISSIVE_ESCAPES;
+ static {
+ final StringBuilder escapesBuilder = new StringBuilder("/");
+ for (char c = '\1'; c < '\255'; c++) {
+ if (c != 'x' && c != 'u' && !Character.isDigit(c) && c != '\n' && c != '\r') {
+ escapesBuilder.append(c);
+ }
+ }
+ PERMISSIVE_ESCAPES = escapesBuilder.toString();
+ }
+
+ public static final TextAttributesKey JSON_BRACKETS = TextAttributesKey.createTextAttributesKey("JSON.BRACKETS", BRACKETS);
+ public static final TextAttributesKey JSON_BRACES = TextAttributesKey.createTextAttributesKey("JSON.BRACES", BRACES);
+ public static final TextAttributesKey JSON_COMMA = TextAttributesKey.createTextAttributesKey("JSON.COMMA", COMMA);
+ public static final TextAttributesKey JSON_COLON = TextAttributesKey.createTextAttributesKey("JSON.COLON", SEMICOLON);
+ public static final TextAttributesKey JSON_NUMBER = TextAttributesKey.createTextAttributesKey("JSON.NUMBER", NUMBER);
+ public static final TextAttributesKey JSON_STRING = TextAttributesKey.createTextAttributesKey("JSON.STRING", STRING);
+ public static final TextAttributesKey JSON_KEYWORD = TextAttributesKey.createTextAttributesKey("JSON.KEYWORD", KEYWORD);
+ public static final TextAttributesKey JSON_LINE_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.LINE_COMMENT", LINE_COMMENT);
+ public static final TextAttributesKey JSON_BLOCK_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.BLOCK_COMMENT", BLOCK_COMMENT);
+
+ // Artificial element type
+ public static final TextAttributesKey JSON_IDENTIFIER = TextAttributesKey.createTextAttributesKey("JSON.IDENTIFIER", IDENTIFIER);
+
+ // Added by annotators
+ public static final TextAttributesKey JSON_PROPERTY_KEY = TextAttributesKey.createTextAttributesKey("JSON.PROPERTY_KEY", INSTANCE_FIELD);
+
+ // String escapes
+ public static final TextAttributesKey JSON_VALID_ESCAPE =
+ TextAttributesKey.createTextAttributesKey("JSON.VALID_ESCAPE", VALID_STRING_ESCAPE);
+ public static final TextAttributesKey JSON_INVALID_ESCAPE =
+ TextAttributesKey.createTextAttributesKey("JSON.INVALID_ESCAPE", INVALID_STRING_ESCAPE);
+
+
+ @NotNull
+ @Override
+ public SyntaxHighlighter getSyntaxHighlighter(@Nullable Project project, @Nullable VirtualFile virtualFile) {
+ return new MyHighlighter(virtualFile);
+ }
+
+ private class MyHighlighter extends SyntaxHighlighterBase {
+ private final Map<IElementType, TextAttributesKey> ourAttributes = new HashMap<>();
+
+ @Nullable
+ private final VirtualFile myFile;
+
+ {
+ fillMap(ourAttributes, JSON_BRACES, JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY);
+ fillMap(ourAttributes, JSON_BRACKETS, JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET);
+ fillMap(ourAttributes, JSON_COMMA, JsonElementTypes.COMMA);
+ fillMap(ourAttributes, JSON_COLON, JsonElementTypes.COLON);
+ fillMap(ourAttributes, JSON_STRING, JsonElementTypes.DOUBLE_QUOTED_STRING);
+ fillMap(ourAttributes, JSON_STRING, JsonElementTypes.SINGLE_QUOTED_STRING);
+ fillMap(ourAttributes, JSON_NUMBER, JsonElementTypes.NUMBER);
+ fillMap(ourAttributes, JSON_KEYWORD, JsonElementTypes.TRUE, JsonElementTypes.FALSE, JsonElementTypes.NULL);
+ fillMap(ourAttributes, JSON_LINE_COMMENT, JsonElementTypes.LINE_COMMENT);
+ fillMap(ourAttributes, JSON_BLOCK_COMMENT, JsonElementTypes.BLOCK_COMMENT);
+ // TODO may be it's worth to add more sensible highlighting for identifiers
+ fillMap(ourAttributes, JSON_IDENTIFIER, JsonElementTypes.IDENTIFIER);
+ fillMap(ourAttributes, HighlighterColors.BAD_CHARACTER, TokenType.BAD_CHARACTER);
+
+ fillMap(ourAttributes, JSON_VALID_ESCAPE, StringEscapesTokenTypes.VALID_STRING_ESCAPE_TOKEN);
+ fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_CHARACTER_ESCAPE_TOKEN);
+ fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_UNICODE_ESCAPE_TOKEN);
+ }
+
+ MyHighlighter(@Nullable VirtualFile file) {
+ myFile = file;
+ }
+
+ @NotNull
+ @Override
+ public Lexer getHighlightingLexer() {
+ LayeredLexer layeredLexer = new LayeredLexer(getLexer());
+ boolean isPermissiveDialect = isPermissiveDialect();
+ layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\"', JsonElementTypes.DOUBLE_QUOTED_STRING, isCanEscapeEol(),
+ isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect) {
+ @NotNull
+ @Override
+ protected IElementType handleSingleSlashEscapeSequence() {
+ return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence();
+ }
+
+ @Override
+ protected boolean shouldAllowSlashZero() {
+ return isPermissiveDialect;
+ }
+ },
+ new IElementType[]{JsonElementTypes.DOUBLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY);
+ layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\'', JsonElementTypes.SINGLE_QUOTED_STRING, isCanEscapeEol(),
+ isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect){
+ @NotNull
+ @Override
+ protected IElementType handleSingleSlashEscapeSequence() {
+ return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence();
+ }
+
+ @Override
+ protected boolean shouldAllowSlashZero() {
+ return isPermissiveDialect;
+ }
+ },
+ new IElementType[]{JsonElementTypes.SINGLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY);
+ return layeredLexer;
+ }
+
+ private boolean isPermissiveDialect() {
+ FileType fileType = myFile == null ? null : myFile.getFileType();
+ boolean isPermissiveDialect = false;
+ if (fileType instanceof JsonFileType) {
+ Language language = ((JsonFileType)fileType).getLanguage();
+ isPermissiveDialect = language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings();
+ }
+ return isPermissiveDialect;
+ }
+
+ @NotNull
+ @Override
+ public TextAttributesKey[] getTokenHighlights(IElementType type) {
+ return pack(ourAttributes.get(type));
+ }
+ }
+
+ @NotNull
+ protected Lexer getLexer() {
+ return new JsonLexer();
+ }
+
+ protected boolean isCanEscapeEol() {
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5FileType.java b/json/src/com/intellij/json/json5/Json5FileType.java
new file mode 100644
index 00000000..b9979faf
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5FileType.java
@@ -0,0 +1,32 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonFileType;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5FileType extends JsonFileType {
+ public static final Json5FileType INSTANCE = new Json5FileType();
+ public static final String DEFAULT_EXTENSION = "json5";
+
+ public Json5FileType() {
+ super(Json5Language.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JSON5";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JSON5";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return DEFAULT_EXTENSION;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5FileTypeFactory.java b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java
new file mode 100644
index 00000000..bf6a0254
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5FileTypeFactory extends FileTypeFactory {
+ @Override
+ public void createFileTypes(@NotNull FileTypeConsumer consumer) {
+ consumer.consume(Json5FileType.INSTANCE, Json5FileType.DEFAULT_EXTENSION);
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5Language.java b/json/src/com/intellij/json/json5/Json5Language.java
new file mode 100644
index 00000000..a6412639
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5Language.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonLanguage;
+
+public class Json5Language extends JsonLanguage {
+ public static final Json5Language INSTANCE = new Json5Language();
+
+ protected Json5Language() {
+ super("JSON5", "application/json5");
+ }
+
+ @Override
+ public boolean hasPermissiveStrings() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5Lexer.java b/json/src/com/intellij/json/json5/Json5Lexer.java
new file mode 100644
index 00000000..0617e379
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5Lexer.java
@@ -0,0 +1,10 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.lexer.FlexAdapter;
+
+public class Json5Lexer extends FlexAdapter {
+ public Json5Lexer() {
+ super(new _Json5Lexer());
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5ParserDefinition.java b/json/src/com/intellij/json/json5/Json5ParserDefinition.java
new file mode 100644
index 00000000..d0b146bc
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5ParserDefinition.java
@@ -0,0 +1,31 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.json.psi.impl.JsonFileImpl;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IFileElementType;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5ParserDefinition extends JsonParserDefinition {
+ public static final IFileElementType FILE = new IFileElementType(Json5Language.INSTANCE);
+
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new Json5Lexer();
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new JsonFileImpl(fileViewProvider, Json5Language.INSTANCE);
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return FILE;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java
new file mode 100644
index 00000000..6ba518b4
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java
@@ -0,0 +1,36 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalkerFactory;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5PsiWalkerFactory implements JsonLikePsiWalkerFactory {
+ @Override
+ public boolean handles(@NotNull PsiElement element) {
+ PsiElement parent = element.getParent();
+ if (parent == null) return false;
+ return JsonDialectUtil.getLanguage(CompletionUtil.getOriginalOrSelf(parent)) == Json5Language.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject) {
+ return new JsonOriginalPsiWalker() {
+ @Override
+ public boolean isNameQuoted() {
+ return false;
+ }
+
+ @Override
+ public boolean onlyDoubleQuotesForStringLiterals() {
+ return false;
+ }
+ };
+ }
+}
diff --git a/json/src/com/intellij/json/json5/_Json5Lexer.flex b/json/src/com/intellij/json/json5/_Json5Lexer.flex
new file mode 100644
index 00000000..14f4130f
--- /dev/null
+++ b/json/src/com/intellij/json/json5/_Json5Lexer.flex
@@ -0,0 +1,64 @@
+package com.intellij.json.json5;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+%%
+
+%{
+ public _Json5Lexer() {
+ this((java.io.Reader)null);
+ }
+%}
+
+%public
+%class _Json5Lexer
+%implements FlexLexer
+%function advance
+%type IElementType
+%unicode
+
+EOL=\R
+WHITE_SPACE=\s+
+HEX_DIGIT=[0-9A-Fa-f]
+
+LINE_COMMENT="//".*
+BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")?
+LINE_TERMINATOR_SEQUENCE=\R
+CRLF= [\ \t \f]* {LINE_TERMINATOR_SEQUENCE}
+DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n]|\\{CRLF})*\"?
+SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n]|\\{CRLF})*'?
+JSON5_NUMBER=(\+|-)?(0|[1-9][0-9]*)?\.?([0-9]+)?([eE][+-]?[0-9]*)?
+HEX_DIGITS=({HEX_DIGIT})+
+HEX_INTEGER_LITERAL=(\+|-)?0[Xx]({HEX_DIGITS})
+NUMBER={JSON5_NUMBER}|{HEX_INTEGER_LITERAL}|Infinity|-Infinity|\+Infinity|NaN|-NaN|\+NaN
+IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+
+
+%%
+<YYINITIAL> {
+ {WHITE_SPACE} { return WHITE_SPACE; }
+
+ "{" { return L_CURLY; }
+ "}" { return R_CURLY; }
+ "[" { return L_BRACKET; }
+ "]" { return R_BRACKET; }
+ "," { return COMMA; }
+ ":" { return COLON; }
+ "true" { return TRUE; }
+ "false" { return FALSE; }
+ "null" { return NULL; }
+
+ {LINE_COMMENT} { return LINE_COMMENT; }
+ {BLOCK_COMMENT} { return BLOCK_COMMENT; }
+ {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; }
+ {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; }
+ {NUMBER} { return NUMBER; }
+ {IDENTIFIER} { return IDENTIFIER; }
+
+}
+
+[^] { return BAD_CHARACTER; }
diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java
new file mode 100644
index 00000000..1ed1f273
--- /dev/null
+++ b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java
@@ -0,0 +1,52 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.codeinsight;
+
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.codeinsight.JsonLiteralChecker;
+import com.intellij.json.codeinsight.StandardJsonLiteralChecker;
+import com.intellij.json.json5.Json5Language;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public class Json5JsonLiteralChecker implements JsonLiteralChecker {
+ private static final Pattern VALID_HEX_ESCAPE = Pattern.compile("\\\\(x[0-9a-fA-F]{2})");
+ private static final Pattern INVALID_NUMERIC_ESCAPE = Pattern.compile("\\\\[1-9]");
+ @Nullable
+ @Override
+ public String getErrorForNumericLiteral(String literalText) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) {
+ String fragmentText = fragment.second;
+ if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && fragmentText.endsWith("\n")) {
+ if (StringUtil.isEmptyOrSpaces(fragmentText.substring(1, fragmentText.length() - 1))) {
+ return null;
+ }
+ }
+
+ if (fragmentText.startsWith("\\x") && VALID_HEX_ESCAPE.matcher(fragmentText).matches()) {
+ return null;
+ }
+
+ if (!StandardJsonLiteralChecker.VALID_ESCAPE.matcher(fragmentText).matches() && !INVALID_NUMERIC_ESCAPE.matcher(fragmentText).matches()) {
+ return null;
+ }
+
+ final String error = StandardJsonLiteralChecker.getStringError(fragmentText);
+ return error == null ? null : Pair.create(fragment.first, error);
+ }
+
+ @Override
+ public boolean isApplicable(PsiElement element) {
+ return JsonDialectUtil.getLanguage(element) == Json5Language.INSTANCE;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java
new file mode 100644
index 00000000..d61cffa1
--- /dev/null
+++ b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java
@@ -0,0 +1,81 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.codeinsight;
+
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+import com.intellij.json.json5.Json5Language;
+import com.intellij.json.psi.JsonLiteral;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonReferenceExpression;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+public class Json5StandardComplianceInspection extends JsonStandardComplianceInspection {
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.compliance5.name");
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ if (!(JsonDialectUtil.getLanguage(holder.getFile()) instanceof Json5Language)) return PsiElementVisitor.EMPTY_VISITOR;
+ return new StandardJson5ValidatingElementVisitor(holder);
+ }
+
+ @Override
+ public JComponent createOptionsPanel() {
+ return null;
+ }
+
+ private class StandardJson5ValidatingElementVisitor extends StandardJsonValidatingElementVisitor {
+ StandardJson5ValidatingElementVisitor(ProblemsHolder holder) {
+ super(holder);
+ }
+
+ @Override
+ protected boolean allowComments() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowSingleQuotes() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowIdentifierPropertyNames() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowTrailingCommas() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowNanInfinity() {
+ return true;
+ }
+
+ @Override
+ protected boolean isValidPropertyName(@NotNull PsiElement literal) {
+ if (literal instanceof JsonLiteral) {
+ String textWithoutHostEscaping = JsonPsiUtil.getElementTextWithoutHostEscaping(literal);
+ return textWithoutHostEscaping.startsWith("\"") || textWithoutHostEscaping.startsWith("'");
+ }
+ if (literal instanceof JsonReferenceExpression) {
+ return StringUtil.isJavaIdentifier(literal.getText());
+ }
+ return false;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java
new file mode 100644
index 00000000..f20466f5
--- /dev/null
+++ b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java
@@ -0,0 +1,20 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.highlighting;
+
+import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory;
+import com.intellij.json.json5.Json5Lexer;
+import com.intellij.lexer.Lexer;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5SyntaxHighlightingFactory extends JsonSyntaxHighlighterFactory {
+ @NotNull
+ @Override
+ protected Lexer getLexer() {
+ return new Json5Lexer();
+ }
+
+ @Override
+ protected boolean isCanEscapeEol() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonContextType.java b/json/src/com/intellij/json/liveTemplates/JsonContextType.java
new file mode 100644
index 00000000..cc5ef222
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonContextType.java
@@ -0,0 +1,22 @@
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.FileTypeBasedContextType;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Konstantin.Ulitin
+ */
+public class JsonContextType extends FileTypeBasedContextType {
+ protected JsonContextType() {
+ super("JSON", JsonBundle.message("json.template.context.type"), JsonFileType.INSTANCE);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile;
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java
new file mode 100644
index 00000000..0e21d5fa
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java
@@ -0,0 +1,21 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.TemplateContextType;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+public class JsonInLiteralsContextType extends TemplateContextType {
+ protected JsonInLiteralsContextType() {
+ super("JSON_STRING_VALUES", "JSON String Values", JsonContextType.class);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile && psiElement().inside(JsonStringLiteral.class).accepts(file.findElementAt(offset));
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java
new file mode 100644
index 00000000..145e1b28
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java
@@ -0,0 +1,33 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.TemplateContextType;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.patterns.PatternCondition;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+public class JsonInPropertyKeysContextType extends TemplateContextType {
+ protected JsonInPropertyKeysContextType() {
+ super("JSON_PROPERTY_KEYS", "JSON Property Keys", JsonContextType.class);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile && psiElement().inside(psiElement(JsonValue.class)
+ .with(new PatternCondition<PsiElement>("insidePropertyKey") {
+ @Override
+ public boolean accepts(@NotNull PsiElement element,
+ ProcessingContext context) {
+ return JsonPsiUtil.isPropertyKey(element);
+ }
+ })).beforeLeaf(psiElement(JsonElementTypes.COLON)).accepts(file.findElementAt(offset));
+ }
+} \ No newline at end of file
diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java
new file mode 100644
index 00000000..f82115fc
--- /dev/null
+++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java
@@ -0,0 +1,20 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.navigation;
+
+import kotlin.NotImplementedError;
+
+public enum JsonQualifiedNameKind {
+ Qualified,
+ JsonPointer;
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case Qualified:
+ return "qualified name";
+ case JsonPointer:
+ return "JSON Pointer";
+ }
+ throw new NotImplementedError("Unknown name kind: " + this.name());
+ }
+}
diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java
new file mode 100644
index 00000000..31ee728c
--- /dev/null
+++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java
@@ -0,0 +1,69 @@
+package com.intellij.json.navigation;
+
+import com.intellij.ide.actions.QualifiedNameProvider;
+import com.intellij.json.JsonUtil;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonElement;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.JsonPointerUtil;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonQualifiedNameProvider implements QualifiedNameProvider {
+ @Nullable
+ @Override
+ public PsiElement adjustElementToCopy(PsiElement element) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getQualifiedName(PsiElement element) {
+ return generateQualifiedName(element, JsonQualifiedNameKind.Qualified);
+ }
+
+ public static String generateQualifiedName(PsiElement element, JsonQualifiedNameKind qualifiedNameKind) {
+ if (!(element instanceof JsonElement)) {
+ return null;
+ }
+ JsonElement parentProperty = PsiTreeUtil.getNonStrictParentOfType(element, JsonProperty.class, JsonArray.class);
+ StringBuilder builder = new StringBuilder();
+ while (parentProperty != null) {
+ if (parentProperty instanceof JsonProperty) {
+ String name = parentProperty.getName();
+ if (qualifiedNameKind == JsonQualifiedNameKind.JsonPointer) {
+ name = name == null ? null : JsonPointerUtil.escapeForJsonPointer(name);
+ }
+ builder.insert(0, name);
+ builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? "/" : ".");
+ }
+ else {
+ int index = JsonUtil.getArrayIndexOfItem(element instanceof JsonProperty ? element.getParent() : element);
+ if (index == -1) return null;
+ builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? ("/" + index) : ("[" + index + "]"));
+ }
+ element = parentProperty;
+ parentProperty = PsiTreeUtil.getParentOfType(parentProperty, JsonProperty.class, JsonArray.class);
+ }
+
+ if (builder.length() == 0) return null;
+
+ // if the first operation is array indexing, we insert the 'root' element $
+ if (builder.charAt(0) == '[') {
+ builder.insert(0, "$");
+ }
+
+ return StringUtil.trimStart(builder.toString(), ".");
+ }
+
+ @Override
+ public PsiElement qualifiedNameToElement(String fqn, Project project) {
+ return null;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonElement.java b/json/src/com/intellij/json/psi/JsonElement.java
new file mode 100644
index 00000000..f3345255
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonElement.java
@@ -0,0 +1,10 @@
+package com.intellij.json.psi;
+
+import com.intellij.psi.NavigatablePsiElement;
+import com.intellij.psi.PsiElement;
+
+/**
+ * @author Mikhail Golubev
+ */
+public interface JsonElement extends PsiElement, NavigatablePsiElement {
+}
diff --git a/json/src/com/intellij/json/psi/JsonElementGenerator.java b/json/src/com/intellij/json/psi/JsonElementGenerator.java
new file mode 100644
index 00000000..535f9d58
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonElementGenerator.java
@@ -0,0 +1,79 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonElementGenerator {
+ private final Project myProject;
+
+ public JsonElementGenerator(@NotNull Project project) {
+ myProject = project;
+ }
+
+ /**
+ * Create lightweight in-memory {@link com.intellij.json.psi.JsonFile} filled with {@code content}.
+ *
+ * @param content content of the file to be created
+ * @return created file
+ */
+ @NotNull
+ public PsiFile createDummyFile(@NotNull String content) {
+ final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(myProject);
+ return psiFileFactory.createFileFromText("dummy." + JsonFileType.INSTANCE.getDefaultExtension(), JsonFileType.INSTANCE, content);
+ }
+
+ /**
+ * Create JSON value from supplied content.
+ *
+ * @param content properly escaped text of JSON value, e.g. Java literal {@code "\"new\\nline\""} if you want to create string literal
+ * @param <T> type of the JSON value desired
+ * @return element created from given text
+ *
+ * @see #createStringLiteral(String)
+ */
+ @NotNull
+ public <T extends JsonValue> T createValue(@NotNull String content) {
+ final PsiFile file = createDummyFile("{\"foo\": " + content + "}");
+ //noinspection unchecked,ConstantConditions
+ return (T)((JsonObject)file.getFirstChild()).getPropertyList().get(0).getValue();
+ }
+
+ @NotNull
+ public JsonObject createObject(@NotNull String content) {
+ final PsiFile file = createDummyFile("{" + content + "}");
+ // noinspection ConstantConditions
+ return (JsonObject) file.getFirstChild();
+ }
+
+ /**
+ * Create JSON string literal from supplied <em>unescaped</em> content.
+ *
+ * @param unescapedContent unescaped content of string literal, e.g. Java literal {@code "new\nline"} (compare with {@link #createValue(String)}).
+ * @return JSON string literal created from given text
+ */
+ @NotNull
+ public JsonStringLiteral createStringLiteral(@NotNull String unescapedContent) {
+ return createValue('"' + StringUtil.escapeStringCharacters(unescapedContent) + '"');
+ }
+
+ @NotNull
+ public JsonProperty createProperty(@NotNull final String name, @NotNull final String value) {
+ final PsiFile file = createDummyFile("{\"" + name + "\": " + value + "}");
+ // noinspection ConstantConditions
+ return ((JsonObject) file.getFirstChild()).getPropertyList().get(0);
+ }
+
+ @NotNull
+ public PsiElement createComma() {
+ final JsonArray jsonArray1 = createValue("[1, 2]");
+ return jsonArray1.getValueList().get(0).getNextSibling();
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonFile.java b/json/src/com/intellij/json/psi/JsonFile.java
new file mode 100644
index 00000000..25fb4829
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonFile.java
@@ -0,0 +1,23 @@
+package com.intellij.json.psi;
+
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public interface JsonFile extends JsonElement, PsiFile {
+ /**
+ * Returns {@link JsonArray} or {@link JsonObject} value according to JSON standard.
+ *
+ * @return top-level JSON element if any or {@code null} otherwise
+ */
+ @Nullable
+ JsonValue getTopLevelValue();
+
+ @NotNull
+ List<JsonValue> getAllTopLevelValues();
+}
diff --git a/json/src/com/intellij/json/psi/JsonParserUtil.java b/json/src/com/intellij/json/psi/JsonParserUtil.java
new file mode 100644
index 00000000..d0f5f467
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonParserUtil.java
@@ -0,0 +1,25 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.parser.GeneratedParserUtilBase;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonParserUtil extends GeneratedParserUtilBase {
+
+ public static boolean notTrailingComma(@NotNull PsiBuilder builder, int level) {
+ if (builder.getTokenType() != JsonElementTypes.COMMA) {
+ return false;
+ }
+ final IElementType afterComma = builder.lookAhead(1);
+ if (afterComma == JsonElementTypes.R_BRACKET || afterComma == JsonElementTypes.R_CURLY) {
+ builder.error("trailing comma");
+ }
+ builder.advanceLexer();
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java
new file mode 100644
index 00000000..4c4a9385
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java
@@ -0,0 +1,42 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.TokenType;
+
+public class JsonPsiChangeUtils {
+ public static void removeCommaSeparatedFromList(final ASTNode myNode, final ASTNode parent) {
+ ASTNode from = myNode, to = myNode.getTreeNext();
+
+ boolean seenComma = false;
+
+ ASTNode toCandidate = to;
+ while (toCandidate != null && toCandidate.getElementType() == TokenType.WHITE_SPACE) {
+ toCandidate = toCandidate.getTreeNext();
+ }
+
+ if (toCandidate != null && toCandidate.getElementType() == JsonElementTypes.COMMA) {
+ toCandidate = toCandidate.getTreeNext();
+ to = toCandidate;
+ seenComma = true;
+
+ if (to != null && to.getElementType() == TokenType.WHITE_SPACE) {
+ to = to.getTreeNext();
+ }
+ }
+
+ if (!seenComma) {
+ ASTNode treePrev = from.getTreePrev();
+
+ while (treePrev != null && treePrev.getElementType() == TokenType.WHITE_SPACE) {
+ from = treePrev;
+ treePrev = treePrev.getTreePrev();
+ }
+ if (treePrev != null && treePrev.getElementType() == JsonElementTypes.COMMA) {
+ from = treePrev;
+ }
+ }
+
+ parent.removeRange(from, to);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonPsiUtil.java b/json/src/com/intellij/json/psi/JsonPsiUtil.java
new file mode 100644
index 00000000..78cf5b34
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonPsiUtil.java
@@ -0,0 +1,231 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES;
+
+/**
+ * Various helper methods for working with PSI of JSON language.
+ *
+ * @author Mikhail Golubev
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class JsonPsiUtil {
+ private JsonPsiUtil() {
+ // empty
+ }
+
+
+ /**
+ * Checks that PSI element represents item of JSON array.
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is array element
+ */
+ public static boolean isArrayElement(@NotNull PsiElement element) {
+ return element instanceof JsonValue && element.getParent() instanceof JsonArray;
+ }
+
+ /**
+ * Checks that PSI element represents key of JSON property (key-value pair of JSON object)
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is property key
+ */
+ public static boolean isPropertyKey(@NotNull PsiElement element) {
+ final PsiElement parent = element.getParent();
+ return parent instanceof JsonProperty && element == ((JsonProperty)parent).getNameElement();
+ }
+
+ /**
+ * Checks that PSI element represents value of JSON property (key-value pair of JSON object)
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is property value
+ */
+ public static boolean isPropertyValue(@NotNull PsiElement element) {
+ final PsiElement parent = element.getParent();
+ return parent instanceof JsonProperty && element == ((JsonProperty)parent).getValue();
+ }
+
+ /**
+ * Find the furthest sibling element with the same type as given anchor.
+ * <p/>
+ * Ignore white spaces for any type of element except {@link com.intellij.json.JsonElementTypes#LINE_COMMENT}
+ * where non indentation white space (that has new line in the middle) will stop the search.
+ *
+ * @param anchor element to start from
+ * @param after whether to scan through sibling elements forward or backward
+ * @return described element or anchor if search stops immediately
+ */
+ @NotNull
+ public static PsiElement findFurthestSiblingOfSameType(@NotNull PsiElement anchor, boolean after) {
+ ASTNode node = anchor.getNode();
+ // Compare by node type to distinguish between different types of comments
+ final IElementType expectedType = node.getElementType();
+ ASTNode lastSeen = node;
+ while (node != null) {
+ final IElementType elementType = node.getElementType();
+ if (elementType == expectedType) {
+ lastSeen = node;
+ }
+ else if (elementType == TokenType.WHITE_SPACE) {
+ if (expectedType == JsonElementTypes.LINE_COMMENT && node.getText().indexOf('\n', 1) != -1) {
+ break;
+ }
+ }
+ else if (!JSON_COMMENTARIES.contains(elementType) || JSON_COMMENTARIES.contains(expectedType)) {
+ break;
+ }
+ node = after ? node.getTreeNext() : node.getTreePrev();
+ }
+ return lastSeen.getPsi();
+ }
+
+ /**
+ * Check that element type of the given AST node belongs to the token set.
+ * <p/>
+ * It slightly less verbose than {@code set.contains(node.getElementType())} and overloaded methods with the same name
+ * allow check ASTNode/PsiElement against both concrete element types and token sets in uniform way.
+ */
+ public static boolean hasElementType(@NotNull ASTNode node, @NotNull TokenSet set) {
+ return set.contains(node.getElementType());
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet)
+ */
+ public static boolean hasElementType(@NotNull ASTNode node, IElementType... types) {
+ return hasElementType(node, TokenSet.create(types));
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet)
+ */
+ public static boolean hasElementType(@NotNull PsiElement element, @NotNull TokenSet set) {
+ return element.getNode() != null && hasElementType(element.getNode(), set);
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.IElementType...)
+ */
+ public static boolean hasElementType(@NotNull PsiElement element, IElementType... types) {
+ return element.getNode() != null && hasElementType(element.getNode(), types);
+ }
+
+ /**
+ * Returns text of the given PSI element. Unlike obvious {@link PsiElement#getText()} this method unescapes text of the element if latter
+ * belongs to injected code fragment using {@link InjectedLanguageManager#getUnescapedText(PsiElement)}.
+ *
+ * @param element PSI element which text is needed
+ * @return text of the element with any host escaping removed
+ */
+ @NotNull
+ public static String getElementTextWithoutHostEscaping(@NotNull PsiElement element) {
+ final InjectedLanguageManager manager = InjectedLanguageManager.getInstance(element.getProject());
+ if (manager.isInjectedFragment(element.getContainingFile())) {
+ return manager.getUnescapedText(element);
+ }
+ else {
+ return element.getText();
+ }
+ }
+
+ /**
+ * Returns content of the string literal (without escaping) striving to preserve as much of user data as possible.
+ * <ul>
+ * <li>If literal length is greater than one and it starts and ends with the same quote and the last quote is not escaped, returns
+ * text without first and last characters.</li>
+ * <li>Otherwise if literal still begins with a quote, returns text without first character only.</li>
+ * <li>Returns unmodified text in all other cases.</li>
+ * </ul>
+ *
+ * @param text presumably result of {@link JsonStringLiteral#getText()}
+ * @return
+ */
+ @NotNull
+ public static String stripQuotes(@NotNull String text) {
+ if (text.length() > 0) {
+ final char firstChar = text.charAt(0);
+ final char lastChar = text.charAt(text.length() - 1);
+ if (firstChar == '\'' || firstChar == '"') {
+ if (text.length() > 1 && firstChar == lastChar && !isEscapedChar(text, text.length() - 1)) {
+ return text.substring(1, text.length() - 1);
+ }
+ return text.substring(1);
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Checks that character in given position is escaped with backslashes.
+ *
+ * @param text text character belongs to
+ * @param position position of the character
+ * @return whether character at given position is escaped, i.e. preceded by odd number of backslashes
+ */
+ public static boolean isEscapedChar(@NotNull String text, int position) {
+ int count = 0;
+ for (int i = position - 1; i >= 0 && text.charAt(i) == '\\'; i--) {
+ count++;
+ }
+ return count % 2 != 0;
+ }
+
+ /**
+ * Add new property and necessary comma either at the beginning of the object literal or at its end.
+ *
+ * @param object object literal
+ * @param property new property, probably created via {@link JsonElementGenerator}
+ * @param first if true make new property first in the object, otherwise append in the end of property list
+ * @return property as returned by {@link PsiElement#addAfter(PsiElement, PsiElement)}
+ */
+ @NotNull
+ public static PsiElement addProperty(@NotNull JsonObject object, @NotNull JsonProperty property, boolean first) {
+ final List<JsonProperty> propertyList = object.getPropertyList();
+ if (!first) {
+ final JsonProperty lastProperty = ContainerUtil.getLastItem(propertyList);
+ if (lastProperty != null) {
+ final PsiElement addedProperty = object.addAfter(property, lastProperty);
+ object.addBefore(new JsonElementGenerator(object.getProject()).createComma(), addedProperty);
+ return addedProperty;
+ }
+ }
+ final PsiElement leftBrace = object.getFirstChild();
+ assert hasElementType(leftBrace, JsonElementTypes.L_CURLY);
+ final PsiElement addedProperty = object.addAfter(property, leftBrace);
+ if (!propertyList.isEmpty()) {
+ object.addAfter(new JsonElementGenerator(object.getProject()).createComma(), addedProperty);
+ }
+ return addedProperty;
+ }
+
+ @NotNull
+ public static Set<String> getOtherSiblingPropertyNames(@Nullable JsonProperty property) {
+ if (property == null) return Collections.emptySet();
+ JsonObject object = ObjectUtils.tryCast(property.getParent(), JsonObject.class);
+ if (object == null) return Collections.emptySet();
+ Set<String> result = ContainerUtil.newHashSet();
+ for (JsonProperty jsonProperty : object.getPropertyList()) {
+ if (jsonProperty != property) {
+ result.add(jsonProperty.getName());
+ }
+ }
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java
new file mode 100644
index 00000000..855655f7
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java
@@ -0,0 +1,32 @@
+package com.intellij.json.psi;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.AbstractElementManipulator;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonStringLiteralManipulator extends AbstractElementManipulator<JsonStringLiteral> {
+
+ @Override
+ public JsonStringLiteral handleContentChange(@NotNull JsonStringLiteral element, @NotNull TextRange range, String newContent)
+ throws IncorrectOperationException {
+ assert new TextRange(0, element.getTextLength()).contains(range);
+
+ final String originalContent = element.getText();
+ final TextRange withoutQuotes = getRangeInElement(element);
+ final JsonElementGenerator generator = new JsonElementGenerator(element.getProject());
+ final String replacement = originalContent.substring(withoutQuotes.getStartOffset(), range.getStartOffset()) +
+ newContent +
+ originalContent.substring(range.getEndOffset(), withoutQuotes.getEndOffset());
+ return (JsonStringLiteral)element.replace(generator.createStringLiteral(replacement));
+ }
+
+ @NotNull
+ @Override
+ public TextRange getRangeInElement(@NotNull JsonStringLiteral element) {
+ final String content = element.getText();
+ final int startOffset = content.startsWith("'") || content.startsWith("\"") ? 1 : 0;
+ final int endOffset = content.length() > 1 && (content.endsWith("'") || content.endsWith("\"")) ? -1 : 0;
+ return new TextRange(startOffset, content.length() + endOffset);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java
new file mode 100644
index 00000000..1f7b7290
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java
@@ -0,0 +1,194 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.LiteralTextEscaper;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import org.jetbrains.annotations.NotNull;
+
+public abstract class JSStringLiteralEscaper<T extends PsiLanguageInjectionHost> extends LiteralTextEscaper<T> {
+ private int[] outSourceOffsets;
+
+ public JSStringLiteralEscaper(T host) {
+ super(host);
+ }
+
+ @Override
+ public boolean decode(@NotNull final TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
+ String subText = rangeInsideHost.substring(myHost.getText());
+
+ Ref<int[]> sourceOffsetsRef = new Ref<>();
+ boolean result = parseStringCharacters(subText, outChars, sourceOffsetsRef, isRegExpLiteral(), !isOneLine());
+ outSourceOffsets = sourceOffsetsRef.get();
+ return result;
+ }
+
+ protected abstract boolean isRegExpLiteral();
+
+ @Override
+ public int getOffsetInHost(int offsetInDecoded, @NotNull final TextRange rangeInsideHost) {
+ int result = offsetInDecoded < outSourceOffsets.length ? outSourceOffsets[offsetInDecoded] : -1;
+ if (result == -1) return -1;
+ return (result <= rangeInsideHost.getLength() ? result : rangeInsideHost.getLength()) + rangeInsideHost.getStartOffset();
+ }
+
+ @Override
+ public boolean isOneLine() {
+ return true;
+ }
+
+ public static boolean parseStringCharacters(String chars, StringBuilder outChars, Ref<int[]> sourceOffsetsRef, boolean regExp, boolean escapeBacktick) {
+ int[] sourceOffsets = new int[chars.length() + 1];
+ sourceOffsetsRef.set(sourceOffsets);
+
+ if (chars.indexOf('\\') < 0) {
+ outChars.append(chars);
+ for (int i = 0; i < sourceOffsets.length; i++) {
+ sourceOffsets[i] = i;
+ }
+ return true;
+ }
+
+ int index = 0;
+ final int outOffset = outChars.length();
+ while (index < chars.length()) {
+ char c = chars.charAt(index++);
+
+ sourceOffsets[outChars.length() - outOffset] = index - 1;
+ sourceOffsets[outChars.length() + 1 - outOffset] = index;
+
+ if (c != '\\') {
+ outChars.append(c);
+ continue;
+ }
+ if (index == chars.length()) return false;
+ c = chars.charAt(index++);
+ if (escapeBacktick && c == '`') {
+ outChars.append(c);
+ }
+ else if (regExp) {
+ if (c != '/') {
+ outChars.append('\\');
+ }
+ outChars.append(c);
+ }
+ else {
+ switch (c) {
+ case 'b':
+ outChars.append('\b');
+ break;
+
+ case 't':
+ outChars.append('\t');
+ break;
+
+ case 'n':
+ outChars.append('\n');
+ break;
+
+ case 'f':
+ outChars.append('\f');
+ break;
+
+ case 'r':
+ outChars.append('\r');
+ break;
+
+ case '"':
+ outChars.append('"');
+ break;
+
+ case '/':
+ outChars.append('/');
+ break;
+
+ case '\n':
+ outChars.append('\n');
+ break;
+ case '\'':
+ outChars.append('\'');
+ break;
+
+ case '\\':
+ outChars.append('\\');
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7': {
+ char startC = c;
+ int v = (int)c - '0';
+ if (index < chars.length()) {
+ c = chars.charAt(index++);
+ if ('0' <= c && c <= '7') {
+ v <<= 3;
+ v += c - '0';
+ if (startC <= '3' && index < chars.length()) {
+ c = chars.charAt(index++);
+ if ('0' <= c && c <= '7') {
+ v <<= 3;
+ v += c - '0';
+ }
+ else {
+ index--;
+ }
+ }
+ }
+ else {
+ index--;
+ }
+ }
+ outChars.append((char)v);
+ }
+ break;
+ case 'x':
+ if (index + 2 <= chars.length()) {
+ try {
+ int v = Integer.parseInt(chars.substring(index, index + 2), 16);
+ outChars.append((char)v);
+ index += 2;
+ }
+ catch (Exception e) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ break;
+ case 'u':
+ if (index + 4 <= chars.length()) {
+ try {
+ int v = Integer.parseInt(chars.substring(index, index + 4), 16);
+ //line separators are invalid here
+ if (v == 0x000a || v == 0x000d) return false;
+ c = chars.charAt(index);
+ if (c == '+' || c == '-') return false;
+ outChars.append((char)v);
+ index += 4;
+ }
+ catch (Exception e) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ break;
+
+ default:
+ outChars.append(c);
+ break;
+ }
+ }
+
+ sourceOffsets[outChars.length() - outOffset] = index;
+ }
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonElementImpl.java b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java
new file mode 100644
index 00000000..e65b4b68
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java
@@ -0,0 +1,23 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.json.psi.JsonElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonElementImpl extends ASTWrapperPsiElement implements JsonElement {
+
+ public JsonElementImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public String toString() {
+ final String className = getClass().getSimpleName();
+ return StringUtil.trimEnd(className, "Impl");
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonFileImpl.java b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java
new file mode 100644
index 00000000..8ddcdd2c
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java
@@ -0,0 +1,43 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.Language;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonFileImpl extends PsiFileBase implements JsonFile {
+
+ public JsonFileImpl(FileViewProvider fileViewProvider, Language language) {
+ super(fileViewProvider, language);
+ }
+
+ @NotNull
+ @Override
+ public FileType getFileType() {
+ return getViewProvider().getFileType();
+ }
+
+ @Nullable
+ @Override
+ public JsonValue getTopLevelValue() {
+ return PsiTreeUtil.getChildOfType(this, JsonValue.class);
+ }
+
+ @NotNull
+ @Override
+ public List<JsonValue> getAllTopLevelValues() {
+ return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonValue.class);
+ }
+
+ @Override
+ public String toString() {
+ return "JsonFile: " + getName();
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java
new file mode 100644
index 00000000..3ada2002
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonLiteral;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
+import org.jetbrains.annotations.NotNull;
+
+abstract class JsonLiteralMixin extends JsonElementImpl implements JsonLiteral {
+ protected JsonLiteralMixin(ASTNode node) {
+ super(node);
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferences() {
+ return ReferenceProvidersRegistry.getReferencesFromProviders(this);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java
new file mode 100644
index 00000000..91c5c3a8
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Mikhail Golubev
+ */
+public abstract class JsonObjectMixin extends JsonContainerImpl implements JsonObject {
+ private final CachedValueProvider<Map<String, JsonProperty>> myPropertyCache =
+ () -> {
+ final Map<String, JsonProperty> cache = new HashMap<>();
+ for (JsonProperty property : getPropertyList()) {
+ final String propertyName = property.getName();
+ // Preserve the old behavior - return the first value in findProperty()
+ if (!cache.containsKey(propertyName)) {
+ cache.put(propertyName, property);
+ }
+ }
+ // Cached value is invalidated every time file containing this object is modified
+ return CachedValueProvider.Result.createSingleDependency(cache, this);
+ };
+
+ public JsonObjectMixin(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Nullable
+ @Override
+ public JsonProperty findProperty(@NotNull String name) {
+ return CachedValuesManager.getCachedValue(this, myPropertyCache).get(name);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java
new file mode 100644
index 00000000..3fc29aef
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java
@@ -0,0 +1,42 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonElementGenerator;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+abstract class JsonPropertyMixin extends JsonElementImpl implements JsonProperty {
+ JsonPropertyMixin(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException {
+ final JsonElementGenerator generator = new JsonElementGenerator(getProject());
+ // Strip only both quotes in case user wants some exotic name like key'
+ getNameElement().replace(generator.createStringLiteral(StringUtil.unquoteString(name)));
+ return this;
+ }
+
+ @Override
+ public PsiReference getReference() {
+ return new JsonPropertyNameReference(this);
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferences() {
+ final PsiReference[] fromProviders = ReferenceProvidersRegistry.getReferencesFromProviders(this);
+ return ArrayUtil.prepend(new JsonPropertyNameReference(this), fromProviders);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java
new file mode 100644
index 00000000..5b51dcb9
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java
@@ -0,0 +1,74 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonPropertyNameReference implements PsiReference {
+ private final JsonProperty myProperty;
+
+ public JsonPropertyNameReference(@NotNull JsonProperty property) {
+ myProperty = property;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getElement() {
+ return myProperty;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getRangeInElement() {
+ final JsonValue nameElement = myProperty.getNameElement();
+ // Either value of string with quotes stripped or element's text as is
+ return ElementManipulators.getValueTextRange(nameElement);
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolve() {
+ return myProperty;
+ }
+
+ @NotNull
+ @Override
+ public String getCanonicalText() {
+ return myProperty.getName();
+ }
+
+ @Override
+ public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
+ return myProperty.setName(newElementName);
+ }
+
+ @Override
+ public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
+ return null;
+ }
+
+ @Override
+ public boolean isReferenceTo(@NotNull PsiElement element) {
+ if (!(element instanceof JsonProperty)) {
+ return false;
+ }
+ // May reference to the property with the same name for compatibility with JavaScript JSON support
+ final JsonProperty otherProperty = (JsonProperty)element;
+ final PsiElement selfResolve = resolve();
+ return otherProperty.getName().equals(getCanonicalText()) && selfResolve != otherProperty;
+ }
+
+ @Override
+ public boolean isSoft() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java
new file mode 100644
index 00000000..126e3f7d
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java
@@ -0,0 +1,233 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.Language;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class JsonPsiImplUtils {
+ static final Key<List<Pair<TextRange, String>>> STRING_FRAGMENTS = new Key<>("JSON string fragments");
+
+ @NotNull
+ public static String getName(@NotNull JsonProperty property) {
+ return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(property.getNameElement().getText()));
+ }
+
+ /**
+ * Actually only JSON string literal should be accepted as valid name of property according to standard,
+ * but for compatibility with JavaScript integration any JSON literals as well as identifiers (unquoted words)
+ * are possible and highlighted as error later.
+ *
+ * @see JsonStandardComplianceInspection
+ */
+ @NotNull
+ public static JsonValue getNameElement(@NotNull JsonProperty property) {
+ final PsiElement firstChild = property.getFirstChild();
+ assert firstChild instanceof JsonLiteral || firstChild instanceof JsonReferenceExpression;
+ return (JsonValue)firstChild;
+ }
+
+ @Nullable
+ public static JsonValue getValue(@NotNull JsonProperty property) {
+ return PsiTreeUtil.getNextSiblingOfType(getNameElement(property), JsonValue.class);
+ }
+
+ public static boolean isQuotedString(@NotNull JsonLiteral literal) {
+ return literal.getNode().findChildByType(JsonParserDefinition.STRING_LITERALS) != null;
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonProperty property) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return property.getName();
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ final JsonValue value = property.getValue();
+ return value instanceof JsonLiteral ? value.getText() : null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ if (property.getValue() instanceof JsonArray) {
+ return AllIcons.Json.Property_brackets;
+ }
+ if (property.getValue() instanceof JsonObject) {
+ return AllIcons.Json.Property_braces;
+ }
+ return PlatformIcons.PROPERTY_ICON;
+ }
+ };
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonArray array) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return JsonBundle.message("json.array");
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ return AllIcons.Json.Array;
+ }
+ };
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonObject object) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return JsonBundle.message("json.object");
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ return AllIcons.Json.Object;
+ }
+ };
+ }
+
+ private static final String ourEscapesTable = "\"\"\\\\//b\bf\fn\nr\rt\t";
+
+ @NotNull
+ public static List<Pair<TextRange, String>> getTextFragments(@NotNull JsonStringLiteral literal) {
+ List<Pair<TextRange, String>> result = literal.getUserData(STRING_FRAGMENTS);
+ if (result == null) {
+ result = new ArrayList<>();
+ final String text = literal.getText();
+ final int length = text.length();
+ int pos = 1, unescapedSequenceStart = 1;
+ while (pos < length) {
+ if (text.charAt(pos) == '\\') {
+ if (unescapedSequenceStart != pos) {
+ result.add(Pair.create(new TextRange(unescapedSequenceStart, pos), text.substring(unescapedSequenceStart, pos)));
+ }
+ if (pos == length - 1) {
+ result.add(Pair.create(new TextRange(pos, pos + 1), "\\"));
+ break;
+ }
+ final char next = text.charAt(pos + 1);
+ switch (next) {
+ case '"':
+ case '\\':
+ case '/':
+ case 'b':
+ case 'f':
+ case 'n':
+ case 'r':
+ case 't':
+ final int idx = ourEscapesTable.indexOf(next);
+ result.add(Pair.create(new TextRange(pos, pos + 2), ourEscapesTable.substring(idx + 1, idx + 2)));
+ pos += 2;
+ break;
+ case 'u':
+ int i = pos + 2;
+ for (; i < pos + 6; i++) {
+ if (i == length || !StringUtil.isHexDigit(text.charAt(i))) {
+ break;
+ }
+ }
+ result.add(Pair.create(new TextRange(pos, i), text.substring(pos, i)));
+ pos = i;
+ break;
+ case 'x':
+ Language language = JsonDialectUtil.getLanguage(literal);
+ if (language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings()) {
+ int i2 = pos + 2;
+ for (; i2 < pos + 4; i2++) {
+ if (i2 == length || !StringUtil.isHexDigit(text.charAt(i2))) {
+ break;
+ }
+ }
+ result.add(Pair.create(new TextRange(pos, i2), text.substring(pos, i2)));
+ pos = i2;
+ break;
+ }
+ default:
+ result.add(Pair.create(new TextRange(pos, pos + 2), text.substring(pos, pos + 2)));
+ pos += 2;
+ }
+ unescapedSequenceStart = pos;
+ }
+ else {
+ pos++;
+ }
+ }
+ final int contentEnd = text.charAt(0) == text.charAt(length - 1) ? length - 1 : length;
+ if (unescapedSequenceStart < contentEnd) {
+ result.add(Pair.create(new TextRange(unescapedSequenceStart, contentEnd), text.substring(unescapedSequenceStart, contentEnd)));
+ }
+ result = Collections.unmodifiableList(result);
+ literal.putUserData(STRING_FRAGMENTS, result);
+ }
+ return result;
+ }
+
+ public static void delete(@NotNull JsonProperty property) {
+ final ASTNode myNode = property.getNode();
+ JsonPsiChangeUtils.removeCommaSeparatedFromList(myNode, myNode.getTreeParent());
+ }
+
+ @NotNull
+ public static String getValue(@NotNull JsonStringLiteral literal) {
+ return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(literal.getText()));
+ }
+
+ public static boolean isPropertyName(@NotNull JsonStringLiteral literal) {
+ final PsiElement parent = literal.getParent();
+ return parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal;
+ }
+
+ public static boolean getValue(@NotNull JsonBooleanLiteral literal) {
+ return literal.textMatches("true");
+ }
+
+ public static double getValue(@NotNull JsonNumberLiteral literal) {
+ return Double.parseDouble(literal.getText());
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java
new file mode 100644
index 00000000..3c0eb0e4
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiRecursiveVisitor;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonRecursiveElementVisitor extends JsonElementVisitor implements PsiRecursiveVisitor {
+
+ @Override
+ public void visitElement(final PsiElement element) {
+ element.acceptChildren(this);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java
new file mode 100644
index 00000000..99baebef
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java
@@ -0,0 +1,45 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.LiteralTextEscaper;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import com.intellij.psi.impl.source.tree.LeafElement;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Konstantin.Ulitin
+ */
+public abstract class JsonStringLiteralMixin extends JsonLiteralImpl implements PsiLanguageInjectionHost {
+ protected JsonStringLiteralMixin(ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public boolean isValidHost() {
+ return true;
+ }
+
+ @Override
+ public PsiLanguageInjectionHost updateText(@NotNull String text) {
+ ASTNode valueNode = getNode().getFirstChildNode();
+ assert valueNode instanceof LeafElement;
+ ((LeafElement)valueNode).replaceWithText(text);
+ return this;
+ }
+
+ @NotNull
+ @Override
+ public LiteralTextEscaper<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() {
+ return new JSStringLiteralEscaper<PsiLanguageInjectionHost>(this) {
+ @Override
+ protected boolean isRegExpLiteral() {
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public void subtreeChanged() {
+ putUserData(JsonPsiImplUtils.STRING_FRAGMENTS, null);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java
new file mode 100644
index 00000000..0b54775e
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.PsiTreeChangeEventImpl;
+import com.intellij.psi.impl.PsiTreeChangePreprocessorBase;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTreeChangePreprocessor extends PsiTreeChangePreprocessorBase {
+ public JsonTreeChangePreprocessor(@NotNull PsiManager psiManager) {
+ super(psiManager);
+ }
+
+ @Override
+ protected boolean acceptsEvent(@NotNull PsiTreeChangeEventImpl event) {
+ return event.getFile() instanceof JsonFile;
+ }
+
+ @Override
+ protected boolean isOutOfCodeBlock(@NotNull PsiElement element) {
+ return element.getLanguage() instanceof JsonLanguage;
+ }
+} \ No newline at end of file
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java
new file mode 100644
index 00000000..9a626219
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java
@@ -0,0 +1,32 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.lang.PsiStructureViewFactory;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewBuilderFactory implements PsiStructureViewFactory {
+ @Nullable
+ @Override
+ public StructureViewBuilder getStructureViewBuilder(@NotNull final PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) {
+ return null;
+ }
+
+ return new TreeBasedStructureViewBuilder() {
+ @NotNull
+ @Override
+ public StructureViewModel createStructureViewModel(@Nullable Editor editor) {
+ return new JsonStructureViewModel(psiFile, editor);
+ }
+ };
+ }
+}
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewElement.java b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java
new file mode 100644
index 00000000..df705efb
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java
@@ -0,0 +1,86 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.TreeElement;
+import com.intellij.json.psi.*;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewElement implements StructureViewTreeElement {
+ private final JsonElement myElement;
+
+ public JsonStructureViewElement(@NotNull JsonElement element) {
+ assert PsiTreeUtil.instanceOf(element, JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class);
+ myElement = element;
+ }
+
+ @Override
+ public JsonElement getValue() {
+ return myElement;
+ }
+
+ @Override
+ public void navigate(boolean requestFocus) {
+ myElement.navigate(requestFocus);
+ }
+
+ @Override
+ public boolean canNavigate() {
+ return myElement.canNavigate();
+ }
+
+ @Override
+ public boolean canNavigateToSource() {
+ return myElement.canNavigateToSource();
+ }
+
+ @NotNull
+ @Override
+ public ItemPresentation getPresentation() {
+ final ItemPresentation presentation = myElement.getPresentation();
+ assert presentation != null;
+ return presentation;
+ }
+
+ @NotNull
+ @Override
+ public TreeElement[] getChildren() {
+ JsonElement value = null;
+ if (myElement instanceof JsonFile) {
+ value = ((JsonFile)myElement).getTopLevelValue();
+ }
+ else if (myElement instanceof JsonProperty) {
+ value = ((JsonProperty)myElement).getValue();
+ }
+ else if (PsiTreeUtil.instanceOf(myElement, JsonObject.class, JsonArray.class)) {
+ value = myElement;
+ }
+ if (value instanceof JsonObject) {
+ final JsonObject object = ((JsonObject)value);
+ return ContainerUtil.map2Array(object.getPropertyList(), TreeElement.class, (Function<JsonProperty, TreeElement>)property -> new JsonStructureViewElement(property));
+ }
+ else if (value instanceof JsonArray) {
+ final JsonArray array = (JsonArray)value;
+ final List<TreeElement> childObjects = ContainerUtil.mapNotNull(array.getValueList(), value1 -> {
+ if (value1 instanceof JsonObject && !((JsonObject)value1).getPropertyList().isEmpty()) {
+ return new JsonStructureViewElement(value1);
+ }
+ else if (value1 instanceof JsonArray && PsiTreeUtil.findChildOfType(value1, JsonProperty.class) != null) {
+ return new JsonStructureViewElement(value1);
+ }
+ return null;
+ });
+ return ArrayUtil.toObjectArray(childObjects, TreeElement.class);
+ }
+ return EMPTY_ARRAY;
+ }
+}
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewModel.java b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java
new file mode 100644
index 00000000..a3bcf62b
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java
@@ -0,0 +1,37 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.StructureViewModelBase;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.Sorter;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewModel extends StructureViewModelBase implements StructureViewModel.ElementInfoProvider {
+
+ public JsonStructureViewModel(@NotNull PsiFile psiFile, @Nullable Editor editor) {
+ super(psiFile, editor, new JsonStructureViewElement((JsonFile)psiFile));
+ withSuitableClasses(JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class);
+ withSorters(Sorter.ALPHA_SORTER);
+ }
+
+ @Override
+ public boolean isAlwaysShowsPlus(StructureViewTreeElement element) {
+ return false;
+ }
+
+ @Override
+ public boolean isAlwaysLeaf(StructureViewTreeElement element) {
+ return false;
+ }
+
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java
new file mode 100644
index 00000000..60f32e84
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java
@@ -0,0 +1,84 @@
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.surroundWith.SurroundDescriptor;
+import com.intellij.lang.surroundWith.Surrounder;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSurroundDescriptor implements SurroundDescriptor {
+ private static final Surrounder[] ourSurrounders = new Surrounder[]{
+ new JsonWithObjectLiteralSurrounder(),
+ new JsonWithArrayLiteralSurrounder(),
+ new JsonWithQuotesSurrounder()
+ };
+
+ @NotNull
+ @Override
+ public PsiElement[] getElementsToSurround(PsiFile file, int startOffset, int endOffset) {
+ PsiElement firstElement = file.findElementAt(startOffset);
+ PsiElement lastElement = file.findElementAt(endOffset - 1);
+
+ // Extend selection beyond possible delimiters
+ while (firstElement != null &&
+ (firstElement instanceof PsiWhiteSpace || firstElement.getNode().getElementType() == JsonElementTypes.COMMA)) {
+ firstElement = firstElement.getNextSibling();
+ }
+ while (lastElement != null &&
+ (lastElement instanceof PsiWhiteSpace || lastElement.getNode().getElementType() == JsonElementTypes.COMMA)) {
+ lastElement = lastElement.getPrevSibling();
+ }
+ if (firstElement != null) {
+ startOffset = firstElement.getTextRange().getStartOffset();
+ }
+ if (lastElement != null) {
+ endOffset = lastElement.getTextRange().getEndOffset();
+ }
+
+ final JsonProperty property = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonProperty.class);
+ if (property != null) {
+ return collectElements(endOffset, property, JsonProperty.class);
+ }
+
+ final JsonValue value = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonValue.class);
+ if (value != null) {
+ return collectElements(endOffset, value, JsonValue.class);
+ }
+ return PsiElement.EMPTY_ARRAY;
+ }
+
+ @NotNull
+ private static <T extends PsiElement> PsiElement[] collectElements(int endOffset, @NotNull T property, @NotNull Class<T> kind) {
+ final List<T> properties = ContainerUtil.newArrayList(property);
+ PsiElement nextSibling = property.getNextSibling();
+ while (nextSibling != null && nextSibling.getTextRange().getEndOffset() <= endOffset) {
+ if (kind.isInstance(nextSibling)) {
+ properties.add(kind.cast(nextSibling));
+ }
+ nextSibling = nextSibling.getNextSibling();
+ }
+ return properties.toArray(PsiElement.EMPTY_ARRAY);
+ }
+
+ @NotNull
+ @Override
+ public Surrounder[] getSurrounders() {
+ return ourSurrounders;
+ }
+
+ @Override
+ public boolean isExclusive() {
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java
new file mode 100644
index 00000000..f4321e71
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java
@@ -0,0 +1,57 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.psi.JsonElementGenerator;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.surroundWith.Surrounder;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class JsonSurrounderBase implements Surrounder {
+ @Override
+ public boolean isApplicable(@NotNull PsiElement[] elements) {
+ return elements.length >= 1 && elements[0] instanceof JsonValue && !JsonPsiUtil.isPropertyKey(elements[0]);
+ }
+
+ @Nullable
+ @Override
+ public TextRange surroundElements(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement[] elements)
+ throws IncorrectOperationException {
+ if (!isApplicable(elements)) {
+ return null;
+ }
+
+ final JsonElementGenerator generator = new JsonElementGenerator(project);
+
+ if (elements.length == 1) {
+ JsonValue replacement = generator.createValue(createReplacementText(elements[0].getText()));
+ elements[0].replace(replacement);
+ }
+ else {
+ final String propertiesText = getTextAndRemoveMisc(elements[0], elements[elements.length - 1]);
+ JsonValue replacement = generator.createValue(createReplacementText(propertiesText));
+ elements[0].replace(replacement);
+ }
+ return null;
+ }
+
+ @NotNull
+ protected static String getTextAndRemoveMisc(@NotNull PsiElement firstProperty, @NotNull PsiElement lastProperty) {
+ final TextRange replacedRange = new TextRange(firstProperty.getTextOffset(), lastProperty.getTextRange().getEndOffset());
+ final String propertiesText = replacedRange.substring(firstProperty.getContainingFile().getText());
+ if (firstProperty != lastProperty) {
+ final PsiElement parent = firstProperty.getParent();
+ parent.deleteChildRange(firstProperty.getNextSibling(), lastProperty);
+ }
+ return propertiesText;
+ }
+
+ @NotNull
+ protected abstract String createReplacementText(@NotNull String textInRange);
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java
new file mode 100644
index 00000000..7f6091f3
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java
@@ -0,0 +1,18 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonWithArrayLiteralSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.array.literal.desc");
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String firstElement) {
+ return "[" + firstElement + "]";
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java
new file mode 100644
index 00000000..40182965
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java
@@ -0,0 +1,85 @@
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This surrounder ported from JavaScript allows to wrap single JSON value or several consecutive JSON properties
+ * in object literal.
+ * <p/>
+ * Examples:
+ * <ol>
+ * <li>{@code [42]} converts to {@code [{"property": 42}]}</li>
+ * <li><pre>
+ * {
+ * "foo": 42,
+ * "bar": false
+ * }
+ * </pre> converts to <pre>
+ * {
+ * "property": {
+ * "foo": 42,
+ * "bar": false
+ * }
+ * }
+ * </pre></li>
+ * </ol>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonWithObjectLiteralSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.object.literal.desc");
+ }
+
+ @Override
+ public boolean isApplicable(@NotNull PsiElement[] elements) {
+ return !JsonPsiUtil.isPropertyKey(elements[0]) && (elements[0] instanceof JsonProperty || elements.length == 1);
+ }
+
+ @Nullable
+ @Override
+ public TextRange surroundElements(@NotNull Project project,
+ @NotNull Editor editor,
+ @NotNull PsiElement[] elements) throws IncorrectOperationException {
+
+ if (!isApplicable(elements)) {
+ return null;
+ }
+
+ final JsonElementGenerator generator = new JsonElementGenerator(project);
+
+ final PsiElement firstElement = elements[0];
+ final JsonElement newNameElement;
+ if (firstElement instanceof JsonValue) {
+ assert elements.length == 1 : "Only single JSON value can be wrapped in object literal";
+ JsonObject replacement = generator.createValue(createReplacementText(firstElement.getText()));
+ replacement = (JsonObject)firstElement.replace(replacement);
+ newNameElement = replacement.getPropertyList().get(0).getNameElement();
+ }
+ else {
+ assert firstElement instanceof JsonProperty;
+ final String propertiesText = getTextAndRemoveMisc(firstElement, elements[elements.length - 1]);
+ final JsonObject tempJsonObject = generator.createValue(createReplacementText("{\n" + propertiesText) + "\n}");
+ JsonProperty replacement = tempJsonObject.getPropertyList().get(0);
+ replacement = (JsonProperty)firstElement.replace(replacement);
+ newNameElement = replacement.getNameElement();
+ }
+ final TextRange rangeWithQuotes = newNameElement.getTextRange();
+ return new TextRange(rangeWithQuotes.getStartOffset() + 1, rangeWithQuotes.getEndOffset() - 1);
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String textInRange) {
+ return "{\n\"property\": " + textInRange + "\n}";
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java
new file mode 100644
index 00000000..665090f6
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonWithQuotesSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.quotes.desc");
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String firstElement) {
+ return "\"" + StringUtil.escapeStringCharacters(firstElement) + "\"";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java
new file mode 100644
index 00000000..abcb943e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java
@@ -0,0 +1,41 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.util.text.StringUtil;
+
+import javax.swing.*;
+
+public enum JsonMappingKind {
+ File,
+ Pattern,
+ Directory;
+
+ public String getDescription() {
+ switch (this) {
+ case File:
+ return "file";
+ case Pattern:
+ return "file path pattern";
+ case Directory:
+ return "directory";
+ }
+ return "";
+ }
+
+ public String getPrefix() {
+ return StringUtil.capitalize(getDescription()) + ": ";
+ }
+
+ public Icon getIcon() {
+ switch (this) {
+ case File:
+ return AllIcons.FileTypes.Any_type;
+ case Pattern:
+ return AllIcons.FileTypes.Unknown;
+ case Directory:
+ return AllIcons.Nodes.Folder;
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java
new file mode 100644
index 00000000..e2c87367
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java
@@ -0,0 +1,60 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonPointerResolver {
+ private final JsonValue myRoot;
+ private final String myPointer;
+
+ public JsonPointerResolver(@NotNull JsonValue root, @NotNull String pointer) {
+ myRoot = root;
+ myPointer = pointer;
+ }
+
+ @Nullable
+ public JsonValue resolve() {
+ JsonValue root = myRoot;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonSchemaVariantsTreeBuilder.buildSteps(myPointer);
+ for (JsonSchemaVariantsTreeBuilder.Step step : steps) {
+ String name = step.getName();
+ if (name != null) {
+ if (!(root instanceof JsonObject)) return null;
+ JsonProperty property = ((JsonObject)root).findProperty(name);
+ root = property == null ? null : property.getValue();
+ }
+ else {
+ int idx = step.getIdx();
+ if (idx < 0) return null;
+
+ if (!(root instanceof JsonArray)) {
+ if (root instanceof JsonObject) {
+ JsonProperty property = ((JsonObject)root).findProperty(String.valueOf(idx));
+ if (property == null) {
+ return null;
+ }
+ root = property.getValue();
+ continue;
+ }
+ else {
+ return null;
+ }
+ }
+ List<JsonValue> list = ((JsonArray)root).getValueList();
+ if (idx >= list.size()) return null;
+ root = list.get(idx);
+ }
+ }
+ return root;
+ }
+
+
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java
new file mode 100644
index 00000000..2cb1c37c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java
@@ -0,0 +1,38 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonPointerUtil {
+ @NotNull
+ public static String escapeForJsonPointer(@NotNull String name) {
+ if (StringUtil.isEmptyOrSpaces(name)) {
+ return URLUtil.encodeURIComponent(name);
+ }
+ return StringUtil.replace(StringUtil.replace(name, "~", "~0"), "/", "~1");
+ }
+
+ @NotNull
+ public static String unescapeJsonPointerPart(@NotNull String part) {
+ part = URLUtil.unescapePercentSequences(part);
+ return StringUtil.replace(StringUtil.replace(part, "~0", "~"), "~1", "/");
+ }
+
+ public static boolean isSelfReference(@Nullable String ref) {
+ return "#".equals(ref) || "#/".equals(ref) || StringUtil.isEmpty(ref);
+ }
+
+ public static List<String> split(@NotNull String pointer) {
+ return StringUtil.split(pointer, "/", true, false);
+ }
+
+ @NotNull
+ public static String normalizeSlashes(@NotNull String ref) {
+ return StringUtil.trimStart(ref.replace('\\', '/'), "/");
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java
new file mode 100644
index 00000000..96b838fe
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java
@@ -0,0 +1,110 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
+import com.intellij.ui.components.JBCheckBox;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.util.ui.FormBuilder;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class JsonSchemaCatalogConfigurable implements Configurable {
+ @NonNls public static final String SETTINGS_JSON_SCHEMA_CATALOG = "settings.json.schema.catalog";
+ public static final String JSON_SCHEMA_CATALOG = "Remote JSON Schemas";
+ @NotNull private final Project myProject;
+ private final JBCheckBox myCatalogCheckBox;
+ private final JBCheckBox myRemoteCheckBox;
+ private final JBCheckBox myPreferRemoteCheckBox;
+
+ public JsonSchemaCatalogConfigurable(@NotNull final Project project) {
+ myProject = project;
+ myCatalogCheckBox = new JBCheckBox("Use schemastore.org JSON Schema catalog");
+ myRemoteCheckBox = new JBCheckBox("Allow downloading JSON Schemas from remote sources");
+ myPreferRemoteCheckBox = new JBCheckBox("Always download the most recent version of schemas");
+ }
+
+ @Nullable
+ @Override
+ public JComponent createComponent() {
+ FormBuilder builder = FormBuilder.createFormBuilder();
+
+ builder.addComponent(myRemoteCheckBox);
+ builder.addVerticalGap(2);
+ myRemoteCheckBox.addChangeListener(c -> {
+ boolean selected = myRemoteCheckBox.isSelected();
+ myCatalogCheckBox.setEnabled(selected);
+ myPreferRemoteCheckBox.setEnabled(selected);
+ if (!selected) {
+ myCatalogCheckBox.setSelected(false);
+ myPreferRemoteCheckBox.setSelected(false);
+ }
+ });
+ addWithComment(builder, myCatalogCheckBox,
+ "Schemas will be downloaded and assigned using the <a href=\"http://schemastore.org/json/\">SchemaStore API</a>");
+ addWithComment(builder, myPreferRemoteCheckBox,
+ "Schemas will always be downloaded from the SchemaStore, even if some of them are bundled with the IDE");
+ return wrap(builder.getPanel());
+ }
+
+ private static void addWithComment(FormBuilder builder, JBCheckBox box, String s) {
+ builder.addComponent(new ComponentPanelBuilder(box).withComment(s).createPanel());
+ }
+
+ private static JPanel wrap(JComponent panel) {
+ JPanel wrapper = new JBPanel(new BorderLayout());
+ wrapper.add(panel, BorderLayout.NORTH);
+ return wrapper;
+ }
+
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myCatalogCheckBox;
+ }
+
+ @Override
+ public void reset() {
+ JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState();
+ final boolean remoteEnabled = state == null || state.myIsRemoteActivityEnabled;
+ myRemoteCheckBox.setSelected(remoteEnabled);
+ myCatalogCheckBox.setEnabled(remoteEnabled);
+ myPreferRemoteCheckBox.setEnabled(remoteEnabled);
+ myCatalogCheckBox.setSelected(state == null || state.myIsCatalogEnabled);
+ myPreferRemoteCheckBox.setSelected(state == null || state.myIsPreferRemoteSchemas);
+ }
+
+ @Override
+ public boolean isModified() {
+ JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState();
+ return state == null
+ || state.myIsCatalogEnabled != myCatalogCheckBox.isSelected()
+ || state.myIsPreferRemoteSchemas != myPreferRemoteCheckBox.isSelected()
+ || state.myIsRemoteActivityEnabled != myRemoteCheckBox.isSelected();
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ JsonSchemaCatalogProjectConfiguration.getInstance(myProject).setState(myCatalogCheckBox.isSelected(),
+ myRemoteCheckBox.isSelected(),
+ myPreferRemoteCheckBox.isSelected());
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Title)
+ @Override
+ public String getDisplayName() {
+ return JSON_SCHEMA_CATALOG;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return SETTINGS_JSON_SCHEMA_CATALOG;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java
new file mode 100644
index 00000000..2a2c3abf
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java
@@ -0,0 +1,86 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.containers.ConcurrentList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.xmlb.annotations.Tag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@State(name = "JsonSchemaCatalogProjectConfiguration", storages = @Storage("jsonCatalog.xml"))
+public class JsonSchemaCatalogProjectConfiguration implements PersistentStateComponent<JsonSchemaCatalogProjectConfiguration.MyState> {
+ public volatile MyState myState = new MyState();
+ private final ConcurrentList<Runnable> myChangeHandlers = ContainerUtil.createConcurrentList();
+
+ public boolean isCatalogEnabled() {
+ MyState state = getState();
+ return state != null && state.myIsCatalogEnabled;
+ }
+
+ public boolean isPreferRemoteSchemas() {
+ MyState state = getState();
+ return state != null && state.myIsPreferRemoteSchemas;
+ }
+
+ public void addChangeHandler(Runnable runnable) {
+ myChangeHandlers.add(runnable);
+ }
+
+ public static JsonSchemaCatalogProjectConfiguration getInstance(@NotNull final Project project) {
+ return ServiceManager.getService(project, JsonSchemaCatalogProjectConfiguration.class);
+ }
+
+ public JsonSchemaCatalogProjectConfiguration() {
+ }
+
+ public void setState(boolean isEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) {
+ myState = new MyState(isEnabled, isRemoteActivityEnabled, isPreferRemoteSchemas);
+ for (Runnable handler : myChangeHandlers) {
+ handler.run();
+ }
+ }
+
+ @Nullable
+ @Override
+ public MyState getState() {
+ return myState;
+ }
+
+ public boolean isRemoteActivityEnabled() {
+ MyState state = getState();
+ return state != null && state.myIsRemoteActivityEnabled;
+ }
+
+ @Override
+ public void loadState(@NotNull MyState state) {
+ myState = state;
+ for (Runnable handler : myChangeHandlers) {
+ handler.run();
+ }
+ }
+
+ static class MyState {
+ @Tag("enabled")
+ public boolean myIsCatalogEnabled = true;
+
+ @Tag("remoteActivityEnabled")
+ public boolean myIsRemoteActivityEnabled = true;
+
+ @Tag("preferRemoteSchemas")
+ public boolean myIsPreferRemoteSchemas = false;
+
+ MyState() {
+ }
+
+ MyState(boolean isCatalogEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) {
+ myIsCatalogEnabled = isCatalogEnabled;
+ myIsRemoteActivityEnabled = isRemoteActivityEnabled;
+ myIsPreferRemoteSchemas = isPreferRemoteSchemas;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java
new file mode 100644
index 00000000..48d49167
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * To make plugin github.com/BlueBoxWare/LibGDXPlugin happy
+ * @author Irina.Chernushina on 4/1/2016.
+ */
+public class JsonSchemaFileType extends LanguageFileType implements FileTypeIdentifiableByVirtualFile {
+ public static final JsonSchemaFileType INSTANCE = new JsonSchemaFileType();
+
+ public JsonSchemaFileType() {
+ super(JsonLanguage.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JSON Schema";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JSON Schema";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return "json";
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ return AllIcons.FileTypes.JsonSchema;
+ }
+
+ @Override
+ public boolean isMyFileType(@NotNull VirtualFile file) {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java
new file mode 100644
index 00000000..78014791
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.IconProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * @author Irina.Chernushina on 5/23/2017.
+ */
+public class JsonSchemaIconProvider extends IconProvider {
+ @Nullable
+ @Override
+ public Icon getIcon(@NotNull PsiElement element, int flags) {
+ if (element instanceof PsiFile && JsonSchemaService.isSchemaFile((PsiFile)element)) {
+ return AllIcons.FileTypes.JsonSchema;
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java
new file mode 100644
index 00000000..ace33637
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Tag;
+import com.intellij.util.xmlb.annotations.XCollection;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.*;
+
+@State(name = "JsonSchemaMappingsProjectConfiguration", storages = @Storage("jsonSchemas.xml"))
+public class JsonSchemaMappingsProjectConfiguration implements PersistentStateComponent<JsonSchemaMappingsProjectConfiguration.MyState> {
+ @NotNull private final Project myProject;
+ public volatile MyState myState = new MyState();
+
+ @Nullable
+ public UserDefinedJsonSchemaConfiguration findMappingBySchemaInfo(JsonSchemaInfo value) {
+ for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) {
+ if (areSimilar(value, configuration)) return configuration;
+ }
+ return null;
+ }
+
+ public boolean areSimilar(JsonSchemaInfo value, UserDefinedJsonSchemaConfiguration configuration) {
+ return Objects.equals(normalizePath(value.getUrl(myProject)), normalizePath(configuration.getRelativePathToSchema()));
+ }
+
+ @Nullable
+ @Contract("null -> null; !null -> !null")
+ public String normalizePath(@Nullable String valueUrl) {
+ if (valueUrl == null) return null;
+ if (StringUtil.contains(valueUrl, "..")) {
+ valueUrl = new File(valueUrl).getAbsolutePath();
+ }
+ return valueUrl.replace('\\', '/');
+ }
+
+ @Nullable
+ public UserDefinedJsonSchemaConfiguration findMappingForFile(VirtualFile file) {
+ VirtualFile projectBaseDir = myProject.getBaseDir();
+ for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) {
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : configuration.patterns) {
+ if (pattern.mappingKind != JsonMappingKind.File) continue;
+ VirtualFile relativeFile = VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts());
+ if (Objects.equals(relativeFile, file) || file.getUrl().equals(pattern.getPath())) {
+ return configuration;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static JsonSchemaMappingsProjectConfiguration getInstance(@NotNull final Project project) {
+ return ServiceManager.getService(project, JsonSchemaMappingsProjectConfiguration.class);
+ }
+
+ public JsonSchemaMappingsProjectConfiguration(@NotNull Project project) {
+ myProject = project;
+ }
+
+ @Nullable
+ @Override
+ public MyState getState() {
+ return myState;
+ }
+
+ public void schemaFileMoved(@NotNull final Project project,
+ @NotNull final String oldRelativePath,
+ @NotNull final String newRelativePath) {
+ final Optional<UserDefinedJsonSchemaConfiguration> old = myState.myState.values().stream()
+ .filter(schema -> FileUtil.pathsEqual(schema.getRelativePathToSchema(), oldRelativePath))
+ .findFirst();
+ old.ifPresent(configuration -> {
+ configuration.setRelativePathToSchema(newRelativePath);
+ JsonSchemaService.Impl.get(project).reset();
+ });
+ }
+
+ public void removeConfiguration(UserDefinedJsonSchemaConfiguration configuration) {
+ for (Map.Entry<String, UserDefinedJsonSchemaConfiguration> entry : myState.myState.entrySet()) {
+ if (entry.getValue() == configuration) {
+ myState.myState.remove(entry.getKey());
+ return;
+ }
+ }
+ }
+
+ public void addConfiguration(UserDefinedJsonSchemaConfiguration configuration) {
+ String name = configuration.getName();
+ while (myState.myState.containsKey(name)) {
+ name += "1";
+ }
+ myState.myState.put(name, configuration);
+ }
+
+ public Map<String, UserDefinedJsonSchemaConfiguration> getStateMap() {
+ return Collections.unmodifiableMap(myState.myState);
+ }
+
+ @Override
+ public void loadState(@NotNull MyState state) {
+ myState = state;
+ JsonSchemaService.Impl.get(myProject).reset();
+ }
+
+ public void setState(@NotNull Map<String, UserDefinedJsonSchemaConfiguration> state) {
+ myState = new MyState(state);
+ }
+
+ static class MyState {
+ @Tag("state")
+ @XCollection
+ public Map<String, UserDefinedJsonSchemaConfiguration> myState = new TreeMap<>();
+
+ MyState() {
+ }
+
+ MyState(Map<String, UserDefinedJsonSchemaConfiguration> state) {
+ myState = state;
+ }
+ }
+} \ No newline at end of file
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java
new file mode 100644
index 00000000..5e4ecda6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilBase;
+import com.intellij.refactoring.listeners.RefactoringElementListener;
+import com.intellij.refactoring.listeners.RefactoringElementListenerProvider;
+import com.intellij.refactoring.listeners.UndoRefactoringElementAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/17/2016.
+ */
+public class JsonSchemaRefactoringListenerProvider implements RefactoringElementListenerProvider {
+ @Nullable
+ @Override
+ public RefactoringElementListener getListener(PsiElement element) {
+ if (element == null) {
+ return null;
+ }
+ final VirtualFile oldFile = PsiUtilBase.asVirtualFile(element);
+ if (oldFile == null || !(oldFile.getFileType() instanceof LanguageFileType) ||
+ !(((LanguageFileType)oldFile.getFileType()).getLanguage().isKindOf(JsonLanguage.INSTANCE))) {
+ return null;
+ }
+ final Project project = element.getProject();
+ if (project.getBaseDir() == null) return null;
+
+ final String oldRelativePath = VfsUtilCore.getRelativePath(oldFile, project.getBaseDir());
+ if (oldRelativePath != null) {
+ final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+ return new UndoRefactoringElementAdapter() {
+ @Override
+ protected void refactored(@NotNull PsiElement element, @Nullable String oldQualifiedName) {
+ final VirtualFile newFile = PsiUtilBase.asVirtualFile(element);
+ if (newFile != null) {
+ final String newRelativePath = VfsUtilCore.getRelativePath(newFile, project.getBaseDir());
+ if (newRelativePath != null) {
+ configuration.schemaFileMoved(project, oldRelativePath, newRelativePath);
+ }
+ }
+ }
+ };
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java
new file mode 100644
index 00000000..b778efae
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java
@@ -0,0 +1,115 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ZipperUpdater;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileContentsChangedAdapter;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.BulkVirtualFileListenerAdapter;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiTreeAnyChangeAbstractAdapter;
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.SequentialTaskExecutor;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.messages.MessageBusConnection;
+import com.intellij.util.messages.Topic;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * @author Irina.Chernushina on 3/30/2016.
+ */
+public class JsonSchemaVfsListener extends BulkVirtualFileListenerAdapter {
+ public static final Topic<Runnable> JSON_SCHEMA_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Schema.Changed", Runnable.class);
+ public static final Topic<Runnable> JSON_DEPS_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Deps.Changed", Runnable.class);
+
+ public static void startListening(@NotNull Project project, @NotNull JsonSchemaService service, @NotNull MessageBusConnection connection) {
+ final MyUpdater updater = new MyUpdater(project, service);
+ connection.subscribe(VirtualFileManager.VFS_CHANGES, new JsonSchemaVfsListener(updater));
+ PsiManager.getInstance(project).addPsiTreeChangeListener(new PsiTreeAnyChangeAbstractAdapter() {
+ @Override
+ protected void onChange(@Nullable PsiFile file) {
+ if (file != null) updater.onFileChange(file.getViewProvider().getVirtualFile());
+ }
+ });
+ }
+
+ private JsonSchemaVfsListener(@NotNull MyUpdater updater) {
+ super(new VirtualFileContentsChangedAdapter() {
+ @NotNull private final MyUpdater myUpdater = updater;
+ @Override
+ protected void onFileChange(@NotNull final VirtualFile schemaFile) {
+ myUpdater.onFileChange(schemaFile);
+ }
+
+ @Override
+ protected void onBeforeFileChange(@NotNull VirtualFile schemaFile) {
+ myUpdater.onFileChange(schemaFile);
+ }
+ });
+ }
+
+ private static class MyUpdater {
+ @NotNull private final Project myProject;
+ private final ZipperUpdater myUpdater;
+ @NotNull private final JsonSchemaService myService;
+ private final Set<VirtualFile> myDirtySchemas = ContainerUtil.newConcurrentSet();
+ private final Runnable myRunnable;
+ private final ExecutorService myTaskExecutor = SequentialTaskExecutor.createSequentialApplicationPoolExecutor(
+ "JsonVfsUpdaterExecutor");
+
+ protected MyUpdater(@NotNull Project project, @NotNull JsonSchemaService service) {
+ myProject = project;
+ myUpdater = new ZipperUpdater(200, Alarm.ThreadToUse.POOLED_THREAD, project);
+ myService = service;
+ myRunnable = () -> {
+ if (myProject.isDisposed()) return;
+ Collection<VirtualFile> scope = new HashSet<>(myDirtySchemas);
+ if (scope.stream().anyMatch(f -> service.possiblyHasReference(f.getName()))) {
+ myProject.getMessageBus().syncPublisher(JSON_DEPS_CHANGED).run();
+ }
+ myDirtySchemas.removeAll(scope);
+ if (scope.isEmpty()) return;
+
+ Collection<VirtualFile> finalScope = ContainerUtil.filter(scope, file -> myService.isApplicableToFile(file)
+ && ((JsonSchemaServiceImpl)myService).isMappedSchema(file, false));
+ if (finalScope.isEmpty()) return;
+ if (myProject.isDisposed()) return;
+ myProject.getMessageBus().syncPublisher(JSON_SCHEMA_CHANGED).run();
+
+ final DaemonCodeAnalyzer analyzer = DaemonCodeAnalyzer.getInstance(project);
+ final PsiManager psiManager = PsiManager.getInstance(project);
+ final Editor[] editors = EditorFactory.getInstance().getAllEditors();
+ Arrays.stream(editors).filter(editor -> editor instanceof EditorEx)
+ .map(editor -> ((EditorEx)editor).getVirtualFile())
+ .filter(file -> file != null && file.isValid())
+ .forEach(file -> {
+ final Collection<VirtualFile> schemaFiles = ((JsonSchemaServiceImpl)myService).getSchemasForFile(file, false, true);
+ if (schemaFiles.stream().anyMatch(finalScope::contains)) {
+ ReadAction.nonBlocking(() -> Optional.ofNullable(psiManager.findFile(file)).ifPresent(analyzer::restart)).submit(myTaskExecutor);
+ }
+ });
+ };
+ }
+
+ protected void onFileChange(@NotNull final VirtualFile schemaFile) {
+ if (JsonFileType.DEFAULT_EXTENSION.equals(schemaFile.getExtension())) {
+ myDirtySchemas.add(schemaFile);
+ myUpdater.queue(myRunnable);
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java
new file mode 100644
index 00000000..8ab70270
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.AtomicClearableLazyValue;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.PairProcessor;
+import com.intellij.util.PatternUtil;
+import com.intellij.util.SmartList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.xmlb.annotations.Tag;
+import com.intellij.util.xmlb.annotations.Transient;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * @author Irina.Chernushina on 4/19/2017.
+ */
+@Tag("SchemaInfo")
+public class UserDefinedJsonSchemaConfiguration {
+ private final static Comparator<Item> ITEM_COMPARATOR = (o1, o2) -> {
+ if (o1.isPattern() != o2.isPattern()) return o1.isPattern() ? -1 : 1;
+ if (o1.isDirectory() != o2.isDirectory()) return o1.isDirectory() ? -1 : 1;
+ return o1.path.compareToIgnoreCase(o2.path);
+ };
+
+ public String name;
+ public String relativePathToSchema;
+ public JsonSchemaVersion schemaVersion = JsonSchemaVersion.SCHEMA_4;
+ public boolean applicationDefined;
+ public List<Item> patterns = new SmartList<>();
+ @Transient
+ private final AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>> myCalculatedPatterns =
+ new AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>>() {
+ @NotNull
+ @Override
+ protected List<PairProcessor<Project, VirtualFile>> compute() {
+ return recalculatePatterns();
+ }
+ };
+
+ public UserDefinedJsonSchemaConfiguration() {
+ }
+
+ public UserDefinedJsonSchemaConfiguration(@NotNull String name,
+ JsonSchemaVersion schemaVersion,
+ @NotNull String relativePathToSchema,
+ boolean applicationDefined,
+ @Nullable List<Item> patterns) {
+ this.name = name;
+ this.relativePathToSchema = relativePathToSchema;
+ this.schemaVersion = schemaVersion;
+ this.applicationDefined = applicationDefined;
+ setPatterns(patterns);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(@NotNull String name) {
+ this.name = name;
+ }
+
+ public String getRelativePathToSchema() {
+ return relativePathToSchema;
+ }
+
+ public JsonSchemaVersion getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public void setSchemaVersion(JsonSchemaVersion schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ }
+
+ public void setRelativePathToSchema(String relativePathToSchema) {
+ this.relativePathToSchema = relativePathToSchema;
+ }
+
+ public boolean isApplicationDefined() {
+ return applicationDefined;
+ }
+
+ public void setApplicationDefined(boolean applicationDefined) {
+ this.applicationDefined = applicationDefined;
+ }
+
+ public List<Item> getPatterns() {
+ return patterns;
+ }
+
+ public void setPatterns(@Nullable List<Item> patterns) {
+ this.patterns.clear();
+ if (patterns != null) this.patterns.addAll(patterns);
+ Collections.sort(this.patterns, ITEM_COMPARATOR);
+ myCalculatedPatterns.drop();
+ }
+
+ public void refreshPatterns() {
+ myCalculatedPatterns.drop();
+ }
+
+ @NotNull
+ public List<PairProcessor<Project, VirtualFile>> getCalculatedPatterns() {
+ return myCalculatedPatterns.getValue();
+ }
+
+ private List<PairProcessor<Project, VirtualFile>> recalculatePatterns() {
+ final List<PairProcessor<Project, VirtualFile>> result = new SmartList<>();
+ for (final Item patternText : patterns) {
+ switch (patternText.mappingKind) {
+ case File:
+ result.add((project, vfile) -> vfile.equals(getRelativeFile(project, patternText)) || vfile.getUrl().equals(patternText.getPath()));
+ break;
+ case Pattern:
+ String pathText = patternText.getPath().replace(File.separatorChar, '/').replace('\\', '/');
+ final Pattern pattern = pathText.isEmpty()
+ ? PatternUtil.NOTHING
+ : pathText.indexOf('/') >= 0
+ ? PatternUtil.compileSafe(".*/" + PatternUtil.convertToRegex(pathText), PatternUtil.NOTHING)
+ : PatternUtil.fromMask(pathText);
+ result.add((project, file) -> JsonSchemaObject.matchPattern(pattern, pathText.indexOf('/') >= 0
+ ? file.getPath()
+ : file.getName()));
+ break;
+ case Directory:
+ result.add((project, vfile) -> {
+ final VirtualFile relativeFile = getRelativeFile(project, patternText);
+ if (relativeFile == null || !VfsUtilCore.isAncestor(relativeFile, vfile, true)) return false;
+ JsonSchemaService service = JsonSchemaService.Impl.get(project);
+ return service.isApplicableToFile(vfile);
+ });
+ break;
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private static VirtualFile getRelativeFile(@NotNull final Project project, @NotNull final Item pattern) {
+ if (project.getBasePath() == null) {
+ return null;
+ }
+
+ final String path = FileUtilRt.toSystemIndependentName(StringUtil.notNullize(pattern.path));
+ final List<String> parts = pathToPartsList(path);
+ if (parts.isEmpty()) {
+ return project.getBaseDir();
+ }
+ else {
+ return VfsUtil.findRelativeFile(project.getBaseDir(), ArrayUtil.toStringArray(parts));
+ }
+ }
+
+ @NotNull
+ private static List<String> pathToPartsList(@NotNull String path) {
+ return ContainerUtil.filter(StringUtil.split(path, "/"), s -> !".".equals(s));
+ }
+
+ @NotNull
+ private static String[] pathToParts(@NotNull String path) {
+ return ArrayUtil.toStringArray(pathToPartsList(path));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserDefinedJsonSchemaConfiguration info = (UserDefinedJsonSchemaConfiguration)o;
+
+ if (applicationDefined != info.applicationDefined) return false;
+ if (schemaVersion != info.schemaVersion) return false;
+ if (!Objects.equals(name, info.name)) return false;
+ if (!Objects.equals(relativePathToSchema, info.relativePathToSchema)) return false;
+
+ return Objects.equals(patterns, info.patterns);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (relativePathToSchema != null ? relativePathToSchema.hashCode() : 0);
+ result = 31 * result + (applicationDefined ? 1 : 0);
+ result = 31 * result + (patterns != null ? patterns.hashCode() : 0);
+ result = 31 * result + schemaVersion.hashCode();
+ return result;
+ }
+
+ public static class Item {
+ public String path;
+ public JsonMappingKind mappingKind = JsonMappingKind.File;
+
+ public Item() {
+ }
+
+ public Item(String path, JsonMappingKind mappingKind) {
+ this.path = neutralizePath(path);
+ this.mappingKind = mappingKind;
+ }
+
+ public Item(String path, boolean isPattern, boolean isDirectory) {
+ this.path = neutralizePath(path);
+ this.mappingKind = isPattern ? JsonMappingKind.Pattern : isDirectory ? JsonMappingKind.Directory : JsonMappingKind.File;
+ }
+
+ @NotNull
+ private static String normalizePath(String path) {
+ if (preserveSlashes(path)) return path;
+ return StringUtil.trimEnd(FileUtilRt.toSystemDependentName(path), File.separatorChar);
+ }
+
+ private static boolean preserveSlashes(String path) {
+ // http/https URLs to schemas
+ // mock URLs of fragments editor
+ return StringUtil.startsWith(path, "http:")
+ || StringUtil.startsWith(path, "https:")
+ || StringUtil.startsWith(path, "mock:");
+ }
+
+ @NotNull
+ private static String neutralizePath(String path) {
+ if (preserveSlashes(path)) return path;
+ return StringUtil.trimEnd(FileUtilRt.toSystemIndependentName(path), '/');
+ }
+
+ public String getPath() {
+ return normalizePath(path);
+ }
+
+ public void setPath(String path) {
+ this.path = neutralizePath(path);
+ }
+
+ public String getError() {
+ switch (mappingKind) {
+ case File:
+ return !StringUtil.isEmpty(path) ? null : "Empty file path doesn't match anything";
+ case Pattern:
+ return !StringUtil.isEmpty(path) ? null : "Empty pattern matches nothing";
+ case Directory:
+ return null;
+ }
+
+ return "Unknown mapping kind";
+ }
+
+ public boolean isPattern() {
+ return mappingKind == JsonMappingKind.Pattern;
+ }
+
+ public void setPattern(boolean pattern) {
+ mappingKind = pattern ? JsonMappingKind.Pattern : JsonMappingKind.File;
+ }
+
+ public boolean isDirectory() {
+ return mappingKind == JsonMappingKind.Directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ mappingKind = directory ? JsonMappingKind.Directory : JsonMappingKind.File;
+ }
+
+ public String getPresentation() {
+ if (mappingKind == JsonMappingKind.Directory && StringUtil.isEmpty(path)) {
+ return mappingKind.getPrefix() + "[Project Directory]";
+ }
+ return mappingKind.getPrefix() + getPath();
+ }
+
+ public String[] getPathParts() {
+ return pathToParts(path);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Item item = (Item)o;
+
+ if (mappingKind != item.mappingKind) return false;
+ return Objects.equals(path, item.path);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hashCode(path);
+ result = 31 * result + Objects.hashCode(mappingKind);
+ return result;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java
new file mode 100644
index 00000000..c805a06a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java
@@ -0,0 +1,81 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Irina.Chernushina on 2/15/2017.
+ */
+public interface JsonLikePsiWalker {
+ JsonOriginalPsiWalker JSON_ORIGINAL_PSI_WALKER = new JsonOriginalPsiWalker();
+
+ /**
+ * Returns YES in place where a property name is expected,
+ * NO in place where a property value is expected,
+ * UNSURE where both property name and property value can be present
+ */
+ ThreeState isName(PsiElement element);
+
+ boolean isPropertyWithValue(@NotNull PsiElement element);
+ PsiElement goUpToCheckable(@NotNull final PsiElement element);
+ @Nullable
+ List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull final PsiElement element, boolean forceLastTransition);
+ boolean isNameQuoted();
+ boolean onlyDoubleQuotesForStringLiterals();
+ default boolean quotesForStringLiterals() { return true; }
+ boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element);
+ Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition);
+ @Nullable
+ JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element);
+ boolean isTopJsonElement(@NotNull PsiElement element);
+ @Nullable
+ JsonValueAdapter createValueAdapter(@NotNull PsiElement element);
+
+ default TextRange adjustErrorHighlightingRange(@NotNull PsiElement element) {
+ return element.getTextRange();
+ }
+
+ @Nullable
+ static JsonLikePsiWalker getWalker(@NotNull final PsiElement element, JsonSchemaObject schemaObject) {
+ if (JSON_ORIGINAL_PSI_WALKER.handles(element)) return JSON_ORIGINAL_PSI_WALKER;
+
+ return JsonLikePsiWalkerFactory.EXTENSION_POINT_NAME.getExtensionList().stream()
+ .filter(extension -> extension.handles(element))
+ .findFirst()
+ .map(extension -> extension.create(schemaObject))
+ .orElse(null);
+ }
+
+ default String getDefaultObjectValue() { return "{}"; }
+ @Nullable default String defaultObjectValueDescription() { return null; }
+ default String getDefaultArrayValue() { return "[]"; }
+ @Nullable default String defaultArrayValueDescription() { return null; }
+
+ default boolean invokeEnterBeforeObjectAndArray() { return false; }
+
+ default String getNodeTextForValidation(PsiElement element) { return element.getText(); }
+
+ default QuickFixAdapter getQuickFixAdapter(Project project) { return null; }
+ interface QuickFixAdapter {
+ @Nullable PsiElement getPropertyValue(PsiElement property);
+ default @NotNull PsiElement adjustValue(@NotNull PsiElement value) { return value; }
+ @Nullable String getPropertyName(PsiElement property);
+ @NotNull PsiElement createProperty(@NotNull final String name, @NotNull final String value);
+ boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement);
+ void removeIfComma(PsiElement forward);
+ boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java
new file mode 100644
index 00000000..aa803d3b
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Irina.Chernushina on 3/7/2017.
+ */
+public interface JsonLikePsiWalkerFactory {
+ ExtensionPointName<JsonLikePsiWalkerFactory> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonLikePsiWalkerFactory");
+
+ boolean handles(@NotNull PsiElement element);
+
+ @NotNull
+ JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java
new file mode 100644
index 00000000..f0926b2e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java
@@ -0,0 +1,31 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * This API provides a mechanism to enable JSON schemas in particular files
+ * This interface should be implemented if you want a particular kind of virtual files to have access to JsonSchemaService APIs
+ *
+ * This API is new in IntelliJ IDEA Platform 2018.2
+ */
+public interface JsonSchemaEnabler {
+ ExtensionPointName<JsonSchemaEnabler> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaEnabler");
+
+ /**
+ * This method should return true if JSON schema mechanism should become applicable to corresponding file.
+ * This method SHOULD NOT ADDRESS INDEXES.
+ * @param file Virtual file to check for
+ * @return true if available, false otherwise
+ */
+ boolean isEnabledForFile(VirtualFile file);
+
+ /**
+ * This method enables/disables JSON schema selection widget
+ * This method SHOULD NOT ADDRESS INDEXES
+ */
+ default boolean shouldShowSwitcherWidget(VirtualFile file) {
+ return true;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java
new file mode 100644
index 00000000..9047afee
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java
@@ -0,0 +1,37 @@
+package com.jetbrains.jsonSchema.extension;
+
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface JsonSchemaFileProvider {
+ boolean isAvailable(@NotNull VirtualFile file);
+
+ @NotNull
+ String getName();
+
+ @Nullable
+ VirtualFile getSchemaFile();
+
+ @NotNull
+ SchemaType getSchemaType();
+
+ default JsonSchemaVersion getSchemaVersion() {
+ return JsonSchemaVersion.SCHEMA_4;
+ }
+
+ @Nullable
+ default String getThirdPartyApiInformation() {
+ return null;
+ }
+
+ default boolean isUserVisible() { return true; }
+
+ @NotNull
+ default String getPresentableName() { return getName(); }
+
+ @Nullable
+ default String getRemoteSource() { return null; }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java
new file mode 100644
index 00000000..e6bb993c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension;
+
+/**
+ * @author Irina.Chernushina on 2/15/2016.
+ */
+public interface JsonSchemaImportedProviderMarker {
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java
new file mode 100644
index 00000000..c05c5c5e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java
@@ -0,0 +1,132 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.impl.JsonSchemaType;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Set;
+
+public class JsonSchemaInfo {
+ @Nullable private final JsonSchemaFileProvider myProvider;
+ @Nullable private final String myUrl;
+ @NotNull private final static Set<String> myDumbNames = ContainerUtil.set(
+ "schema",
+ "lib",
+ "cli",
+ "packages",
+ "master",
+ "format",
+ "angular", // the only angular-related schema is the 'angular-cli', so we skip the repo name
+ "config");
+
+ public JsonSchemaInfo(@NotNull JsonSchemaFileProvider provider) {
+ myProvider = provider;
+ myUrl = null;
+ }
+
+ public JsonSchemaInfo(@NotNull String url) {
+ myUrl = url;
+ myProvider = null;
+ }
+
+ @Nullable
+ public JsonSchemaFileProvider getProvider() {
+ return myProvider;
+ }
+
+ @NotNull
+ public String getUrl(Project project) {
+ if (myProvider != null) {
+ String remoteSource = myProvider.getRemoteSource();
+ if (remoteSource != null) {
+ return remoteSource;
+ }
+
+ VirtualFile schemaFile = myProvider.getSchemaFile();
+ if (schemaFile == null) return "";
+
+ if (schemaFile instanceof HttpVirtualFile) {
+ return schemaFile.getUrl();
+ }
+
+ return getRelativePath(project, schemaFile.getPath());
+ }
+ else {
+ assert myUrl != null;
+ return myUrl;
+ }
+ }
+
+ @NotNull
+ public String getDescription() {
+ if (myProvider != null) {
+ String providerName = myProvider.getPresentableName();
+ return sanitizeName(providerName);
+ }
+
+ assert myUrl != null;
+
+ // the only weird case
+ if ("http://json.schemastore.org/config".equals(myUrl)
+ || "https://schemastore.azurewebsites.net/schemas/json/config.json".equals(myUrl)) {
+ return "asp.net config";
+ }
+
+ String url = myUrl.replace('\\', '/');
+
+ return ContainerUtil.reverse(StringUtil.split(url, "/"))
+ .stream()
+ .map(p -> sanitizeName(p))
+ .filter(p -> !isVeryDumbName(p))
+ .findFirst().orElse(sanitizeName(myUrl));
+ }
+
+ public static boolean isVeryDumbName(@Nullable String possibleName) {
+ if (StringUtil.isEmptyOrSpaces(possibleName) || myDumbNames.contains(possibleName)) return true;
+ return StringUtil.split(possibleName, ".").stream().allMatch(s -> JsonSchemaType.isInteger(s));
+ }
+
+ @NotNull
+ private static String sanitizeName(@NotNull String providerName) {
+ return StringUtil.trimEnd(StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema"), ".schema");
+ }
+
+ @NotNull
+ public JsonSchemaVersion getSchemaVersion() {
+ return myProvider != null ? myProvider.getSchemaVersion() : JsonSchemaVersion.SCHEMA_4;
+ }
+
+ @NotNull
+ public static String getRelativePath(@NotNull Project project, @NotNull String text) {
+ text = text.trim();
+ if (project.isDefault() || project.getBasePath() == null) return text;
+ if (StringUtil.isEmptyOrSpaces(text)) return text;
+ final File ioFile = new File(text);
+ if (!ioFile.isAbsolute()) return text;
+ VirtualFile file = VfsUtil.findFileByIoFile(ioFile, false);
+ if (file == null) return text;
+ final String relativePath = VfsUtilCore.getRelativePath(file, project.getBaseDir());
+ if (relativePath != null) return relativePath;
+ if (isMeaningfulAncestor(VfsUtilCore.getCommonAncestor(file, project.getBaseDir()))) {
+ String path = VfsUtilCore.findRelativePath(project.getBaseDir(), file, File.separatorChar);
+ if (path != null) return path;
+ }
+ return text;
+ }
+
+ private static boolean isMeaningfulAncestor(@Nullable VirtualFile ancestor) {
+ if (ancestor == null) return false;
+ VirtualFile homeDir = VfsUtil.getUserHomeDir();
+ return homeDir != null && VfsUtilCore.isAncestor(homeDir, ancestor, true);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java
new file mode 100644
index 00000000..829c8c50
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java
@@ -0,0 +1,125 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.NullableLazyValue;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import kotlin.NotImplementedError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/24/2016.
+ */
+public class JsonSchemaProjectSelfProviderFactory implements JsonSchemaProviderFactory {
+ public static final int TOTAL_PROVIDERS = 3;
+ private static final String SCHEMA_JSON_FILE_NAME = "schema.json";
+ private static final String SCHEMA06_JSON_FILE_NAME = "schema06.json";
+ private static final String SCHEMA07_JSON_FILE_NAME = "schema07.json";
+
+ @NotNull
+ @Override
+ public List<JsonSchemaFileProvider> getProviders(@NotNull final Project project) {
+ return ContainerUtil.list(new MyJsonSchemaFileProvider(project, SCHEMA_JSON_FILE_NAME),
+ new MyJsonSchemaFileProvider(project, SCHEMA06_JSON_FILE_NAME),
+ new MyJsonSchemaFileProvider(project, SCHEMA07_JSON_FILE_NAME));
+ }
+
+ public static class MyJsonSchemaFileProvider implements JsonSchemaFileProvider {
+ @NotNull private final Project myProject;
+ @NotNull private final NullableLazyValue<VirtualFile> mySchemaFile;
+ @NotNull private final String myFileName;
+
+ public boolean isSchemaV4() {
+ return SCHEMA_JSON_FILE_NAME.equals(myFileName);
+ }
+ public boolean isSchemaV6() {
+ return SCHEMA06_JSON_FILE_NAME.equals(myFileName);
+ }
+ public boolean isSchemaV7() {
+ return SCHEMA07_JSON_FILE_NAME.equals(myFileName);
+ }
+
+ private MyJsonSchemaFileProvider(@NotNull final Project project, @NotNull String fileName) {
+ myProject = project;
+ myFileName = fileName;
+ // schema file can not be static here, because in schema's user data we cache project-scope objects (i.e. which can refer to project)
+ mySchemaFile = NullableLazyValue.createValue(() -> JsonSchemaProviderFactory.getResourceFile(JsonSchemaProjectSelfProviderFactory.class, "/jsonSchema/" + fileName));
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull VirtualFile file) {
+ if (myProject.isDisposed()) return false;
+ JsonSchemaService service = JsonSchemaService.Impl.get(myProject);
+ if (!service.isApplicableToFile(file)) return false;
+ JsonSchemaVersion schemaVersion = service.getSchemaVersion(file);
+ if (schemaVersion == null) return false;
+ switch (schemaVersion) {
+ case SCHEMA_4:
+ return isSchemaV4();
+ case SCHEMA_6:
+ return isSchemaV6();
+ case SCHEMA_7:
+ return isSchemaV7();
+ }
+
+ throw new NotImplementedError("Unknown schema version: " + schemaVersion);
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion() {
+ return isSchemaV4() ? JsonSchemaVersion.SCHEMA_4 : isSchemaV7() ? JsonSchemaVersion.SCHEMA_7 : JsonSchemaVersion.SCHEMA_6;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return myFileName;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSchemaFile() {
+ return mySchemaFile.getValue();
+ }
+
+ @NotNull
+ @Override
+ public SchemaType getSchemaType() {
+ return SchemaType.schema;
+ }
+
+ @Nullable
+ @Override
+ public String getRemoteSource() {
+ switch (myFileName) {
+ case SCHEMA_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-04/schema";
+ case SCHEMA06_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-06/schema";
+ case SCHEMA07_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-07/schema";
+ }
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public String getPresentableName() {
+ switch (myFileName) {
+ case SCHEMA_JSON_FILE_NAME:
+ return "JSON schema v4";
+ case SCHEMA06_JSON_FILE_NAME:
+ return "JSON schema v6";
+ case SCHEMA07_JSON_FILE_NAME:
+ return "JSON schema v7";
+ }
+ return getName();
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java
new file mode 100644
index 00000000..70df36dd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java
@@ -0,0 +1,42 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.URL;
+import java.util.List;
+
+public interface JsonSchemaProviderFactory {
+ ExtensionPointName<JsonSchemaProviderFactory> EP_NAME = ExtensionPointName.create("JavaScript.JsonSchema.ProviderFactory");
+ Logger LOG = Logger.getInstance(JsonSchemaProviderFactory.class);
+
+ @NotNull
+ List<JsonSchemaFileProvider> getProviders(@NotNull Project project);
+
+ /**
+ * Finds a {@link VirtualFile} instance corresponding to a specified resource path (relative or absolute).
+ *
+ * @param baseClass
+ * @param resourcePath String identifying a resource (relative or absolute)
+ * See {@link Class#getResource(String)} for more details
+ * @return VirtualFile instance, or null if not found
+ */
+ static VirtualFile getResourceFile(@NotNull Class baseClass, @NotNull String resourcePath) {
+ URL url = baseClass.getResource(resourcePath);
+ if (url == null) {
+ LOG.error("Cannot find resource " + resourcePath);
+ return null;
+ }
+ VirtualFile file = VfsUtil.findFileByURL(url);
+ if (file == null) {
+ LOG.error("Cannot find file by " + resourcePath);
+ return null;
+ }
+ return file;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java
new file mode 100644
index 00000000..09117dd5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java
@@ -0,0 +1,138 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.PairProcessor;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/13/2016.
+ */
+public class JsonSchemaUserDefinedProviderFactory implements JsonSchemaProviderFactory {
+ @NotNull
+ @Override
+ public List<JsonSchemaFileProvider> getProviders(@NotNull Project project) {
+ final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+
+ final Map<String, UserDefinedJsonSchemaConfiguration> map = configuration.getStateMap();
+ final List<JsonSchemaFileProvider> providers = map.values().stream()
+ .map(schema -> createProvider(project, schema)).collect(Collectors.toList());
+
+ return providers.isEmpty() ? Collections.emptyList() : providers;
+ }
+
+ @NotNull
+ public MyProvider createProvider(@NotNull Project project,
+ UserDefinedJsonSchemaConfiguration schema) {
+ String relPath = schema.getRelativePathToSchema();
+ return new MyProvider(project, schema.getSchemaVersion(), schema.getName(),
+ isHttpPath(relPath) || new File(relPath).isAbsolute()
+ ? relPath
+ : new File(project.getBasePath(),
+ relPath).getAbsolutePath(),
+ schema.getCalculatedPatterns());
+ }
+
+ static class MyProvider implements JsonSchemaFileProvider, JsonSchemaImportedProviderMarker {
+ @NotNull private final Project myProject;
+ @NotNull private final JsonSchemaVersion myVersion;
+ @NotNull private final String myName;
+ @NotNull private final String myFile;
+ private VirtualFile myVirtualFile;
+ @NotNull private final List<? extends PairProcessor<Project, VirtualFile>> myPatterns;
+
+ MyProvider(@NotNull final Project project,
+ @NotNull final JsonSchemaVersion version,
+ @NotNull final String name,
+ @NotNull final String file,
+ @NotNull final List<? extends PairProcessor<Project, VirtualFile>> patterns) {
+ myProject = project;
+ myVersion = version;
+ myName = name;
+ myFile = file;
+ myPatterns = patterns;
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion() {
+ return myVersion;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSchemaFile() {
+ if (myVirtualFile != null && myVirtualFile.isValid()) return myVirtualFile;
+ String path = myFile;
+ if (isHttpPath(path)) {
+ myVirtualFile = JsonFileResolver.urlToFile(path);
+ }
+ else {
+ final LocalFileSystem lfs = LocalFileSystem.getInstance();
+ myVirtualFile = lfs.findFileByPath(myFile);
+ if (myVirtualFile == null) {
+ myVirtualFile = lfs.refreshAndFindFileByPath(myFile);
+ }
+ }
+ return myVirtualFile;
+ }
+
+ @NotNull
+ @Override
+ public SchemaType getSchemaType() {
+ return SchemaType.userSchema;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return myName;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull VirtualFile file) {
+ //noinspection SimplifiableIfStatement
+ if (myPatterns.isEmpty() || file.isDirectory() || !file.isValid() || getSchemaFile() == null) return false;
+ return myPatterns.stream().anyMatch(processor -> processor.process(myProject, file));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MyProvider provider = (MyProvider)o;
+
+ if (!myName.equals(provider.myName)) return false;
+ return FileUtil.pathsEqual(myFile, provider.myFile);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myName.hashCode();
+ result = 31 * result + FileUtil.pathHashCode(myFile);
+ return result;
+ }
+
+ @Nullable
+ @Override
+ public String getRemoteSource() {
+ return isHttpPath(myFile) ? myFile : null;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java
new file mode 100644
index 00000000..54dc903f
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+public interface JsonWidgetSuppressor {
+ ExtensionPointName<JsonWidgetSuppressor> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonWidgetSuppressor");
+
+ /**
+ * Allows to suppress JSON widget for particular files
+ * This method can access indexes and PSI
+ */
+ boolean suppressSwitcherWidget(@NotNull VirtualFile file, @NotNull Project project);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java
new file mode 100644
index 00000000..b59e5a1c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension;
+
+/**
+ * @author Irina.Chernushina on 3/29/2016.
+ */
+public enum SchemaType {
+ schema, embeddedSchema, userSchema, remoteSchema
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java
new file mode 100644
index 00000000..af247f78
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension.adapters;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonArrayValueAdapter extends JsonValueAdapter {
+ @NotNull List<JsonValueAdapter> getElements();
+
+ @Override
+ default boolean isNull() {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java
new file mode 100644
index 00000000..627ddf56
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension.adapters;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonObjectValueAdapter extends JsonValueAdapter {
+ @NotNull List<JsonPropertyAdapter> getPropertyList();
+
+ @Override
+ default boolean isNull() {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java
new file mode 100644
index 00000000..e82279bf
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension.adapters;
+
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonPropertyAdapter {
+ @Nullable String getName();
+ @Nullable JsonValueAdapter getNameValueAdapter();
+ @NotNull Collection<JsonValueAdapter> getValues();
+ @NotNull PsiElement getDelegate();
+ @Nullable JsonObjectValueAdapter getParentObject();
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java
new file mode 100644
index 00000000..1d8f2742
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.extension.adapters;
+
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonValueAdapter {
+ default boolean isShouldBeIgnored() {return false;}
+ boolean isObject();
+ boolean isArray();
+ boolean isStringLiteral();
+ boolean isNumberLiteral();
+ boolean isBooleanLiteral();
+ boolean isNull();
+
+ @NotNull PsiElement getDelegate();
+
+ @Nullable JsonObjectValueAdapter getAsObject();
+ @Nullable JsonArrayValueAdapter getAsArray();
+
+ default boolean shouldCheckIntegralRequirements() {return true;}
+}
diff --git a/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java
new file mode 100644
index 00000000..2070e3fc
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java
@@ -0,0 +1,75 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.ide;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface JsonSchemaService {
+ class Impl {
+ public static JsonSchemaService get(@NotNull Project project) {
+ return ServiceManager.getService(project, JsonSchemaService.class);
+ }
+ }
+
+ static boolean isSchemaFile(@NotNull PsiFile psiFile) {
+ final VirtualFile file = psiFile.getViewProvider().getVirtualFile();
+ JsonSchemaService service = Impl.get(psiFile.getProject());
+ return service.isSchemaFile(file) && service.isApplicableToFile(file);
+ }
+
+ boolean isSchemaFile(@NotNull VirtualFile file);
+
+ @Nullable
+ JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file);
+
+ @NotNull
+ Collection<VirtualFile> getSchemaFilesForFile(@NotNull VirtualFile file);
+
+ void registerRemoteUpdateCallback(Runnable callback);
+ void unregisterRemoteUpdateCallback(Runnable callback);
+ void registerResetAction(Runnable action);
+ void unregisterResetAction(Runnable action);
+
+ void registerReference(String ref);
+ boolean possiblyHasReference(String ref);
+
+ void triggerUpdateRemote();
+
+ @Nullable
+ JsonSchemaObject getSchemaObject(@NotNull VirtualFile file);
+
+ @Nullable
+ JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile);
+
+ @Nullable
+ VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent);
+
+ @Nullable
+ JsonSchemaFileProvider getSchemaProvider(@NotNull final VirtualFile schemaFile);
+
+ void reset();
+
+ ModificationTracker getAnySchemaChangeTracker();
+
+ List<JsonSchemaInfo> getAllUserVisibleSchemas();
+
+ boolean isApplicableToFile(@Nullable VirtualFile file);
+
+ @NotNull
+ static String normalizeId(@NotNull String id) {
+ id = id.endsWith("#") ? id.substring(0, id.length() - 1) : id;
+ return id.startsWith("#") ? id.substring(1) : id;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java
new file mode 100644
index 00000000..b069841a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java
@@ -0,0 +1,41 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.util.Function;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class CachedValueProviderOnPsiFile<T> implements CachedValueProvider<T> {
+ private final PsiFile myPsiFile;
+
+ public CachedValueProviderOnPsiFile(@NotNull PsiFile psiFile) {
+ myPsiFile = psiFile;
+ }
+
+ @Nullable
+ @Override
+ public Result<T> compute() {
+ return CachedValueProvider.Result.create(evaluate(myPsiFile), myPsiFile);
+ }
+
+ @Nullable
+ public abstract T evaluate(@NotNull PsiFile psiFile);
+
+ @Nullable
+ public static <T> T getOrCompute(@NotNull PsiFile psiFile, @NotNull Function<? super PsiFile, ? extends T> eval, @NotNull Key<CachedValue<T>> key) {
+ final CachedValueProvider<T> provider = new CachedValueProviderOnPsiFile<T>(psiFile) {
+ @Override
+ @Nullable
+ public T evaluate(@NotNull PsiFile psiFile) {
+ return eval.fun(psiFile);
+ }
+ };
+ return ReadAction.compute(() -> CachedValuesManager.getCachedValue(psiFile, key, provider));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java
new file mode 100644
index 00000000..70f66a56
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+public class EnumArrayValueWrapper {
+ @NotNull private final Object[] myValues;
+
+ public EnumArrayValueWrapper(@NotNull Object[] values) {
+ myValues = values;
+ }
+
+ @NotNull
+ public Object[] getValues() {
+ return myValues;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + Arrays.stream(myValues).map(v -> v.toString()).collect(Collectors.joining(", ")) + "]";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java
new file mode 100644
index 00000000..b8da4134
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class EnumObjectValueWrapper {
+ @NotNull private final Map<String, Object> myValues;
+
+ public EnumObjectValueWrapper(@NotNull Map<String, Object> values) {
+ myValues = values;
+ }
+
+ @NotNull
+ public Map<String, Object> getValues() {
+ return myValues;
+ }
+
+ @Override
+ public String toString() {
+ return "{" + myValues.entrySet().stream().map(v -> "\"" + v.getKey() + "\": " + v.getValue()).collect(Collectors.joining(", ")) + "}";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java
new file mode 100644
index 00000000..40773ae4
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java
@@ -0,0 +1,192 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.navigation.JsonQualifiedNameKind;
+import com.intellij.json.navigation.JsonQualifiedNameProvider;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.SyntaxTraverser;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.util.AstLoadingFilter;
+import com.intellij.util.Function;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class JsonCachedValues {
+ private static final Key<CachedValue<JsonSchemaObject>> JSON_OBJECT_CACHE_KEY = Key.create("JsonSchemaObjectCache");
+
+ @Nullable
+ public static JsonSchemaObject getSchemaObject(@NotNull VirtualFile schemaFile, @NotNull Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(schemaFile, project);
+ return computeForFile(schemaFile, project, JsonCachedValues::computeSchemaObject, JSON_OBJECT_CACHE_KEY);
+ }
+
+ @Nullable
+ private static JsonSchemaObject computeSchemaObject(@NotNull PsiFile f) {
+ final JsonObject topLevelValue = AstLoadingFilter.forceAllowTreeLoading(
+ f,
+ () -> ObjectUtils.tryCast(((JsonFile)f).getTopLevelValue(), JsonObject.class));
+ if (topLevelValue != null) {
+ return new JsonSchemaReader().read(topLevelValue);
+ }
+ return null;
+ }
+
+ static final String URL_CACHE_KEY = "JsonSchemaUrlCache";
+ private static final Key<CachedValue<String>> SCHEMA_URL_KEY = Key.create(URL_CACHE_KEY);
+ @Nullable
+ public static String getSchemaUrlFromSchemaProperty(@NotNull VirtualFile file,
+ @NotNull Project project) {
+ String value = JsonSchemaFileValuesIndex.getCachedValue(project, file, URL_CACHE_KEY);
+ if (value != null) {
+ return JsonSchemaFileValuesIndex.NULL.equals(value) ? null : value;
+ }
+
+ PsiFile psiFile = resolveFile(file, project);
+ return !(psiFile instanceof JsonFile) ? null : CachedValueProviderOnPsiFile
+ .getOrCompute(psiFile, JsonCachedValues::fetchSchemaUrl, SCHEMA_URL_KEY);
+ }
+
+ private static PsiFile resolveFile(@NotNull VirtualFile file,
+ @NotNull Project project) {
+ if (project.isDisposed() || !file.isValid()) return null;
+ return PsiManager.getInstance(project).findFile(file);
+ }
+
+ @Nullable
+ static String fetchSchemaUrl(@Nullable PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) return null;
+ final String url = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText()).get(URL_CACHE_KEY);
+ return url == null || JsonSchemaFileValuesIndex.NULL.equals(url) ? null : url;
+ }
+
+ static final String ID_CACHE_KEY = "JsonSchemaIdCache";
+ static final String OBSOLETE_ID_CACHE_KEY = "JsonSchemaObsoleteIdCache";
+ private static final Key<CachedValue<String>> SCHEMA_ID_CACHE_KEY = Key.create(ID_CACHE_KEY);
+ @Nullable
+ public static String getSchemaId(@NotNull final VirtualFile schemaFile,
+ @NotNull final Project project) {
+ String value = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, ID_CACHE_KEY);
+ if (value != null && !JsonSchemaFileValuesIndex.NULL.equals(value)) return JsonSchemaService.normalizeId(value);
+ String obsoleteValue = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, OBSOLETE_ID_CACHE_KEY);
+ if (obsoleteValue != null && !JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return JsonSchemaService.normalizeId(obsoleteValue);
+ if (JsonSchemaFileValuesIndex.NULL.equals(value) || JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return null;
+
+ final String result = computeForFile(schemaFile, project, JsonCachedValues::fetchSchemaId, SCHEMA_ID_CACHE_KEY);
+ return result == null ? null : JsonSchemaService.normalizeId(result);
+ }
+
+ @Nullable
+ private static <T> T computeForFile(@NotNull final VirtualFile schemaFile,
+ @NotNull final Project project,
+ @NotNull Function<? super PsiFile, ? extends T> eval,
+ @NotNull Key<CachedValue<T>> cacheKey) {
+ final PsiFile psiFile = resolveFile(schemaFile, project);
+ if (!(psiFile instanceof JsonFile)) return null;
+ return CachedValueProviderOnPsiFile.getOrCompute(psiFile, eval, cacheKey);
+ }
+
+ static final String ID_PATHS_CACHE_KEY = "JsonSchemaIdToPointerCache";
+ private static final Key<CachedValue<Map<String, String>>> SCHEMA_ID_PATHS_CACHE_KEY = Key.create(ID_PATHS_CACHE_KEY);
+ public static Collection<String> getAllIdsInFile(PsiFile psiFile) {
+ final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY);
+ return map == null ? ContainerUtil.emptyList() : map.keySet();
+ }
+ @Nullable
+ public static String resolveId(PsiFile psiFile, String id) {
+ final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY);
+ return map == null ? null : map.get(id);
+ }
+
+ private static Map<String, String> computeIdsMap(PsiFile file) {
+ return SyntaxTraverser.psiTraverser(file).filter(JsonProperty.class).filter(p -> "$id".equals(p.getName()))
+ .filter(p -> p.getValue() instanceof JsonStringLiteral)
+ .toMap(p -> ((JsonStringLiteral)Objects.requireNonNull(p.getValue())).getValue(),
+ p -> JsonQualifiedNameProvider.generateQualifiedName(p.getParent(), JsonQualifiedNameKind.JsonPointer));
+ }
+
+ @Nullable
+ static String fetchSchemaId(@NotNull PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) return null;
+ final Map<String, String> props = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText());
+ final String id = props.get(ID_CACHE_KEY);
+ if (id != null && !JsonSchemaFileValuesIndex.NULL.equals(id)) return id;
+ final String obsoleteId = props.get(OBSOLETE_ID_CACHE_KEY);
+ return obsoleteId == null || JsonSchemaFileValuesIndex.NULL.equals(obsoleteId) ? null : obsoleteId;
+ }
+
+
+ private static final Key<CachedValue<List<Pair<Collection<String>, String>>>> SCHEMA_CATALOG_CACHE_KEY = Key.create("JsonSchemaCatalogCache");
+ @Nullable
+ public static List<Pair<Collection<String>, String>> getSchemaCatalog(@NotNull final VirtualFile catalog,
+ @NotNull final Project project) {
+ if (!catalog.isValid()) return null;
+ return computeForFile(catalog, project, JsonCachedValues::computeSchemaCatalog, SCHEMA_CATALOG_CACHE_KEY);
+ }
+
+ private static List<Pair<Collection<String>, String>> computeSchemaCatalog(PsiFile catalog) {
+ if (!catalog.isValid()) return null;
+ JsonValue value = AstLoadingFilter.forceAllowTreeLoading(catalog, () -> ((JsonFile)catalog).getTopLevelValue());
+ if (!(value instanceof JsonObject)) return null;
+
+ JsonProperty schemas = ((JsonObject)value).findProperty("schemas");
+ if (schemas == null) return null;
+
+ JsonValue schemasValue = schemas.getValue();
+ if (!(schemasValue instanceof JsonArray)) return null;
+ List<Pair<Collection<String>, String>> catalogMap = ContainerUtil.newArrayList();
+ fillMap((JsonArray)schemasValue, catalogMap);
+ return catalogMap;
+ }
+
+ private static void fillMap(@NotNull JsonArray array, @NotNull List<Pair<Collection<String>, String>> catalogMap) {
+ for (JsonValue value: array.getValueList()) {
+ if (!(value instanceof JsonObject)) continue;
+ JsonProperty fileMatch = ((JsonObject)value).findProperty("fileMatch");
+ Collection<String> masks = fileMatch == null ? ContainerUtil.emptyList() : resolveMasks(fileMatch.getValue());
+ JsonProperty url = ((JsonObject)value).findProperty("url");
+ if (url == null) continue;
+ JsonValue urlValue = url.getValue();
+ if (urlValue instanceof JsonStringLiteral) {
+ String urlStringValue = ((JsonStringLiteral)urlValue).getValue();
+ if (!StringUtil.isEmpty(urlStringValue)) {
+ catalogMap.add(Pair.create(masks, urlStringValue));
+ }
+ }
+ }
+ }
+
+ @NotNull
+ private static Collection<String> resolveMasks(@Nullable JsonValue value) {
+ if (value instanceof JsonStringLiteral) {
+ return ContainerUtil.createMaybeSingletonList(((JsonStringLiteral)value).getValue());
+ }
+
+ if (value instanceof JsonArray) {
+ List<String> strings = ContainerUtil.newArrayList();
+ for (JsonValue val: ((JsonArray)value).getValueList()) {
+ if (val instanceof JsonStringLiteral) {
+ strings.add(((JsonStringLiteral)val).getValue());
+ }
+ }
+ return strings;
+ }
+
+ return ContainerUtil.emptyList();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java
new file mode 100644
index 00000000..0ef6403c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+public class JsonComplianceCheckerOptions {
+ public static final JsonComplianceCheckerOptions RELAX_ENUM_CHECK = new JsonComplianceCheckerOptions(true);
+
+ private final boolean isCaseInsensitiveEnumCheck;
+ public JsonComplianceCheckerOptions(boolean caseInsensitiveEnumCheck) {isCaseInsensitiveEnumCheck = caseInsensitiveEnumCheck;}
+
+ public boolean isCaseInsensitiveEnumCheck() {
+ return isCaseInsensitiveEnumCheck;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java
new file mode 100644
index 00000000..660ae3bd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java
@@ -0,0 +1,10 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+public enum JsonErrorPriority {
+ NOT_SCHEMA,
+ TYPE_MISMATCH,
+ MEDIUM_PRIORITY,
+ MISSING_PROPS,
+ LOW_PRIORITY
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java
new file mode 100644
index 00000000..1262aa5c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java
@@ -0,0 +1,217 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.impl.adapters.JsonJsonPropertyAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/16/2017.
+ */
+public class JsonOriginalPsiWalker implements JsonLikePsiWalker {
+ public static final JsonOriginalPsiWalker INSTANCE = new JsonOriginalPsiWalker();
+
+ public boolean handles(@NotNull PsiElement element) {
+ PsiElement parent = element.getParent();
+ return parent != null && (element instanceof JsonElement || element instanceof LeafPsiElement && parent instanceof JsonElement)
+ && JsonDialectUtil.isStandardJson(CompletionUtil.getOriginalOrSelf(parent));
+ }
+
+ @Override
+ public ThreeState isName(PsiElement element) {
+ final PsiElement parent = element.getParent();
+ if (parent instanceof JsonObject) {
+ return ThreeState.YES;
+ } else if (parent instanceof JsonProperty) {
+ return PsiTreeUtil.isAncestor(((JsonProperty)parent).getNameElement(), element, false) ? ThreeState.YES : ThreeState.NO;
+ }
+ return ThreeState.NO;
+ }
+
+ @Override
+ public boolean isPropertyWithValue(@NotNull PsiElement element) {
+ return element instanceof JsonProperty && ((JsonProperty)element).getValue() != null;
+ }
+
+ @Override
+ public PsiElement goUpToCheckable(@NotNull PsiElement element) {
+ PsiElement current = element;
+ while (current != null && !(current instanceof PsiFile)) {
+ if (current instanceof JsonValue || current instanceof JsonProperty) {
+ return current;
+ }
+ current = current.getParent();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull PsiElement element, boolean forceLastTransition) {
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = new ArrayList<>();
+ PsiElement current = element;
+ while (! (current instanceof PsiFile)) {
+ final PsiElement position = current;
+ current = current.getParent();
+ if (current instanceof JsonArray) {
+ JsonArray array = (JsonArray)current;
+ final List<JsonValue> list = array.getValueList();
+ int idx = -1;
+ for (int i = 0; i < list.size(); i++) {
+ final JsonValue value = list.get(i);
+ if (value.equals(position)) {
+ idx = i;
+ break;
+ }
+ }
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createArrayElementStep(idx));
+ } else if (current instanceof JsonProperty) {
+ final String propertyName = ((JsonProperty)current).getName();
+ current = current.getParent();
+ if (!(current instanceof JsonObject)) return null;//incorrect syntax?
+ // if either value or not first in the chain - needed for completion variant
+ if (position != element || forceLastTransition) {
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(propertyName));
+ }
+ } else if (current instanceof JsonObject && position instanceof JsonProperty) {
+ // if either value or not first in the chain - needed for completion variant
+ if (position != element || forceLastTransition) {
+ final String propertyName = ((JsonProperty)position).getName();
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(propertyName));
+ }
+ } else if (current instanceof PsiFile) {
+ break;
+ } else {
+ return null;//something went wrong
+ }
+ }
+ Collections.reverse(steps);
+ return steps;
+ }
+
+ @Override
+ public boolean isNameQuoted() {
+ return true;
+ }
+
+ @Override
+ public boolean onlyDoubleQuotesForStringLiterals() {
+ return true;
+ }
+
+ @Override
+ public boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element) {
+ PsiElement current = element instanceof JsonProperty ? element : PsiTreeUtil.getParentOfType(element, JsonProperty.class);
+ while (current != null && current.getNode().getElementType() != JsonElementTypes.COMMA) {
+ current = current.getNextSibling();
+ }
+ int commaOffset = current == null ? Integer.MAX_VALUE : current.getTextRange().getStartOffset();
+ final int offset = element.getTextRange().getStartOffset();
+ final JsonObject object = PsiTreeUtil.getParentOfType(element, JsonObject.class);
+ if (object != null) {
+ for (JsonProperty property : object.getPropertyList()) {
+ final int pOffset = property.getTextRange().getStartOffset();
+ if (pOffset >= offset && !PsiTreeUtil.isAncestor(property, element, false)) {
+ return pOffset < commaOffset;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition) {
+ final JsonObject object = PsiTreeUtil.getParentOfType(originalPosition, JsonObject.class);
+ if (object != null) {
+ return object.getPropertyList().stream()
+ .filter(p -> !isNameQuoted() || p.getNameElement() instanceof JsonStringLiteral)
+ .map(p -> StringUtil.unquoteString(p.getName())).collect(Collectors.toSet());
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ public JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element) {
+ final JsonProperty property = PsiTreeUtil.getParentOfType(element, JsonProperty.class, false);
+ if (property == null) return null;
+ return new JsonJsonPropertyAdapter(property);
+ }
+
+ @Override
+ public boolean isTopJsonElement(@NotNull PsiElement element) {
+ return element instanceof PsiFile;
+ }
+
+ @Nullable
+ @Override
+ public JsonValueAdapter createValueAdapter(@NotNull PsiElement element) {
+ return element instanceof JsonValue ? JsonJsonPropertyAdapter.createAdapterByType((JsonValue)element) : null;
+ }
+
+ @Override
+ public QuickFixAdapter getQuickFixAdapter(Project project) {
+ return new QuickFixAdapter() {
+ private final JsonElementGenerator myGenerator = new JsonElementGenerator(project);
+ @Nullable
+ @Override
+ public PsiElement getPropertyValue(PsiElement property) {
+ assert property instanceof JsonProperty;
+ return ((JsonProperty)property).getValue();
+ }
+
+ @NotNull
+ @Override
+ public String getPropertyName(PsiElement property) {
+ assert property instanceof JsonProperty;
+ return ((JsonProperty)property).getName();
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createProperty(@NotNull String name, @NotNull String value) {
+ return myGenerator.createProperty(name, value);
+ }
+
+ @Override
+ public boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement) {
+ if (backward instanceof JsonProperty) {
+ self.addAfter(myGenerator.createComma(), backward);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeIfComma(PsiElement forward) {
+ if (forward instanceof LeafPsiElement && ((LeafPsiElement)forward).getElementType() == JsonElementTypes.COMMA) {
+ forward.delete();
+ }
+ }
+
+ @Override
+ public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) {
+ return true;
+ }
+ };
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java
new file mode 100644
index 00000000..e0a9ed84
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.codeInsight.completion.CompletionUtilCore;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.paths.WebReference;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceProvider;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileInfoManager;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.ProcessingContext;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonPointerResolver;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.List;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 3/31/2016.
+ */
+public class JsonPointerReferenceProvider extends PsiReferenceProvider {
+ private final boolean myOnlyFilePart;
+
+ public JsonPointerReferenceProvider(boolean onlyFilePart) {
+ myOnlyFilePart = onlyFilePart;
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
+ if (!(element instanceof JsonStringLiteral)) return PsiReference.EMPTY_ARRAY;
+ List<PsiReference> refs = ContainerUtil.newArrayList();
+
+ List<Pair<TextRange, String>> fragments = ((JsonStringLiteral)element).getTextFragments();
+ if (fragments.size() != 1) return PsiReference.EMPTY_ARRAY;
+ Pair<TextRange, String> fragment = fragments.get(0);
+ String originalText = element.getText();
+ int hash = originalText.indexOf('#');
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(fragment.second);
+ String id = splitter.getSchemaId();
+ if (id != null) {
+ if (id.startsWith("#")) {
+ refs.add(new JsonSchemaIdReference((JsonValue)element, id));
+ }
+ else {
+ addFileOrWebReferences(element, refs, hash, id);
+ }
+ }
+ if (!myOnlyFilePart) {
+ String relativePath = normalizeSlashes(JsonSchemaService.normalizeId(splitter.getRelativePath()));
+ List<String> parts1 = split(relativePath);
+ String[] strings = ContainerUtil.toArray(parts1, String[]::new);
+ List<String> parts2 = split(normalizeSlashes(originalText.substring(hash + 1)));
+ if (strings.length == parts2.size()) {
+ int start = hash + 2;
+ for (int i = 0; i < parts2.size(); i++) {
+ int length = parts2.get(i).length();
+ if (i == parts2.size() - 1) length--;
+ refs.add(new JsonPointerReference((JsonValue)element, new TextRange(start, start + length),
+ (id == null ? "" : id) + "#/" + StringUtil.join(strings, 0, i + 1, "/")));
+ start += length + 1;
+ }
+ }
+ }
+ return refs.size() == 0 ? PsiReference.EMPTY_ARRAY : ContainerUtil.toArray(refs, PsiReference[]::new);
+ }
+
+ private void addFileOrWebReferences(@NotNull PsiElement element, List<PsiReference> refs, int hashIndex, String id) {
+ if (isHttpPath(id)) {
+ refs.add(new WebReference(element, new TextRange(1, hashIndex >= 0 ? hashIndex : id.length() + 1), id));
+ return;
+ }
+
+ ContainerUtil.addAll(refs, new FileReferenceSet(id, element, 1, null, true,
+ true, new JsonFileType[]{JsonFileType.INSTANCE}) {
+ @Override
+ public boolean isEmptyPathAllowed() {
+ return true;
+ }
+
+ @Override
+ protected boolean isSoft() {
+ return true;
+ }
+
+ @Override
+ public FileReference createFileReference(TextRange range, int index, String text) {
+ if (hashIndex != -1 && range.getStartOffset() >= hashIndex) return null;
+ if (hashIndex != -1 && range.getEndOffset() > hashIndex) {
+ range = new TextRange(range.getStartOffset(), hashIndex);
+ text = text.substring(0, text.indexOf('#'));
+ }
+ return new FileReference(this, range, index, text) {
+ @Override
+ protected Object createLookupItem(PsiElement candidate) {
+ return FileInfoManager.getFileLookupItem(candidate);
+ }
+ };
+ }
+ }.getAllReferences());
+ }
+
+ @Nullable
+ static PsiElement resolveForPath(PsiElement element, String text, boolean alwaysRoot) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(text);
+ VirtualFile schemaFile = CompletionUtil.getOriginalOrSelf(element.getContainingFile()).getVirtualFile();
+ if (splitter.isAbsolute()) {
+ assert splitter.getSchemaId() != null;
+ schemaFile = service.findSchemaFileByReference(splitter.getSchemaId(), schemaFile);
+ if (schemaFile == null) return null;
+ }
+
+ final String normalized = JsonSchemaService.normalizeId(splitter.getRelativePath());
+ if (!alwaysRoot && (StringUtil.isEmptyOrSpaces(normalized) || split(normalizeSlashes(normalized)).size() == 0)) {
+ return element.getManager().findFile(schemaFile);
+ }
+ final List<String> chain = split(normalizeSlashes(normalized));
+ final JsonSchemaObject schemaObject = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (schemaObject == null) return null;
+
+ return new JsonPointerResolver(schemaObject.getJsonObject(), StringUtil.join(chain, "/")).resolve();
+ }
+
+ public static class JsonSchemaIdReference extends JsonSchemaBaseReference<JsonValue> {
+ private final String myText;
+
+ private JsonSchemaIdReference(JsonValue element, String text) {
+ super(element, getRange(element));
+ myText = text;
+ }
+
+ @NotNull
+ private static TextRange getRange(JsonValue element) {
+ final TextRange range = element.getTextRange().shiftLeft(element.getTextOffset());
+ return new TextRange(range.getStartOffset() + 1, range.getEndOffset() - 1);
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ final String id = JsonCachedValues.resolveId(myElement.getContainingFile(), myText);
+ if (id == null) return null;
+ return resolveForPath(myElement, "#" + id, false);
+ }
+
+ @NotNull
+ @Override
+ public Object[] getVariants() {
+ return JsonCachedValues.getAllIdsInFile(myElement.getContainingFile()).toArray();
+ }
+ }
+
+ private static class JsonPointerReference extends JsonSchemaBaseReference<JsonValue> {
+ private final String myFullPath;
+
+ JsonPointerReference(JsonValue element, TextRange textRange, String curPath) {
+ super(element, textRange);
+ myFullPath = curPath;
+ }
+
+ @NotNull
+ @Override
+ public String getCanonicalText() {
+ return myFullPath;
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ return resolveForPath(myElement, getCanonicalText(), false);
+ }
+
+ @Override
+ protected boolean isIdenticalTo(JsonSchemaBaseReference that) {
+ return super.isIdenticalTo(that) && getRangeInElement().equals(that.getRangeInElement());
+ }
+
+ @NotNull
+ @Override
+ public Object[] getVariants() {
+ String text = getCanonicalText();
+ int index = text.indexOf(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED);
+ if (index >= 0) {
+ String part = text.substring(0, index);
+ text = prepare(part);
+ String prefix = null;
+ PsiElement element = resolveForPath(myElement, text, true);
+ int indexOfSlash = part.lastIndexOf('/');
+ if (indexOfSlash != -1 && indexOfSlash < text.length() - 1 && indexOfSlash < index) {
+ prefix = text.substring(indexOfSlash + 1);
+ element = resolveForPath(myElement, prepare(text.substring(0, indexOfSlash)), true);
+ }
+ String finalPrefix = prefix;
+ if (element instanceof JsonObject) {
+ return ((JsonObject)element).getPropertyList().stream()
+ .filter(p -> p.getValue() instanceof JsonContainer && (finalPrefix == null || p.getName().startsWith(finalPrefix)))
+ .map(p -> LookupElementBuilder.create(p, escapeForJsonPointer(p.getName()))
+ .withIcon(getIcon(p.getValue()))).toArray();
+ }
+ else if (element instanceof JsonArray) {
+ List<JsonValue> list = ((JsonArray)element).getValueList();
+ List<Object> values = ContainerUtil.newLinkedList();
+ for (int i = 0; i < list.size(); i++) {
+ String stringValue = String.valueOf(i);
+ if (prefix != null && !stringValue.startsWith(prefix)) continue;
+ values.add(LookupElementBuilder.create(stringValue).withIcon(getIcon(list.get(i))));
+ }
+ return ContainerUtil.toArray(values, Object[]::new);
+ }
+ }
+
+ return ArrayUtil.EMPTY_OBJECT_ARRAY;
+ }
+
+ private static Icon getIcon(JsonValue value) {
+ if (value instanceof JsonObject) {
+ return AllIcons.Json.Object;
+ }
+ else if (value instanceof JsonArray) {
+ return AllIcons.Json.Array;
+ }
+ return AllIcons.Nodes.Property;
+ }
+ }
+
+ @NotNull
+ private static String prepare(String part) {
+ return part.endsWith("#/") ? part : StringUtil.trimEnd(part, '/');
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java
new file mode 100644
index 00000000..44cd3704
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java
@@ -0,0 +1,62 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceProvider;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public class JsonRequiredPropsReferenceProvider extends PsiReferenceProvider {
+ @NotNull
+ @Override
+ public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
+ return new PsiReference[] {new JsonRequiredPropReference((JsonStringLiteral)element)};
+ }
+
+ @Nullable
+ public static JsonObject findPropertiesObject(PsiElement element) {
+ PsiElement parent = getParentSafe(getParentSafe(getParentSafe(element)));
+ if (!(parent instanceof JsonObject)) return null;
+ Optional<JsonProperty> propertiesProp =
+ ((JsonObject)parent).getPropertyList().stream().filter(p -> "properties".equals(p.getName())).findFirst();
+ if (propertiesProp.isPresent()) {
+ JsonValue value = propertiesProp.get().getValue();
+ if (value instanceof JsonObject) {
+ return (JsonObject)value;
+ }
+ }
+ return null;
+ }
+
+ private static PsiElement getParentSafe(@Nullable PsiElement element) {
+ return element == null ? null : element.getParent();
+ }
+
+ private static class JsonRequiredPropReference extends JsonSchemaBaseReference<JsonStringLiteral> {
+ JsonRequiredPropReference(JsonStringLiteral element) {
+ super(element, ElementManipulators.getValueTextRange(element));
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ JsonObject propertiesObject = findPropertiesObject(getElement());
+ if (propertiesObject != null) {
+ String name = getElement().getValue();
+ for (JsonProperty property : propertiesObject.getPropertyList()) {
+ if (name.equals(property.getName())) return property;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java
new file mode 100644
index 00000000..1fb1f5a2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java
@@ -0,0 +1,1147 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonContainer;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.SmartList;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 4/25/2017.
+ */
+class JsonSchemaAnnotatorChecker {
+ private static final Set<JsonSchemaType> PRIMITIVE_TYPES =
+ ContainerUtil.set(JsonSchemaType._integer, JsonSchemaType._number, JsonSchemaType._boolean, JsonSchemaType._string, JsonSchemaType._null);
+ private final Map<PsiElement, JsonValidationError> myErrors;
+ private final JsonComplianceCheckerOptions myOptions;
+ private boolean myHadTypeError;
+ private static final String ENUM_MISMATCH_PREFIX = "Value should be one of: ";
+
+ protected JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions options) {
+ myOptions = options;
+ myErrors = new HashMap<>();
+ }
+
+ public Map<PsiElement, JsonValidationError> getErrors() {
+ return myErrors;
+ }
+
+ public boolean isHadTypeError() {
+ return myHadTypeError;
+ }
+
+ public static JsonSchemaAnnotatorChecker checkByMatchResult(@NotNull JsonValueAdapter elementToCheck,
+ @NotNull final MatchResult result,
+ @NotNull JsonComplianceCheckerOptions options) {
+ final List<JsonSchemaAnnotatorChecker> checkers = new ArrayList<>();
+ if (result.myExcludingSchemas.isEmpty() && result.mySchemas.size() == 1) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+ checker.checkByScheme(elementToCheck, result.mySchemas.iterator().next());
+ checkers.add(checker);
+ }
+ else {
+ if (!result.mySchemas.isEmpty()) {
+ checkers.add(processSchemasVariants(result.mySchemas, elementToCheck, false, options).getSecond());
+ }
+ if (!result.myExcludingSchemas.isEmpty()) {
+ // we can have several oneOf groups, each about, for instance, a part of properties
+ // - then we should allow properties from neighbour schemas (even if additionalProperties=false)
+ final List<JsonSchemaAnnotatorChecker> list = result.myExcludingSchemas.stream()
+ .map(group -> processSchemasVariants(group, elementToCheck, true, options).getSecond()).collect(Collectors.toList());
+ checkers.add(mergeErrors(list, options, result.myExcludingSchemas));
+ }
+ }
+ if (checkers.isEmpty()) return null;
+ if (checkers.size() == 1) return checkers.get(0);
+
+ return checkers.stream()
+ .filter(checker -> !checker.isHadTypeError())
+ .findFirst()
+ .orElse(checkers.get(0));
+ }
+
+ private static JsonSchemaAnnotatorChecker mergeErrors(@NotNull List<JsonSchemaAnnotatorChecker> list,
+ @NotNull JsonComplianceCheckerOptions options,
+ List<Collection<? extends JsonSchemaObject>> excludingSchemas) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+
+ for (JsonSchemaAnnotatorChecker ch: list) {
+ for (Map.Entry<PsiElement, JsonValidationError> element: ch.myErrors.entrySet()) {
+ JsonValidationError error = element.getValue();
+ if (error.getFixableIssueKind() == JsonValidationError.FixableIssueKind.ProhibitedProperty) {
+ String propertyName = ((JsonValidationError.ProhibitedPropertyIssueData)error.getIssueData()).propertyName;
+ boolean skip = false;
+ for (Collection<? extends JsonSchemaObject> objects : excludingSchemas) {
+ Set<String> keys = objects.stream().map(o -> o.getProperties().keySet()).flatMap(Set::stream).collect(Collectors.toSet());
+ if (keys.contains(propertyName)) skip = true;
+ }
+ if (skip) continue;
+ }
+ checker.myErrors.put(element.getKey(), error);
+ }
+ }
+ return checker;
+ }
+
+ private void error(final String error, final PsiElement holder,
+ JsonErrorPriority priority) {
+ error(error, holder, JsonValidationError.FixableIssueKind.None, null, priority);
+ }
+
+ private void error(final PsiElement newHolder, JsonValidationError error) {
+ error(error.getMessage(), newHolder, error.getFixableIssueKind(), error.getIssueData(), error.getPriority());
+ }
+
+ private void error(final String error, final PsiElement holder,
+ JsonValidationError.FixableIssueKind fixableIssueKind,
+ JsonValidationError.IssueData data,
+ JsonErrorPriority priority) {
+ if (myErrors.containsKey(holder)) return;
+ myErrors.put(holder, new JsonValidationError(error, fixableIssueKind, data, priority));
+ }
+
+ private void typeError(final @NotNull PsiElement value, final @NotNull JsonSchemaType... allowedTypes) {
+ if (allowedTypes.length > 0) {
+ if (allowedTypes.length == 1) {
+ error(String.format("Type is not allowed. Expected: %s.", allowedTypes[0].getName()), value,
+ JsonValidationError.FixableIssueKind.ProhibitedType,
+ new JsonValidationError.TypeMismatchIssueData(allowedTypes),
+ JsonErrorPriority.TYPE_MISMATCH);
+ } else {
+ final String typesText = Arrays.stream(allowedTypes)
+ .map(JsonSchemaType::getName)
+ .distinct()
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.joining(", "));
+ error(String.format("Type is not allowed. Expected one of: %s.", typesText), value,
+ JsonValidationError.FixableIssueKind.ProhibitedType,
+ new JsonValidationError.TypeMismatchIssueData(allowedTypes),
+ JsonErrorPriority.TYPE_MISMATCH);
+ }
+ } else {
+ error("Type is not allowed", value, JsonErrorPriority.TYPE_MISMATCH);
+ }
+ myHadTypeError = true;
+ }
+
+ public void checkByScheme(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) {
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ checkForEnum(value.getDelegate(), schema);
+ boolean checkedNumber = false;
+ boolean checkedString = false;
+ boolean checkedArray = false;
+ boolean checkedObject = false;
+ if (type != null) {
+ JsonSchemaType schemaType = getMatchingSchemaType(schema, type);
+ if (schemaType != null && !schemaType.equals(type)) {
+ typeError(value.getDelegate(), schemaType);
+ }
+ else {
+ if (JsonSchemaType._string_number.equals(type)) {
+ checkNumber(value.getDelegate(), schema, type);
+ checkedNumber = true;
+ checkString(value.getDelegate(), schema);
+ checkedString = true;
+ }
+ else if (JsonSchemaType._number.equals(type) || JsonSchemaType._integer.equals(type)) {
+ checkNumber(value.getDelegate(), schema, type);
+ checkedNumber = true;
+ }
+ else if (JsonSchemaType._string.equals(type)) {
+ checkString(value.getDelegate(), schema);
+ checkedString = true;
+ }
+ else if (JsonSchemaType._array.equals(type)) {
+ checkArray(value, schema);
+ checkedArray = true;
+ }
+ else if (JsonSchemaType._object.equals(type)) {
+ checkObject(value, schema);
+ checkedObject = true;
+ }
+ }
+ }
+
+ if ((!myHadTypeError || myErrors.isEmpty()) && !value.isShouldBeIgnored()) {
+ PsiElement delegate = value.getDelegate();
+ if (!checkedNumber && schema.hasNumericChecks() && value.isNumberLiteral()) {
+ checkNumber(delegate, schema, JsonSchemaType._number);
+ }
+ if (!checkedString && schema.hasStringChecks() && value.isStringLiteral()) {
+ checkString(delegate, schema);
+ checkedString = true;
+ }
+ if (!checkedArray && schema.hasArrayChecks() && value.isArray()) {
+ checkArray(value, schema);
+ checkedArray = true;
+ }
+ if (hasMinMaxLengthChecks(schema)) {
+ if (value.isStringLiteral()) {
+ if (!checkedString) {
+ checkString(delegate, schema);
+ }
+ }
+ else if (value.isArray()) {
+ if (!checkedArray) {
+ checkArray(value, schema);
+ }
+ }
+ }
+ if (!checkedObject && schema.hasObjectChecks() && value.isObject()) {
+ checkObject(value, schema);
+ }
+ }
+
+ if (schema.getNot() != null) {
+ final MatchResult result = new JsonSchemaResolver(schema.getNot()).detailedResolve();
+ if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return;
+
+ // if 'not' uses reference to owning schema back -> do not check, seems it does not make any sense
+ if (result.mySchemas.stream().anyMatch(s -> schema.getJsonObject().equals(s.getJsonObject())) ||
+ result.myExcludingSchemas.stream().flatMap(Collection::stream)
+ .anyMatch(s -> schema.getJsonObject().equals(s.getJsonObject()))) return;
+
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions);
+ if (checker == null || checker.isCorrect()) error("Validates against 'not' schema", value.getDelegate(), JsonErrorPriority.NOT_SCHEMA);
+ }
+
+ if (schema.getIf() != null) {
+ MatchResult result = new JsonSchemaResolver(schema.getIf()).detailedResolve();
+ if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return;
+
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions);
+ if (checker != null) {
+ if (checker.isCorrect()) {
+ JsonSchemaObject then = schema.getThen();
+ if (then == null) {
+ error("Validates against 'if' branch but no 'then' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else {
+ checkObjectBySchemaRecordErrors(then, value);
+ }
+ }
+ else {
+ JsonSchemaObject schemaElse = schema.getElse();
+ if (schemaElse == null) {
+ error("Validates counter 'if' branch but no 'else' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else {
+ checkObjectBySchemaRecordErrors(schemaElse, value);
+ }
+ }
+ }
+ }
+ }
+
+ private void checkObjectBySchemaRecordErrors(@NotNull JsonSchemaObject schema, @NotNull JsonValueAdapter object) {
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(object, new JsonSchemaResolver(schema).detailedResolve(), myOptions);
+ if (checker != null) {
+ myHadTypeError = checker.isHadTypeError();
+ myErrors.putAll(checker.getErrors());
+ }
+ }
+
+ private void checkObject(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) {
+ final JsonObjectValueAdapter object = value.getAsObject();
+ if (object == null) return;
+
+ final List<JsonPropertyAdapter> propertyList = object.getPropertyList();
+ final Set<String> set = new HashSet<>();
+ for (JsonPropertyAdapter property : propertyList) {
+ final String name = StringUtil.notNullize(property.getName());
+ JsonSchemaObject propertyNamesSchema = schema.getPropertyNamesSchema();
+ if (propertyNamesSchema != null) {
+ JsonValueAdapter nameValueAdapter = property.getNameValueAdapter();
+ if (nameValueAdapter != null) {
+ checkByScheme(nameValueAdapter, propertyNamesSchema);
+ }
+ }
+
+ final JsonSchemaVariantsTreeBuilder.Step step = JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(name);
+ final Pair<ThreeState, JsonSchemaObject> pair = step.step(schema, true);
+ if (ThreeState.NO.equals(pair.getFirst()) && !set.contains(name)) {
+ error(JsonBundle.message("json.schema.annotation.not.allowed.property", name), property.getDelegate(),
+ JsonValidationError.FixableIssueKind.ProhibitedProperty,
+ new JsonValidationError.ProhibitedPropertyIssueData(name), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else if (ThreeState.UNSURE.equals(pair.getFirst())) {
+ for (JsonValueAdapter propertyValue : property.getValues()) {
+ checkObjectBySchemaRecordErrors(pair.getSecond(), propertyValue);
+ }
+ }
+ set.add(name);
+ }
+
+ if (object.shouldCheckIntegralRequirements()) {
+ final Set<String> required = schema.getRequired();
+ if (required != null) {
+ HashSet<String> requiredNames = ContainerUtil.newHashSet(required);
+ requiredNames.removeAll(set);
+ if (!requiredNames.isEmpty()) {
+ JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, requiredNames);
+ error("Missing required " + data.getMessage(false), value.getDelegate(), JsonValidationError.FixableIssueKind.MissingProperty, data,
+ JsonErrorPriority.MISSING_PROPS);
+ }
+ }
+ if (schema.getMinProperties() != null && propertyList.size() < schema.getMinProperties()) {
+ error("Number of properties is less than " + schema.getMinProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (schema.getMaxProperties() != null && propertyList.size() > schema.getMaxProperties()) {
+ error("Number of properties is greater than " + schema.getMaxProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ final Map<String, List<String>> dependencies = schema.getPropertyDependencies();
+ if (dependencies != null) {
+ for (Map.Entry<String, List<String>> entry : dependencies.entrySet()) {
+ if (set.contains(entry.getKey())) {
+ final List<String> list = entry.getValue();
+ HashSet<String> deps = ContainerUtil.newHashSet(list);
+ deps.removeAll(set);
+ if (!deps.isEmpty()) {
+ JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, deps);
+ error("Dependency is violated: " + data.getMessage(false) + " must be specified, since '" + entry.getKey() + "' is specified",
+ value.getDelegate(),
+ JsonValidationError.FixableIssueKind.MissingProperty,
+ data, JsonErrorPriority.MISSING_PROPS);
+ }
+ }
+ }
+ }
+ final Map<String, JsonSchemaObject> schemaDependencies = schema.getSchemaDependencies();
+ if (schemaDependencies != null) {
+ for (Map.Entry<String, JsonSchemaObject> entry : schemaDependencies.entrySet()) {
+ if (set.contains(entry.getKey())) {
+ checkObjectBySchemaRecordErrors(entry.getValue(), value);
+ }
+ }
+ }
+ }
+
+ validateAsJsonSchema(object.getDelegate());
+ }
+
+ @Nullable
+ private static Object getDefaultValueFromEnum(@NotNull JsonSchemaObject propertySchema, @NotNull Ref<Integer> enumCount) {
+ List<Object> enumValues = propertySchema.getEnum();
+ if (enumValues != null) {
+ enumCount.set(enumValues.size());
+ if (enumValues.size() == 1) {
+ Object defaultObject = enumValues.get(0);
+ return defaultObject instanceof String ? StringUtil.unquoteString((String)defaultObject) : defaultObject;
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static JsonValidationError.MissingMultiplePropsIssueData createMissingPropertiesData(@NotNull JsonSchemaObject schema,
+ HashSet<String> requiredNames) {
+ List<JsonValidationError.MissingPropertyIssueData> allProps = ContainerUtil.newArrayList();
+ for (String req: requiredNames) {
+ JsonSchemaObject propertySchema = resolvePropertySchema(schema, req);
+ Object defaultValue = propertySchema == null ? null : propertySchema.getDefault();
+ Ref<Integer> enumCount = Ref.create(0);
+
+ JsonSchemaType type = null;
+
+ if (propertySchema != null) {
+ MatchResult result = null;
+ Object valueFromEnum = getDefaultValueFromEnum(propertySchema, enumCount);
+ if (valueFromEnum != null) {
+ defaultValue = valueFromEnum;
+ }
+ else {
+ result = new JsonSchemaResolver(propertySchema).detailedResolve();
+ if (result.mySchemas.size() == 1) {
+ valueFromEnum = getDefaultValueFromEnum(result.mySchemas.get(0), enumCount);
+ if (valueFromEnum != null) {
+ defaultValue = valueFromEnum;
+ }
+ }
+ }
+ type = propertySchema.getType();
+ if (type == null) {
+ if (result == null) {
+ result = new JsonSchemaResolver(propertySchema).detailedResolve();
+ }
+ if (result.mySchemas.size() == 1) {
+ type = result.mySchemas.get(0).getType();
+ }
+ }
+ }
+ allProps.add(new JsonValidationError.MissingPropertyIssueData(req,
+ type,
+ defaultValue,
+ enumCount.get()));
+ }
+
+ return new JsonValidationError.MissingMultiplePropsIssueData(allProps);
+ }
+
+ private static JsonSchemaObject resolvePropertySchema(@NotNull JsonSchemaObject schema, String req) {
+ if (schema.getProperties().containsKey(req)) {
+ return schema.getProperties().get(req);
+ }
+ else {
+ JsonSchemaObject propertySchema = schema.getMatchingPatternPropertySchema(req);
+ if (propertySchema != null) {
+ return propertySchema;
+ }
+ else {
+ JsonSchemaObject additionalPropertiesSchema = schema.getAdditionalPropertiesSchema();
+ if (additionalPropertiesSchema != null) {
+ return additionalPropertiesSchema;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void validateAsJsonSchema(@NotNull PsiElement objElement) {
+ final JsonObject object = ObjectUtils.tryCast(objElement, JsonObject.class);
+ if (object == null) return;
+
+ if (!JsonSchemaService.isSchemaFile(objElement.getContainingFile())) {
+ return;
+ }
+
+ final VirtualFile schemaFile = object.getContainingFile().getVirtualFile();
+ if (schemaFile == null) return;
+
+ final JsonSchemaObject schemaObject = JsonSchemaService.Impl.get(object.getProject()).getSchemaObjectForSchemaFile(schemaFile);
+ if (schemaObject == null) return;
+
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = JsonOriginalPsiWalker.INSTANCE.findPosition(object, true);
+ if (position == null) return;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = skipProperties(position);
+ // !! not root schema, because we validate the schema written in the file itself
+ final MatchResult result = new JsonSchemaResolver(schemaObject, false, steps).detailedResolve();
+ for (JsonSchemaObject s: result.mySchemas) {
+ reportInvalidPatternProperties(s);
+ reportPatternErrors(s);
+ }
+ result.myExcludingSchemas.stream().flatMap(Collection::stream).filter(s -> schemaFile.equals(s.getSchemaFile()))
+ .forEach(schema -> {
+ reportInvalidPatternProperties(schema);
+ reportPatternErrors(schema);
+ });
+ }
+
+ private void reportPatternErrors(JsonSchemaObject schema) {
+ for (JsonSchemaObject prop : schema.getProperties().values()) {
+ final String patternError = prop.getPatternError();
+ if (patternError == null || prop.getPattern() == null) {
+ continue;
+ }
+
+ final JsonContainer element = prop.getJsonObject();
+ if (!(element instanceof JsonObject) || !element.isValid()) {
+ continue;
+ }
+
+ final JsonProperty pattern = ((JsonObject)element).findProperty("pattern");
+ if (pattern != null) {
+ error(StringUtil.convertLineSeparators(patternError), pattern.getValue(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ private void reportInvalidPatternProperties(JsonSchemaObject schema) {
+ final Map<JsonContainer, String> invalidPatternProperties = schema.getInvalidPatternProperties();
+ if (invalidPatternProperties == null) return;
+
+ for (Map.Entry<JsonContainer, String> entry : invalidPatternProperties.entrySet()) {
+ final JsonContainer element = entry.getKey();
+ if (element == null || !element.isValid()) continue;
+ final PsiElement parent = element.getParent();
+ if (parent instanceof JsonProperty) {
+ error(StringUtil.convertLineSeparators(entry.getValue()), ((JsonProperty)parent).getNameElement(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ private static List<JsonSchemaVariantsTreeBuilder.Step> skipProperties(List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ final Iterator<JsonSchemaVariantsTreeBuilder.Step> iterator = position.iterator();
+ boolean canSkip = true;
+ while (iterator.hasNext()) {
+ final JsonSchemaVariantsTreeBuilder.Step step = iterator.next();
+ if (canSkip && step.isFromObject() && JsonSchemaObject.PROPERTIES.equals(step.getName())) {
+ iterator.remove();
+ canSkip = false;
+ }
+ else {
+ canSkip = true;
+ }
+ }
+ return position;
+ }
+
+ private static boolean checkEnumValue(@NotNull Object object,
+ @NotNull JsonLikePsiWalker walker,
+ @Nullable JsonValueAdapter adapter,
+ @NotNull String text,
+ @NotNull BiFunction<String, String, Boolean> stringEq) {
+ if (object instanceof EnumArrayValueWrapper) {
+ if (adapter instanceof JsonArrayValueAdapter) {
+ List<JsonValueAdapter> elements = ((JsonArrayValueAdapter)adapter).getElements();
+ Object[] values = ((EnumArrayValueWrapper)object).getValues();
+ if (elements.size() == values.length) {
+ for (int i = 0; i < values.length; i++) {
+ if (!checkEnumValue(values[i], walker, elements.get(i), walker.getNodeTextForValidation(elements.get(i).getDelegate()), stringEq)) return false;
+ }
+ return true;
+ }
+ }
+ }
+ else if (object instanceof EnumObjectValueWrapper) {
+ if (adapter instanceof JsonObjectValueAdapter) {
+ List<JsonPropertyAdapter> props = ((JsonObjectValueAdapter)adapter).getPropertyList();
+ Map<String, Object> values = ((EnumObjectValueWrapper)object).getValues();
+ if (props.size() == values.size()) {
+ for (JsonPropertyAdapter prop : props) {
+ if (!values.containsKey(prop.getName())) return false;
+ for (JsonValueAdapter value : prop.getValues()) {
+ if (!checkEnumValue(values.get(prop.getName()), walker, value, walker.getNodeTextForValidation(value.getDelegate()), stringEq)) return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+ else {
+ if (walker.onlyDoubleQuotesForStringLiterals()) {
+ if (stringEq.apply(object.toString(), text)) return true;
+ }
+ else {
+ if (equalsIgnoreQuotes(object.toString(), text, walker.quotesForStringLiterals(), stringEq)) return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void checkForEnum(PsiElement value, JsonSchemaObject schema) {
+ List<Object> enumItems = schema.getEnum();
+ if (enumItems == null) return;
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(value, schema);
+ if (walker == null) return;
+ final String text = StringUtil.notNullize(walker.getNodeTextForValidation(value));
+ BiFunction<String, String, Boolean> eq = myOptions.isCaseInsensitiveEnumCheck() ? String::equalsIgnoreCase : String::equals;
+ for (Object object : enumItems) {
+ if (checkEnumValue(object, walker, walker.createValueAdapter(value), text, eq)) return;
+ }
+ error(ENUM_MISMATCH_PREFIX + StringUtil.join(enumItems, o -> o.toString(), ", "), value,
+ JsonValidationError.FixableIssueKind.NonEnumValue, null, JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+
+ private static boolean equalsIgnoreQuotes(@NotNull final String s1,
+ @NotNull final String s2,
+ boolean requireQuotedValues,
+ BiFunction<String, String, Boolean> eq) {
+ final boolean quoted1 = StringUtil.isQuotedString(s1);
+ final boolean quoted2 = StringUtil.isQuotedString(s2);
+ if (requireQuotedValues && quoted1 != quoted2) return false;
+ if (requireQuotedValues && !quoted1) return eq.apply(s1, s2);
+ return eq.apply(StringUtil.unquoteString(s1), StringUtil.unquoteString(s2));
+ }
+
+ private void checkArray(JsonValueAdapter value, JsonSchemaObject schema) {
+ final JsonArrayValueAdapter asArray = value.getAsArray();
+ if (asArray == null) return;
+ final List<JsonValueAdapter> elements = asArray.getElements();
+ if (schema.getMinLength() != null && elements.size() < schema.getMinLength()) {
+ error("Array is shorter than " + schema.getMinLength(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ checkArrayItems(value, elements, schema);
+ }
+
+ @NotNull
+ private static Pair<JsonSchemaObject, JsonSchemaAnnotatorChecker> processSchemasVariants(
+ @NotNull final Collection<? extends JsonSchemaObject> collection,
+ @NotNull final JsonValueAdapter value, boolean isOneOf, JsonComplianceCheckerOptions options) {
+
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ JsonSchemaObject selected = null;
+ if (type == null) {
+ if (!value.isShouldBeIgnored()) checker.typeError(value.getDelegate(), getExpectedTypes(collection));
+ }
+ else {
+ final List<JsonSchemaObject> filtered = ContainerUtil.newArrayListWithCapacity(collection.size());
+ for (JsonSchemaObject schema: collection) {
+ if (!areSchemaTypesCompatible(schema, type)) continue;
+ filtered.add(schema);
+ }
+ if (filtered.isEmpty()) checker.typeError(value.getDelegate(), getExpectedTypes(collection));
+ else if (filtered.size() == 1) {
+ selected = filtered.get(0);
+ checker.checkByScheme(value, selected);
+ }
+ else {
+ if (isOneOf) {
+ selected = checker.processOneOf(value, filtered);
+ }
+ else {
+ selected = checker.processAnyOf(value, filtered);
+ }
+ }
+ }
+ return Pair.create(selected, checker);
+ }
+
+ private final static JsonSchemaType[] NO_TYPES = new JsonSchemaType[0];
+ private static JsonSchemaType[] getExpectedTypes(final Collection<? extends JsonSchemaObject> schemas) {
+ final List<JsonSchemaType> list = new ArrayList<>();
+ for (JsonSchemaObject schema : schemas) {
+ final JsonSchemaType type = schema.getType();
+ if (type != null) {
+ list.add(type);
+ } else {
+ final Set<JsonSchemaType> variants = schema.getTypeVariants();
+ if (variants != null) {
+ list.addAll(variants);
+ }
+ }
+ }
+ return list.isEmpty() ? NO_TYPES : list.toArray(NO_TYPES);
+ }
+
+ public static boolean areSchemaTypesCompatible(@NotNull final JsonSchemaObject schema, @NotNull final JsonSchemaType type) {
+ final JsonSchemaType matchingSchemaType = getMatchingSchemaType(schema, type);
+ if (matchingSchemaType != null) return matchingSchemaType.equals(type);
+ if (schema.getEnum() != null) {
+ return PRIMITIVE_TYPES.contains(type);
+ }
+ return true;
+ }
+
+ @Nullable
+ private static JsonSchemaType getMatchingSchemaType(@NotNull JsonSchemaObject schema, @NotNull JsonSchemaType input) {
+ if (schema.getType() != null) {
+ final JsonSchemaType matchType = schema.getType();
+ if (matchType != null) {
+ if (JsonSchemaType._integer.equals(input) && JsonSchemaType._number.equals(matchType)) {
+ return input;
+ }
+ if (JsonSchemaType._string_number.equals(input) && (JsonSchemaType._number.equals(matchType)
+ || JsonSchemaType._integer.equals(matchType)
+ || JsonSchemaType._string.equals(matchType))) {
+ return input;
+ }
+ return matchType;
+ }
+ }
+ if (schema.getTypeVariants() != null) {
+ Set<JsonSchemaType> matchTypes = schema.getTypeVariants();
+ if (matchTypes.contains(input)) {
+ return input;
+ }
+ if (JsonSchemaType._integer.equals(input) && matchTypes.contains(JsonSchemaType._number)) {
+ return input;
+ }
+ if (JsonSchemaType._string_number.equals(input) &&
+ (matchTypes.contains(JsonSchemaType._number)
+ || matchTypes.contains(JsonSchemaType._integer)
+ || matchTypes.contains(JsonSchemaType._string))) {
+ return input;
+ }
+ //nothing matches, lets return one of the list so that other heuristics does not match
+ return matchTypes.iterator().next();
+ }
+ if (!schema.getProperties().isEmpty() && JsonSchemaType._object.equals(input)) return JsonSchemaType._object;
+ return null;
+ }
+
+ private void checkArrayItems(@NotNull JsonValueAdapter array, @NotNull final List<JsonValueAdapter> list, final JsonSchemaObject schema) {
+ if (schema.isUniqueItems()) {
+ final MultiMap<String, JsonValueAdapter> valueTexts = new MultiMap<>();
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(array.getDelegate(), schema);
+ assert walker != null;
+ for (JsonValueAdapter adapter : list) {
+ valueTexts.putValue(walker.getNodeTextForValidation(adapter.getDelegate()), adapter);
+ }
+
+ for (Map.Entry<String, Collection<JsonValueAdapter>> entry: valueTexts.entrySet()) {
+ if (entry.getValue().size() > 1) {
+ for (JsonValueAdapter item: entry.getValue()) {
+ error("Item is not unique", item.getDelegate(), JsonErrorPriority.TYPE_MISMATCH);
+ }
+ }
+ }
+ }
+ if (schema.getContainsSchema() != null) {
+ boolean match = false;
+ for (JsonValueAdapter item: list) {
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(item, new JsonSchemaResolver(schema.getContainsSchema()).detailedResolve(), myOptions);
+ if (checker == null || checker.myErrors.size() == 0 && !checker.myHadTypeError) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ error("No match for 'contains' rule", array.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+ }
+ if (schema.getItemsSchema() != null) {
+ for (JsonValueAdapter item : list) {
+ checkObjectBySchemaRecordErrors(schema.getItemsSchema(), item);
+ }
+ }
+ else if (schema.getItemsSchemaList() != null) {
+ final Iterator<JsonSchemaObject> iterator = schema.getItemsSchemaList().iterator();
+ for (JsonValueAdapter arrayValue : list) {
+ if (iterator.hasNext()) {
+ checkObjectBySchemaRecordErrors(iterator.next(), arrayValue);
+ }
+ else {
+ if (!Boolean.TRUE.equals(schema.getAdditionalItemsAllowed())) {
+ error("Additional items are not allowed", arrayValue.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else if (schema.getAdditionalItemsSchema() != null) {
+ checkObjectBySchemaRecordErrors(schema.getAdditionalItemsSchema(), arrayValue);
+ }
+ }
+ }
+ }
+ if (schema.getMinItems() != null && list.size() < schema.getMinItems()) {
+ error("Array is shorter than " + schema.getMinItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (schema.getMaxItems() != null && list.size() > schema.getMaxItems()) {
+ error("Array is longer than " + schema.getMaxItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+
+ private static boolean hasMinMaxLengthChecks(JsonSchemaObject schema) {
+ return schema.getMinLength() != null || schema.getMaxLength() != null;
+ }
+
+ private void checkString(PsiElement propValue, JsonSchemaObject schema) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema);
+ assert walker != null;
+ final String value = StringUtil.unquoteString(walker.getNodeTextForValidation(propValue));
+ if (schema.getMinLength() != null) {
+ if (value.length() < schema.getMinLength()) {
+ error("String is shorter than " + schema.getMinLength(), propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+ if (schema.getMaxLength() != null) {
+ if (value.length() > schema.getMaxLength()) {
+ error("String is longer than " + schema.getMaxLength(), propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+ if (schema.getPattern() != null) {
+ if (schema.getPatternError() != null) {
+ error("Can not check string by pattern because of error: " + StringUtil.convertLineSeparators(schema.getPatternError()),
+ propValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (!schema.checkByPattern(value)) {
+ error("String is violating the pattern: '" + StringUtil.convertLineSeparators(schema.getPattern()) + "'", propValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ // I think we are not gonna to support format, there are a couple of RFCs there to check upon..
+ /*
+ if (schema.getFormat() != null) {
+ LOG.info("Unsupported property used: 'format'");
+ }*/
+ }
+
+ private void checkNumber(PsiElement propValue, JsonSchemaObject schema, JsonSchemaType schemaType) {
+ Number value;
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema);
+ assert walker != null;
+ String valueText = walker.getNodeTextForValidation(propValue);
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ try {
+ value = Integer.valueOf(valueText);
+ }
+ catch (NumberFormatException e) {
+ error("Integer value expected", propValue,
+ JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH);
+ return;
+ }
+ }
+ else {
+ try {
+ value = Double.valueOf(valueText);
+ }
+ catch (NumberFormatException e) {
+ if (!JsonSchemaType._string_number.equals(schemaType)) {
+ error("Double value expected", propValue,
+ JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH);
+ }
+ return;
+ }
+ }
+ final Number multipleOf = schema.getMultipleOf();
+ if (multipleOf != null) {
+ final double leftOver = value.doubleValue() % multipleOf.doubleValue();
+ if (leftOver > 0.000001) {
+ final String multipleOfValue = String.valueOf(Math.abs(multipleOf.doubleValue() - multipleOf.intValue()) < 0.000001 ?
+ multipleOf.intValue() : multipleOf);
+ error("Is not multiple of " + multipleOfValue, propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+
+ checkMinimum(schema, value, propValue, schemaType);
+ checkMaximum(schema, value, propValue, schemaType);
+ }
+
+ private void checkMaximum(JsonSchemaObject schema, Number value, PsiElement propertyValue,
+ @NotNull JsonSchemaType propValueType) {
+
+ Number exclusiveMaximumNumber = schema.getExclusiveMaximumNumber();
+ if (exclusiveMaximumNumber != null) {
+ if (JsonSchemaType._integer.equals(propValueType)) {
+ final int intValue = exclusiveMaximumNumber.intValue();
+ if (value.intValue() >= intValue) {
+ error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ final double doubleValue = exclusiveMaximumNumber.doubleValue();
+ if (value.doubleValue() >= doubleValue) {
+ error("Greater than an exclusive maximum " + exclusiveMaximumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ Number maximum = schema.getMaximum();
+ if (maximum == null) return;
+ boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMaximum());
+ if (JsonSchemaType._integer.equals(propValueType)) {
+ final int intValue = maximum.intValue();
+ if (isExclusive) {
+ if (value.intValue() >= intValue) {
+ error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.intValue() > intValue) {
+ error("Greater than a maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ else {
+ final double doubleValue = maximum.doubleValue();
+ if (isExclusive) {
+ if (value.doubleValue() >= doubleValue) {
+ error("Greater than an exclusive maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.doubleValue() > doubleValue) {
+ error("Greater than a maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ }
+
+ private void checkMinimum(JsonSchemaObject schema, Number value, PsiElement propertyValue,
+ @NotNull JsonSchemaType schemaType) {
+ // schema v6 - exclusiveMinimum is numeric now
+ Number exclusiveMinimumNumber = schema.getExclusiveMinimumNumber();
+ if (exclusiveMinimumNumber != null) {
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ final int intValue = exclusiveMinimumNumber.intValue();
+ if (value.intValue() <= intValue) {
+ error("Less than an exclusive minimum" + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ final double doubleValue = exclusiveMinimumNumber.doubleValue();
+ if (value.doubleValue() <= doubleValue) {
+ error("Less than an exclusive minimum " + exclusiveMinimumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ Number minimum = schema.getMinimum();
+ if (minimum == null) return;
+ boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMinimum());
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ final int intValue = minimum.intValue();
+ if (isExclusive) {
+ if (value.intValue() <= intValue) {
+ error("Less than an exclusive minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.intValue() < intValue) {
+ error("Less than a minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ else {
+ final double doubleValue = minimum.doubleValue();
+ if (isExclusive) {
+ if (value.doubleValue() <= doubleValue) {
+ error("Less than an exclusive minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.doubleValue() < doubleValue) {
+ error("Less than a minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ }
+
+ // returns the schema, selected for annotation
+ private JsonSchemaObject processOneOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> oneOf) {
+ final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> correct = new SmartList<>();
+ for (JsonSchemaObject object : oneOf) {
+ // skip it if something JS awaited, we do not process it currently
+ if (object.isShouldValidateAgainstJSType()) continue;
+
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions);
+ checker.checkByScheme(value, object);
+
+ if (checker.isCorrect()) {
+ candidateErroneousCheckers.clear();
+ candidateErroneousSchemas.clear();
+ correct.add(object);
+ }
+ else {
+ candidateErroneousCheckers.add(checker);
+ candidateErroneousSchemas.add(object);
+ }
+ }
+ if (correct.size() == 1) return correct.get(0);
+ if (correct.size() > 0) {
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ if (type != null) {
+ // also check maybe some currently not checked properties like format are different with schemes
+ // todo note that JsonSchemaObject#equals is broken by design, so normally it shouldn't be used until rewritten
+ // but for now we use it here to avoid similar schemas being marked as duplicates
+ if (ContainerUtil.newHashSet(correct).size() > 1 && !schemesDifferWithNotCheckedProperties(correct)) {
+ error("Validates to more than one variant", value.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+ }
+ return ContainerUtil.getLastItem(correct);
+ }
+
+ return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, true);
+ }
+
+ private static boolean schemesDifferWithNotCheckedProperties(@NotNull final List<JsonSchemaObject> list) {
+ return list.stream().anyMatch(s -> !StringUtil.isEmptyOrSpaces(s.getFormat()));
+ }
+
+ private enum AverageFailureAmount {
+ Light,
+ MissingItems,
+ Medium,
+ Hard,
+ NotSchema
+ }
+
+ @NotNull
+ private static AverageFailureAmount getAverageFailureAmount(@NotNull JsonSchemaAnnotatorChecker checker) {
+ int lowPriorityCount = 0;
+ boolean hasMedium = false;
+ boolean hasMissing = false;
+ boolean hasHard = false;
+ Collection<JsonValidationError> values = checker.getErrors().values();
+ for (JsonValidationError value: values) {
+ switch (value.getPriority()) {
+ case LOW_PRIORITY:
+ lowPriorityCount++;
+ break;
+ case MISSING_PROPS:
+ hasMissing = true;
+ break;
+ case MEDIUM_PRIORITY:
+ hasMedium = true;
+ break;
+ case TYPE_MISMATCH:
+ hasHard = true;
+ break;
+ case NOT_SCHEMA:
+ return AverageFailureAmount.NotSchema;
+ }
+ }
+
+ if (hasHard) {
+ return AverageFailureAmount.Hard;
+ }
+
+ // missing props should win against other conditions
+ if (hasMissing) {
+ return AverageFailureAmount.MissingItems;
+ }
+
+ if (hasMedium) {
+ return AverageFailureAmount.Medium;
+ }
+
+ return lowPriorityCount <= 3 ? AverageFailureAmount.Light : AverageFailureAmount.Medium;
+ }
+
+ // returns the schema, selected for annotation
+ private JsonSchemaObject processAnyOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> anyOf) {
+ final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList();
+
+ for (JsonSchemaObject object : anyOf) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions);
+ checker.checkByScheme(value, object);
+ if (checker.isCorrect()) {
+ return object;
+ }
+ // maybe we still find the correct schema - continue to iterate
+ candidateErroneousCheckers.add(checker);
+ candidateErroneousSchemas.add(object);
+ }
+
+ return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, false);
+ }
+
+ /**
+ * Filters schema validation results to get the result with the "minimal" amount of errors.
+ * This is needed in case of oneOf or anyOf conditions, when there exist no match.
+ * I.e., when we have multiple schema candidates, but none is applicable.
+ * In this case we need to show the most "suitable" error messages
+ * - by detecting the most "likely" schema corresponding to the current entity
+ */
+ @Nullable
+ private JsonSchemaObject showErrorsAndGetLeastErroneous(@NotNull List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers,
+ @NotNull List<JsonSchemaObject> candidateErroneousSchemas,
+ boolean isOneOf) {
+ JsonSchemaObject current = null;
+ JsonSchemaObject currentWithMinAverage = null;
+ Optional<AverageFailureAmount> minAverage = candidateErroneousCheckers.stream()
+ .map(c -> getAverageFailureAmount(c))
+ .min(Comparator.comparingInt(c -> c.ordinal()));
+ int min = minAverage.orElse(AverageFailureAmount.Hard).ordinal();
+
+ int minErrorCount = candidateErroneousCheckers.stream().map(c -> c.getErrors().size()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
+
+ MultiMap<PsiElement, JsonValidationError> errorsWithMinAverage = MultiMap.create();
+ MultiMap<PsiElement, JsonValidationError> allErrors = MultiMap.create();
+ for (int i = 0; i < candidateErroneousCheckers.size(); i++) {
+ JsonSchemaAnnotatorChecker checker = candidateErroneousCheckers.get(i);
+ final boolean isMoreThanMinErrors = checker.getErrors().size() > minErrorCount;
+ final boolean isMoreThanAverage = getAverageFailureAmount(checker).ordinal() > min;
+ if (!isMoreThanMinErrors) {
+ if (isMoreThanAverage) {
+ currentWithMinAverage = candidateErroneousSchemas.get(i);
+ }
+ else {
+ current = candidateErroneousSchemas.get(i);
+ }
+
+ for (Map.Entry<PsiElement, JsonValidationError> entry: checker.getErrors().entrySet()) {
+ (isMoreThanAverage ? errorsWithMinAverage : allErrors).putValue(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ if (allErrors.isEmpty()) allErrors = errorsWithMinAverage;
+
+ for (Map.Entry<PsiElement, Collection<JsonValidationError>> entry : allErrors.entrySet()) {
+ Collection<JsonValidationError> value = entry.getValue();
+ if (value.size() == 0) continue;
+ if (value.size() == 1) {
+ error(entry.getKey(), value.iterator().next());
+ continue;
+ }
+ JsonValidationError error = tryMergeErrors(value, isOneOf);
+ if (error != null) {
+ error(entry.getKey(), error);
+ }
+ else {
+ for (JsonValidationError validationError : value) {
+ error(entry.getKey(), validationError);
+ }
+ }
+ }
+
+ if (current == null) {
+ current = currentWithMinAverage;
+ }
+ if (current == null) {
+ current = ContainerUtil.getLastItem(candidateErroneousSchemas);
+ }
+
+ return current;
+ }
+
+ @Nullable
+ private static JsonValidationError tryMergeErrors(@NotNull Collection<JsonValidationError> errors, boolean isOneOf) {
+ JsonValidationError.FixableIssueKind commonIssueKind = null;
+ for (JsonValidationError error : errors) {
+ JsonValidationError.FixableIssueKind currentIssueKind = error.getFixableIssueKind();
+ if (currentIssueKind == JsonValidationError.FixableIssueKind.None) return null;
+ else if (commonIssueKind == null) commonIssueKind = currentIssueKind;
+ else if (currentIssueKind != commonIssueKind) return null;
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.NonEnumValue) {
+ return new JsonValidationError(ENUM_MISMATCH_PREFIX
+ + errors
+ .stream()
+ .map(e -> StringUtil.trimStart(e.getMessage(), ENUM_MISMATCH_PREFIX))
+ .map(e -> StringUtil.split(e, ", "))
+ .flatMap(e -> e.stream())
+ .distinct()
+ .collect(Collectors.joining(", ")), commonIssueKind, null, errors.iterator().next().getPriority());
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.MissingProperty) {
+ String prefix = isOneOf ? "One of the following property sets is required: " : "Should have at least one of the following property sets: ";
+ return new JsonValidationError(prefix +
+ errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData())
+ .map(d -> d.getMessage(false)).collect(Collectors.joining(", or ")),
+ isOneOf ? JsonValidationError.FixableIssueKind.MissingOneOfProperty : JsonValidationError.FixableIssueKind.MissingAnyOfProperty,
+ new JsonValidationError.MissingOneOfPropsIssueData(errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData()).collect(
+ Collectors.toList())), errors.iterator().next().getPriority());
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.ProhibitedType) {
+ final Set<JsonSchemaType> allTypes = errors.stream().map(e -> (JsonValidationError.TypeMismatchIssueData)e.getIssueData())
+ .flatMap(d -> Arrays.stream(d.expectedTypes)).collect(Collectors.toSet());
+
+ if (allTypes.size() == 1) return errors.iterator().next();
+
+ String commonTypeMessage = "Type is not allowed. Expected one of: " + allTypes.stream().map(t -> t.getDescription()).sorted().collect(Collectors.joining(", ")) + ".";
+ return new JsonValidationError(commonTypeMessage, JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(ContainerUtil.toArray(allTypes, JsonSchemaType[]::new)),
+ errors.iterator().next().getPriority());
+ }
+
+ return null;
+ }
+
+ public boolean isCorrect() {
+ return myErrors.isEmpty();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java
new file mode 100644
index 00000000..39992c7d
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java
@@ -0,0 +1,58 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.psi.impl.source.resolve.ResolveCache;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 3/31/2016.
+ */
+public abstract class JsonSchemaBaseReference<T extends PsiElement> extends PsiReferenceBase<T> {
+ public JsonSchemaBaseReference(T element, TextRange textRange) {
+ super(element, textRange, true);
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolve() {
+ return ResolveCache.getInstance(getElement().getProject()).resolveWithCaching(this, MyResolver.INSTANCE, false, false);
+ }
+
+ @Nullable
+ public abstract PsiElement resolveInner();
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JsonSchemaBaseReference that = (JsonSchemaBaseReference)o;
+
+ return isIdenticalTo(that);
+ }
+
+ protected boolean isIdenticalTo(JsonSchemaBaseReference that) {
+ return myElement.equals(that.myElement);
+ }
+
+ @Override
+ public int hashCode() {
+ return myElement.hashCode();
+ }
+
+ private static class MyResolver implements ResolveCache.Resolver {
+ private static final MyResolver INSTANCE = new MyResolver();
+
+ @Override
+ @Nullable
+ public PsiElement resolve(@NotNull PsiReference ref, boolean incompleteCode) {
+ return ((JsonSchemaBaseReference)ref).resolveInner();
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java
new file mode 100644
index 00000000..cbe848a6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java
@@ -0,0 +1,672 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.AutoPopupController;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.DataManager;
+import com.intellij.internal.statistic.service.fus.collectors.FUSApplicationUsageTrigger;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Caret;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorModificationUtil;
+import com.intellij.openapi.editor.SelectionModel;
+import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.editor.actionSystem.EditorActionManager;
+import com.intellij.openapi.editor.actions.EditorActionUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+import com.intellij.util.Consumer;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.SchemaType;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
+
+import javax.swing.*;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 10/1/2015.
+ */
+public class JsonSchemaCompletionContributor extends CompletionContributor {
+ private static final String BUILTIN_USAGE_KEY = "json.schema.builtin.completion";
+ private static final String SCHEMA_USAGE_KEY = "json.schema.schema.completion";
+ private static final String USER_USAGE_KEY = "json.schema.user.completion";
+ private static final String REMOTE_USAGE_KEY = "json.schema.remote.completion";
+
+ @Override
+ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
+ final PsiElement position = parameters.getPosition();
+ final VirtualFile file = PsiUtilCore.getVirtualFile(position);
+ if (file == null) return;
+
+ final JsonSchemaService service = JsonSchemaService.Impl.get(position.getProject());
+ if (!service.isApplicableToFile(file)) return;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(file);
+ if (rootSchema == null) return;
+ PsiElement positionParent = position.getParent();
+ if (positionParent != null) {
+ PsiElement parent = positionParent.getParent();
+ if (parent instanceof JsonProperty && "$ref".equals(((JsonProperty)parent).getName()) && service.isSchemaFile(file)) {
+ return;
+ }
+ }
+
+ final VirtualFile schemaFile = rootSchema.getSchemaFile();
+ updateStat(service.getSchemaProvider(schemaFile), schemaFile);
+ doCompletion(parameters, result, rootSchema);
+ }
+
+ public static void doCompletion(@NotNull final CompletionParameters parameters,
+ @NotNull final CompletionResultSet result,
+ @NotNull final JsonSchemaObject rootSchema) {
+ doCompletion(parameters, result, rootSchema, true);
+ }
+
+ public static void doCompletion(@NotNull final CompletionParameters parameters,
+ @NotNull final CompletionResultSet result,
+ @NotNull final JsonSchemaObject rootSchema,
+ boolean stop) {
+ final PsiElement completionPosition = parameters.getOriginalPosition() != null ? parameters.getOriginalPosition() :
+ parameters.getPosition();
+ new Worker(rootSchema, parameters.getPosition(), completionPosition, result).work();
+ if (stop) {
+ result.stopHere();
+ }
+ }
+
+ @TestOnly
+ @NotNull
+ public static List<LookupElement> getCompletionVariants(@NotNull final JsonSchemaObject schema,
+ @NotNull final PsiElement position, @NotNull final PsiElement originalPosition) {
+ final List<LookupElement> result = new ArrayList<>();
+ new Worker(schema, position, originalPosition, element -> result.add(element)).work();
+ return result;
+ }
+
+ private static void updateStat(@Nullable JsonSchemaFileProvider provider, VirtualFile schemaFile) {
+ if (provider == null) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ // auto-detected and auto-downloaded JSON schemas
+ FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance();
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY);
+ }
+ return;
+ }
+ final SchemaType schemaType = provider.getSchemaType();
+ FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance();
+ switch (schemaType) {
+ case schema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, SCHEMA_USAGE_KEY);
+ break;
+ case userSchema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, USER_USAGE_KEY);
+ break;
+ case embeddedSchema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, BUILTIN_USAGE_KEY);
+ break;
+ case remoteSchema:
+ // this works only for user-specified remote schemas in our settings, but not for auto-detected remote schemas
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY);
+ break;
+ }
+ }
+
+ private static class Worker {
+ @NotNull private final JsonSchemaObject myRootSchema;
+ @NotNull private final PsiElement myPosition;
+ @NotNull private final PsiElement myOriginalPosition;
+ @NotNull private final Consumer<LookupElement> myResultConsumer;
+ private final boolean myWrapInQuotes;
+ private final boolean myInsideStringLiteral;
+ // we need this set to filter same-named suggestions (they can be suggested by several matching schemes)
+ private final Set<LookupElement> myVariants;
+ private final JsonLikePsiWalker myWalker;
+
+ Worker(@NotNull JsonSchemaObject rootSchema, @NotNull PsiElement position,
+ @NotNull PsiElement originalPosition, @NotNull final Consumer<LookupElement> resultConsumer) {
+ myRootSchema = rootSchema;
+ myPosition = position;
+ myOriginalPosition = originalPosition;
+ myResultConsumer = resultConsumer;
+ myVariants = new HashSet<>();
+ myWalker = JsonLikePsiWalker.getWalker(myPosition, myRootSchema);
+ myWrapInQuotes = myWalker != null && myWalker.isNameQuoted() && !(position.getParent() instanceof JsonStringLiteral);
+ myInsideStringLiteral = position.getParent() instanceof JsonStringLiteral;
+ }
+
+ public void work() {
+ if (myWalker == null) return;
+ final PsiElement checkable = myWalker.goUpToCheckable(myPosition);
+ if (checkable == null) return;
+ final ThreeState isName = myWalker.isName(checkable);
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(checkable, isName == ThreeState.NO);
+ if (position == null || position.isEmpty() && isName == ThreeState.NO) return;
+
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(myRootSchema, false, position).resolve();
+ final Set<String> knownNames = ContainerUtil.newHashSet();
+ // too long here, refactor further
+ schemas.forEach(schema -> {
+ if (isName != ThreeState.NO) {
+ final boolean insertComma = myWalker.hasPropertiesBehindAndNoComma(myPosition);
+ final boolean hasValue = myWalker.isPropertyWithValue(checkable);
+
+ final Collection<String> properties = myWalker.getPropertyNamesOfParentObject(myOriginalPosition, myPosition);
+ final JsonPropertyAdapter adapter = myWalker.getParentPropertyAdapter(myOriginalPosition);
+
+ final Map<String, JsonSchemaObject> schemaProperties = schema.getProperties();
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaProperties, knownNames);
+ addIfThenElsePropertyNameVariants(schema, insertComma, hasValue, properties, adapter, knownNames);
+ }
+
+ if (isName != ThreeState.YES) {
+ suggestValues(schema, isName == ThreeState.NO);
+ }
+ });
+
+ for (LookupElement variant : myVariants) {
+ myResultConsumer.consume(variant);
+ }
+ }
+
+ private void addIfThenElsePropertyNameVariants(@NotNull JsonSchemaObject schema,
+ boolean insertComma,
+ boolean hasValue,
+ @NotNull Collection<String> properties,
+ @Nullable JsonPropertyAdapter adapter,
+ Set<String> knownNames) {
+ if (schema.getIf() == null) return;
+
+ JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(myPosition, schema);
+ JsonPropertyAdapter propertyAdapter = walker == null ? null : walker.getParentPropertyAdapter(myPosition);
+ if (propertyAdapter == null) return;
+
+ JsonObjectValueAdapter object = propertyAdapter.getParentObject();
+ if (object == null) return;
+
+ JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK);
+ checker.checkByScheme(object, schema.getIf());
+ if (checker.isCorrect()) {
+ JsonSchemaObject then = schema.getThen();
+ if (then != null) {
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, then.getProperties(), knownNames);
+ }
+ }
+ else {
+ JsonSchemaObject schemaElse = schema.getElse();
+ if (schemaElse != null) {
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaElse.getProperties(), knownNames);
+ }
+ }
+ }
+
+ private void addAllPropertyVariants(boolean insertComma,
+ boolean hasValue,
+ Collection<String> properties,
+ JsonPropertyAdapter adapter,
+ Map<String, JsonSchemaObject> schemaProperties, Set<String> knownNames) {
+ schemaProperties.keySet().stream()
+ .filter(name -> !properties.contains(name) && !knownNames.contains(name) || adapter != null && name.equals(adapter.getName()))
+ .forEach(name -> {knownNames.add(name); addPropertyVariant(name, schemaProperties.get(name), hasValue, insertComma);});
+ }
+
+ private void suggestValues(JsonSchemaObject schema, boolean isSurelyValue) {
+ suggestValuesForSchemaVariants(schema.getAnyOf(), isSurelyValue);
+ suggestValuesForSchemaVariants(schema.getOneOf(), isSurelyValue);
+ suggestValuesForSchemaVariants(schema.getAllOf(), isSurelyValue);
+
+ if (schema.getEnum() != null) {
+ for (Object o : schema.getEnum()) {
+ if (myInsideStringLiteral && !(o instanceof String)) continue;
+ addValueVariant(o.toString(), null);
+ }
+ }
+ else if (isSurelyValue) {
+ final JsonSchemaType type = schema.guessType();
+ suggestSpecialValues(type);
+ if (type != null) {
+ suggestByType(schema, type);
+ } else if (schema.getTypeVariants() != null) {
+ for (JsonSchemaType schemaType : schema.getTypeVariants()) {
+ suggestByType(schema, schemaType);
+ }
+ }
+ }
+ }
+
+ private void suggestSpecialValues(@Nullable JsonSchemaType type) {
+ if (JsonSchemaVersion.isSchemaSchemaId(myRootSchema.getId()) && type == JsonSchemaType._string) {
+ JsonPropertyAdapter propertyAdapter = myWalker.getParentPropertyAdapter(myOriginalPosition);
+ if (propertyAdapter == null || !"required".equals(propertyAdapter.getName())) return;
+ PsiElement checkable = myWalker.goUpToCheckable(myPosition);
+ if (!(checkable instanceof JsonStringLiteral) && !(checkable instanceof JsonReferenceExpression)) return;
+ JsonObject propertiesObject = JsonRequiredPropsReferenceProvider.findPropertiesObject(checkable);
+ if (propertiesObject == null) return;
+ PsiElement parent = checkable.getParent();
+ Set<String> items = parent instanceof JsonArray ? ((JsonArray)parent).getValueList().stream()
+ .filter(v -> v instanceof JsonStringLiteral).map(v -> ((JsonStringLiteral)v).getValue()).collect(Collectors.toSet()) : ContainerUtil.newHashSet();
+ propertiesObject.getPropertyList().stream().map(p -> p.getName()).filter(n -> !items.contains(n)).forEach(n -> addStringVariant(n));
+ }
+ }
+
+ private void suggestByType(JsonSchemaObject schema, JsonSchemaType type) {
+ if (JsonSchemaType._string.equals(type)) {
+ addPossibleStringValue(schema);
+ }
+ if (myInsideStringLiteral){
+ return;
+ }
+ if (JsonSchemaType._boolean.equals(type)) {
+ addPossibleBooleanValue(type);
+ } else if (JsonSchemaType._null.equals(type)) {
+ addValueVariant("null", null);
+ } else if (JsonSchemaType._array.equals(type)) {
+ String value = myWalker.getDefaultArrayValue();
+ addValueVariant(value, null,
+ myWalker.defaultArrayValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length()));
+ } else if (JsonSchemaType._object.equals(type)) {
+ String value = myWalker.getDefaultObjectValue();
+ addValueVariant(value, null,
+ myWalker.defaultObjectValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length()));
+ }
+ }
+
+ private void addPossibleStringValue(JsonSchemaObject schema) {
+ Object defaultValue = schema.getDefault();
+ String defaultValueString = defaultValue == null ? null : defaultValue.toString();
+ addStringVariant(defaultValueString);
+ }
+
+ private void addStringVariant(String defaultValueString) {
+ if (!StringUtil.isEmpty(defaultValueString)) {
+ String normalizedValue = defaultValueString;
+ boolean shouldQuote = myWalker.quotesForStringLiterals();
+ boolean isQuoted = StringUtil.isQuotedString(normalizedValue);
+ if (shouldQuote && !isQuoted) {
+ normalizedValue = StringUtil.wrapWithDoubleQuote(normalizedValue);
+ }
+ else if (!shouldQuote && isQuoted) {
+ normalizedValue = StringUtil.unquoteString(normalizedValue);
+ }
+ addValueVariant(normalizedValue, null);
+ }
+ }
+
+ private void suggestValuesForSchemaVariants(List<JsonSchemaObject> list, boolean isSurelyValue) {
+ if (list != null && list.size() > 0) {
+ for (JsonSchemaObject schemaObject : list) {
+ suggestValues(schemaObject, isSurelyValue);
+ }
+ }
+ }
+
+ private void addPossibleBooleanValue(JsonSchemaType type) {
+ if (JsonSchemaType._boolean.equals(type)) {
+ addValueVariant("true", null);
+ addValueVariant("false", null);
+ }
+ }
+
+
+ private void addValueVariant(@NotNull String key, @SuppressWarnings("SameParameterValue") @Nullable final String description) {
+ addValueVariant(key, description, null, null);
+ }
+
+ private void addValueVariant(@NotNull String key,
+ @SuppressWarnings("SameParameterValue") @Nullable final String description,
+ @Nullable final String altText,
+ @Nullable InsertHandler<LookupElement> handler) {
+ LookupElementBuilder builder = LookupElementBuilder.create(!myWrapInQuotes ? StringUtil.unquoteString(key) : key);
+ if (altText != null) {
+ builder = builder.withPresentableText(altText);
+ }
+ if (description != null) {
+ builder = builder.withTypeText(description);
+ }
+ if (handler != null) {
+ builder = builder.withInsertHandler(handler);
+ }
+ myVariants.add(builder);
+ }
+
+ private void addPropertyVariant(@NotNull String key, @NotNull JsonSchemaObject jsonSchemaObject, boolean hasValue, boolean insertComma) {
+ final Collection<JsonSchemaObject> variants = new JsonSchemaResolver(jsonSchemaObject).resolve();
+ jsonSchemaObject = ObjectUtils.coalesce(ContainerUtil.getFirstItem(variants), jsonSchemaObject);
+ key = !myWrapInQuotes ? key : StringUtil.wrapWithDoubleQuote(key);
+ LookupElementBuilder builder = LookupElementBuilder.create(key);
+
+ final String typeText = JsonSchemaDocumentationProvider.getBestDocumentation(true, jsonSchemaObject);
+ if (!StringUtil.isEmptyOrSpaces(typeText)) {
+ final String text = StringUtil.removeHtmlTags(typeText);
+ final int firstSentenceMark = text.indexOf(". ");
+ builder = builder.withTypeText(firstSentenceMark == -1 ? text : text.substring(0, firstSentenceMark + 1), true);
+ }
+ else {
+ String type = jsonSchemaObject.getTypeDescription(true);
+ if (type != null) {
+ builder = builder.withTypeText(type, true);
+ }
+ }
+
+ builder = builder.withIcon(getIcon(jsonSchemaObject.guessType()));
+
+ if (hasSameType(variants)) {
+ final JsonSchemaType type = jsonSchemaObject.guessType();
+ final List<Object> values = jsonSchemaObject.getEnum();
+ Object defaultValue = jsonSchemaObject.getDefault();
+
+ boolean hasValues = !ContainerUtil.isEmpty(values);
+ if (type != null || hasValues || defaultValue != null) {
+ builder = builder.withInsertHandler(
+ !hasValues || values.stream().map(v -> v.getClass()).distinct().count() == 1 ?
+ createPropertyInsertHandler(jsonSchemaObject, hasValue, insertComma) :
+ createDefaultPropertyInsertHandler(true, insertComma));
+ }
+ else {
+ builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma));
+ }
+ } else if (!hasValue) {
+ builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma));
+ }
+
+ myVariants.add(builder);
+ }
+
+ @NotNull
+ private static Icon getIcon(@Nullable JsonSchemaType type) {
+ if (type == null) return AllIcons.Nodes.Property;
+ switch (type) {
+ case _object:
+ return AllIcons.Json.Object;
+ case _array:
+ return AllIcons.Json.Array;
+ default:
+ return AllIcons.Nodes.Property;
+ }
+ }
+
+ private static boolean hasSameType(@NotNull Collection<JsonSchemaObject> variants) {
+ return variants.stream().map(JsonSchemaObject::guessType).filter(Objects::nonNull).distinct().count() <= 1;
+ }
+
+ private static InsertHandler<LookupElement> createArrayOrObjectLiteralInsertHandler(boolean newline, int insertedTextSize) {
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ Editor editor = context.getEditor();
+
+ if (!newline) {
+ EditorModificationUtil.moveCaretRelatively(editor, -1);
+ }
+ else {
+ EditorModificationUtil.moveCaretRelatively(editor, -insertedTextSize);
+ PsiDocumentManager.getInstance(context.getProject()).commitDocument(editor.getDocument());
+ invokeEnterHandler(editor);
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(editor, null);
+ }
+ };
+ }
+
+ private InsertHandler<LookupElement> createDefaultPropertyInsertHandler(@SuppressWarnings("SameParameterValue") boolean hasValue,
+ boolean insertComma) {
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ ApplicationManager.getApplication().assertWriteAccessAllowed();
+ Editor editor = context.getEditor();
+ Project project = context.getProject();
+
+ if (handleInsideQuotesInsertion(context, editor, hasValue)) return;
+
+ // inserting longer string for proper formatting
+ final String stringToInsert = ": 1" + (insertComma ? "," : "");
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true, 2);
+ formatInsertedString(context, stringToInsert.length());
+ final int offset = editor.getCaretModel().getOffset();
+ context.getDocument().deleteString(offset, offset + 1);
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ };
+ }
+
+ @NotNull
+ private InsertHandler<LookupElement> createPropertyInsertHandler(@NotNull JsonSchemaObject jsonSchemaObject,
+ final boolean hasValue,
+ boolean insertComma) {
+ JsonSchemaType type = jsonSchemaObject.guessType();
+ List<Object> values = jsonSchemaObject.getEnum();
+ if (type == null && values != null && !values.isEmpty()) type = detectType(values);
+ final Object defaultValue = jsonSchemaObject.getDefault();
+ final String defaultValueAsString = defaultValue == null || defaultValue instanceof JsonSchemaObject ? null :
+ (defaultValue instanceof String ? "\"" + defaultValue + "\"" :
+ String.valueOf(defaultValue));
+ JsonSchemaType finalType = type;
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ ApplicationManager.getApplication().assertWriteAccessAllowed();
+ Editor editor = context.getEditor();
+ Project project = context.getProject();
+ String stringToInsert = null;
+ final String comma = insertComma ? "," : "";
+
+ if (handleInsideQuotesInsertion(context, editor, hasValue)) return;
+
+ PsiElement element = context.getFile().findElementAt(editor.getCaretModel().getOffset());
+ boolean insertColon = element == null || !":".equals(element.getText());
+ if (!insertColon) {
+ editor.getCaretModel().moveToOffset(editor.getCaretModel().getOffset() + 1);
+ }
+
+ if (finalType != null) {
+ boolean hadEnter;
+ switch (finalType) {
+ case _object:
+ EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ",
+ false, true,
+ insertColon ? 2 : 1);
+ hadEnter = false;
+ boolean invokeEnter = myWalker.invokeEnterBeforeObjectAndArray();
+ if (insertColon && invokeEnter) {
+ invokeEnterHandler(editor);
+ hadEnter = true;
+ }
+ if (insertColon) {
+ stringToInsert = myWalker.getDefaultObjectValue() + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ hadEnter ? 0 : 1);
+ }
+
+ if (hadEnter || !insertColon) {
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+ if (!hadEnter && stringToInsert != null) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+ if (stringToInsert != null && !invokeEnter) {
+ invokeEnterHandler(editor);
+ }
+ break;
+ case _boolean:
+ String value = String.valueOf(Boolean.TRUE.toString().equals(defaultValueAsString));
+ stringToInsert = (insertColon ? ": " : " ") + value + comma;
+ SelectionModel model = editor.getSelectionModel();
+
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ stringToInsert.length() - comma.length());
+ formatInsertedString(context, stringToInsert.length());
+ int start = editor.getSelectionModel().getSelectionStart();
+ model.setSelection(start - value.length(), start);
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ break;
+ case _array:
+ EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ",
+ false, true,
+ insertColon ? 2 : 1);
+ hadEnter = false;
+ if (insertColon && myWalker.invokeEnterBeforeObjectAndArray()) {
+ invokeEnterHandler(editor);
+ hadEnter = true;
+ }
+ if (insertColon) {
+ stringToInsert = myWalker.getDefaultArrayValue() + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ hadEnter ? 0 : 1);
+ }
+ if (hadEnter) {
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+
+ if (stringToInsert != null) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+ break;
+ case _string:
+ case _integer:
+ insertPropertyWithEnum(context, editor, defaultValueAsString, values, finalType, comma, myWalker, insertColon);
+ break;
+ default:
+ }
+ }
+ else {
+ insertPropertyWithEnum(context, editor, defaultValueAsString, values, null, comma, myWalker, insertColon);
+ }
+ }
+ };
+ }
+
+ private static void invokeEnterHandler(Editor editor) {
+ EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER);
+ Caret caret = editor.getCaretModel().getCurrentCaret();
+ handler.execute(editor, caret,
+ new CaretSpecificDataContext(DataManager.getInstance().getDataContext(editor.getContentComponent()), caret));
+ }
+
+ private boolean handleInsideQuotesInsertion(@NotNull InsertionContext context, @NotNull Editor editor, boolean hasValue) {
+ if (myInsideStringLiteral) {
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = context.getFile().findElementAt(offset);
+ int tailOffset = context.getTailOffset();
+ int guessEndOffset = tailOffset + 1;
+ if (element instanceof LeafPsiElement) {
+ if (handleIncompleteString(editor, element)) return false;
+ int endOffset = element.getTextRange().getEndOffset();
+ if (endOffset > tailOffset) {
+ context.getDocument().deleteString(tailOffset, endOffset - 1);
+ }
+ }
+ if (hasValue) {
+ return true;
+ }
+ editor.getCaretModel().moveToOffset(guessEndOffset);
+ } else editor.getCaretModel().moveToOffset(context.getTailOffset());
+ return false;
+ }
+
+ private static boolean handleIncompleteString(@NotNull Editor editor, @NotNull PsiElement element) {
+ if (((LeafPsiElement)element).getElementType() == TokenType.WHITE_SPACE) {
+ PsiElement prevSibling = element.getPrevSibling();
+ if (prevSibling instanceof JsonProperty) {
+ JsonValue nameElement = ((JsonProperty)prevSibling).getNameElement();
+ if (!nameElement.getText().endsWith("\"")) {
+ editor.getCaretModel().moveToOffset(nameElement.getTextRange().getEndOffset());
+ EditorModificationUtil.insertStringAtCaret(editor, "\"", false, true, 1);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static JsonSchemaType detectType(List<Object> values) {
+ JsonSchemaType type = null;
+ for (Object value : values) {
+ JsonSchemaType newType = null;
+ if (value instanceof Integer) newType = JsonSchemaType._integer;
+ if (type != null && !type.equals(newType)) return null;
+ type = newType;
+ }
+ return type;
+ }
+ }
+
+ private static void insertPropertyWithEnum(InsertionContext context,
+ Editor editor,
+ String defaultValue,
+ List<Object> values,
+ JsonSchemaType type,
+ String comma,
+ JsonLikePsiWalker walker,
+ boolean insertColon) {
+ if (!walker.quotesForStringLiterals() && defaultValue != null) {
+ defaultValue = StringUtil.unquoteString(defaultValue);
+ }
+ final boolean isNumber = type != null && (JsonSchemaType._integer.equals(type) || JsonSchemaType._number.equals(type)) ||
+ type == null && (defaultValue != null &&
+ !StringUtil.isQuotedString(defaultValue) || values != null && ContainerUtil.and(values, v -> !(v instanceof String)));
+ boolean hasValues = !ContainerUtil.isEmpty(values);
+ boolean hasDefaultValue = !StringUtil.isEmpty(defaultValue);
+ boolean hasQuotes = isNumber || !walker.quotesForStringLiterals();
+ final String colonWs = insertColon ? ": " : " ";
+ String stringToInsert = colonWs + (hasDefaultValue ? defaultValue : (hasQuotes ? "" : "\"\"")) + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true,
+ insertColon ? 2 : 1);
+ if (!hasQuotes || hasDefaultValue) {
+ SelectionModel model = editor.getSelectionModel();
+ int caretStart = model.getSelectionStart();
+ int newOffset = caretStart + (hasDefaultValue ? defaultValue.length() : 1);
+ if (hasDefaultValue && !hasQuotes) newOffset--;
+ model.setSelection(hasQuotes ? caretStart : (caretStart + 1), newOffset);
+ editor.getCaretModel().moveToOffset(newOffset);
+ }
+
+ if (!walker.invokeEnterBeforeObjectAndArray() && !stringToInsert.equals(colonWs + comma)) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+
+ if (hasValues) {
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ }
+
+ public static void formatInsertedString(@NotNull InsertionContext context,
+ int offset) {
+ Project project = context.getProject();
+ PsiDocumentManager.getInstance(project).commitDocument(context.getDocument());
+ CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
+ codeStyleManager.reformatText(context.getFile(), context.getStartOffset(), context.getTailOffset() + offset);
+ }
+} \ No newline at end of file
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java
new file mode 100644
index 00000000..12e52c43
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java
@@ -0,0 +1,157 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class JsonSchemaComplianceChecker {
+ private static final Key<Set<PsiElement>> ANNOTATED_PROPERTIES = Key.create("JsonSchema.Properties.Annotated");
+
+ @NotNull private final JsonSchemaObject myRootSchema;
+ @NotNull private final ProblemsHolder myHolder;
+ @NotNull private final JsonLikePsiWalker myWalker;
+ private final LocalInspectionToolSession mySession;
+ @NotNull private final JsonComplianceCheckerOptions myOptions;
+ @Nullable private final String myMessagePrefix;
+
+ public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull JsonLikePsiWalker walker,
+ @NotNull LocalInspectionToolSession session,
+ @NotNull JsonComplianceCheckerOptions options) {
+ this(rootSchema, holder, walker, session, options, null);
+ }
+
+ public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull JsonLikePsiWalker walker,
+ @NotNull LocalInspectionToolSession session,
+ @NotNull JsonComplianceCheckerOptions options,
+ @Nullable String messagePrefix) {
+ myRootSchema = rootSchema;
+ myHolder = holder;
+ myWalker = walker;
+ mySession = session;
+ myOptions = options;
+ myMessagePrefix = messagePrefix;
+ }
+
+ public void annotate(@NotNull final PsiElement element) {
+ final JsonPropertyAdapter firstProp = myWalker.getParentPropertyAdapter(element);
+ if (firstProp != null) {
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(firstProp.getDelegate(), true);
+ if (position == null || position.isEmpty()) return;
+ final MatchResult result = new JsonSchemaResolver(myRootSchema, false, position).detailedResolve();
+ for (JsonValueAdapter value : firstProp.getValues()) {
+ createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(value, result, myOptions));
+ }
+ }
+ checkRoot(element, firstProp);
+ }
+
+ private void checkRoot(@NotNull PsiElement element, @Nullable JsonPropertyAdapter firstProp) {
+ JsonValueAdapter rootToCheck;
+ if (firstProp == null) {
+ rootToCheck = findTopLevelElement(myWalker, element);
+ } else {
+ rootToCheck = firstProp.getParentObject();
+ if (rootToCheck == null || !myWalker.isTopJsonElement(rootToCheck.getDelegate().getParent())) {
+ return;
+ }
+ }
+ if (rootToCheck != null) {
+ final MatchResult matchResult = new JsonSchemaResolver(myRootSchema).detailedResolve();
+ createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(rootToCheck, matchResult, myOptions));
+ }
+ }
+
+ private void createWarnings(@Nullable JsonSchemaAnnotatorChecker checker) {
+ if (checker == null || checker.isCorrect()) return;
+ // compute intersecting ranges - we'll solve warning priorities based on this information
+ List<TextRange> ranges = ContainerUtil.newArrayList();
+ List<List<Map.Entry<PsiElement, JsonValidationError>>> entries = ContainerUtil.newArrayList();
+ for (Map.Entry<PsiElement, JsonValidationError> entry : checker.getErrors().entrySet()) {
+ TextRange range = entry.getKey().getTextRange();
+ boolean processed = false;
+ for (int i = 0; i < ranges.size(); i++) {
+ TextRange currRange = ranges.get(i);
+ if (currRange.intersects(range)) {
+ ranges.set(i, new TextRange(Math.min(currRange.getStartOffset(), range.getStartOffset()), Math.max(currRange.getEndOffset(), range.getEndOffset())));
+ entries.get(i).add(entry);
+ processed = true;
+ break;
+ }
+ }
+ if (processed) continue;
+
+ ranges.add(range);
+ entries.add(ContainerUtil.newArrayList(entry));
+ }
+
+ // for each set of intersecting ranges, compute the best errors to show
+ for (List<Map.Entry<PsiElement, JsonValidationError>> entryList : entries) {
+ int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
+ for (Map.Entry<PsiElement, JsonValidationError> entry : entryList) {
+ JsonValidationError validationError = entry.getValue();
+ PsiElement psiElement = entry.getKey();
+ if (validationError.getPriority().ordinal() > min) {
+ continue;
+ }
+ TextRange range = myWalker.adjustErrorHighlightingRange(psiElement);
+ range = range.shiftLeft(psiElement.getTextRange().getStartOffset());
+ registerError(psiElement, range, validationError);
+ }
+ }
+ }
+
+ private void registerError(@NotNull PsiElement psiElement, @NotNull TextRange range, @NotNull JsonValidationError validationError) {
+ if (checkIfAlreadyProcessed(psiElement)) return;
+ String value = validationError.getMessage();
+ if (myMessagePrefix != null) value = myMessagePrefix + value;
+ LocalQuickFix[] fix = validationError.createFixes(myWalker.getQuickFixAdapter(myHolder.getProject()));
+ if (fix.length == 0) {
+ myHolder.registerProblem(psiElement, range, value);
+ }
+ else {
+ myHolder.registerProblem(psiElement, range, value, fix);
+ }
+ }
+
+ private static JsonValueAdapter findTopLevelElement(@NotNull JsonLikePsiWalker walker, @NotNull PsiElement element) {
+ final Ref<PsiElement> ref = new Ref<>();
+ PsiTreeUtil.findFirstParent(element, el -> {
+ final boolean isTop = walker.isTopJsonElement(el);
+ if (!isTop) ref.set(el);
+ return isTop;
+ });
+ return ref.isNull() ? null : walker.createValueAdapter(ref.get());
+ }
+
+ private boolean checkIfAlreadyProcessed(@NotNull PsiElement property) {
+ Set<PsiElement> data = mySession.getUserData(ANNOTATED_PROPERTIES);
+ if (data == null) {
+ data = new HashSet<>();
+ mySession.putUserData(ANNOTATED_PROPERTIES, data);
+ }
+ if (data.contains(property)) return true;
+ data.add(property);
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java
new file mode 100644
index 00000000..04c09278
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.EditorNotificationPanel;
+import com.intellij.ui.EditorNotifications;
+import com.intellij.ui.LightColors;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.SchemaType;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/19/2016.
+ */
+public class JsonSchemaConflictNotificationProvider extends EditorNotifications.Provider<EditorNotificationPanel> {
+ private static final Key<EditorNotificationPanel> KEY = Key.create("json.schema.conflict.notification.panel");
+
+ @NotNull
+ private final Project myProject;
+ @NotNull
+ private final JsonSchemaService myJsonSchemaService;
+
+ public JsonSchemaConflictNotificationProvider(@NotNull Project project,
+ @NotNull JsonSchemaService jsonSchemaService) {
+ myProject = project;
+ myJsonSchemaService = jsonSchemaService;
+ }
+
+ @NotNull
+ @Override
+ public Key<EditorNotificationPanel> getKey() {
+ return KEY;
+ }
+
+ @Nullable
+ @Override
+ public EditorNotificationPanel createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) {
+ if (!myJsonSchemaService.isApplicableToFile(file)) return null;
+ final Collection<VirtualFile> schemaFiles = ContainerUtil.newArrayList();
+ if (!hasConflicts(schemaFiles, file)) return null;
+
+ final String message = createMessage(schemaFiles, myJsonSchemaService,
+ "; ", "<html>There are several JSON Schemas mapped to this file: ", "</html>");
+ if (message == null) return null;
+
+ final EditorNotificationPanel panel = new EditorNotificationPanel(LightColors.RED);
+ panel.setText(message);
+ panel.createActionLabel("Edit JSON Schema Mappings", () -> {
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, new JsonSchemaMappingsConfigurable(myProject));
+ EditorNotifications.getInstance(myProject).updateNotifications(file);
+ });
+ return panel;
+ }
+
+ private boolean hasConflicts(@NotNull Collection<VirtualFile> files, @NotNull VirtualFile file) {
+ List<JsonSchemaFileProvider> providers = ((JsonSchemaServiceImpl)myJsonSchemaService).getProvidersForFile(file);
+ for (JsonSchemaFileProvider provider : providers) {
+ if (provider.getSchemaType() != SchemaType.userSchema) continue;
+ VirtualFile schemaFile = provider.getSchemaFile();
+ if (schemaFile != null) {
+ files.add(schemaFile);
+ }
+ }
+ return files.size() > 1;
+ }
+
+ public static String createMessage(@NotNull final Collection<? extends VirtualFile> schemaFiles,
+ @NotNull JsonSchemaService jsonSchemaService,
+ @NotNull String separator,
+ @NotNull String prefix,
+ @NotNull String suffix) {
+ final List<Pair<Boolean, String>> pairList = schemaFiles.stream()
+ .map(file -> jsonSchemaService.getSchemaProvider(file))
+ .filter(Objects::nonNull)
+ .map(provider -> Pair.create(SchemaType.userSchema.equals(provider.getSchemaType()), provider.getName()))
+ .collect(Collectors.toList());
+
+ final long numOfSystemSchemas = pairList.stream().filter(pair -> !pair.getFirst()).count();
+ // do not report anything if there is only one system schema and one user schema (user overrides schema that we provide)
+ if (pairList.size() == 2 && numOfSystemSchemas == 1) return null;
+
+ final boolean withTypes = numOfSystemSchemas > 0;
+ return pairList.stream().map(pair -> {
+ if (withTypes) {
+ return String.format("%s schema '%s'", Boolean.TRUE.equals(pair.getFirst()) ? "user" : "system", pair.getSecond());
+ }
+ else {
+ return pair.getSecond();
+ }
+ }).collect(Collectors.joining(separator, prefix, suffix));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java
new file mode 100644
index 00000000..4bcb0732
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java
@@ -0,0 +1,218 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.documentation.DocumentationMarkup;
+import com.intellij.lang.documentation.DocumentationProvider;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.FakePsiElement;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+
+public class JsonSchemaDocumentationProvider implements DocumentationProvider {
+ @Nullable
+ @Override
+ public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
+ return findSchemaAndGenerateDoc(element, originalElement, true, null);
+ }
+
+ @Nullable
+ @Override
+ public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
+ String forcedPropName = null;
+ if (element instanceof FakeDocElement) {
+ forcedPropName = ((FakeDocElement)element).myAltName;
+ element = ((FakeDocElement)element).myContextElement;
+ }
+ return findSchemaAndGenerateDoc(element, originalElement, false, forcedPropName);
+ }
+
+ @Nullable
+ public static String findSchemaAndGenerateDoc(PsiElement element,
+ @Nullable PsiElement originalElement,
+ final boolean preferShort,
+ @Nullable String forcedPropName) {
+ if (element instanceof FakeDocElement) return null;
+ element = isWhitespaceOrComment(originalElement) ? element : ObjectUtils.coalesce(originalElement, element);
+ final PsiFile containingFile = element.getContainingFile();
+ if (containingFile == null) return null;
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ VirtualFile virtualFile = containingFile.getViewProvider().getVirtualFile();
+ if (!service.isApplicableToFile(virtualFile)) return null;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile);
+ if (rootSchema == null) return null;
+
+ return generateDoc(element, rootSchema, preferShort, forcedPropName);
+ }
+
+ private static boolean isWhitespaceOrComment(@Nullable PsiElement originalElement) {
+ return originalElement instanceof PsiWhiteSpace || originalElement instanceof PsiComment;
+ }
+
+ @Nullable
+ public static String generateDoc(@NotNull final PsiElement element,
+ @NotNull final JsonSchemaObject rootSchema,
+ final boolean preferShort,
+ @Nullable String forcedPropName) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema);
+ if (walker == null) return null;
+
+ final PsiElement checkable = walker.goUpToCheckable(element);
+ if (checkable == null) return null;
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, true);
+ if (position == null) return null;
+ if (forcedPropName != null) {
+ if (isWhitespaceOrComment(element)) {
+ position.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName));
+ }
+ else {
+ if (position.isEmpty()) {
+ return null;
+ }
+ final JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1);
+ if (lastStep.getName() == null) return null;
+ position.set(position.size() - 1, JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName));
+ }
+ }
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, true, position).resolve();
+
+ String htmlDescription = null;
+ List<JsonSchemaType> possibleTypes = ContainerUtil.newArrayList();
+ for (JsonSchemaObject schema : schemas) {
+ if (htmlDescription == null) {
+ htmlDescription = getBestDocumentation(preferShort, schema);
+ }
+ if (schema.getType() != null && schema.getType() != JsonSchemaType._any) {
+ possibleTypes.add(schema.getType());
+ }
+ else if (schema.getTypeVariants() != null) {
+ possibleTypes.addAll(schema.getTypeVariants());
+ }
+ }
+
+ return htmlDescription == null
+ ? null
+ : appendNameTypeAndApi(position, getThirdPartyApiInfo(element, rootSchema), possibleTypes, htmlDescription, preferShort);
+ }
+
+ @NotNull
+ private static String appendNameTypeAndApi(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> position,
+ @NotNull String apiInfo,
+ @NotNull List<JsonSchemaType> possibleTypes,
+ @NotNull String htmlDescription, boolean preferShort) {
+ if (position.size() == 0) return htmlDescription;
+
+ JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1);
+ String name = lastStep.getName();
+ if (name == null) return htmlDescription;
+
+ String type = "";
+ String schemaType = JsonSchemaObject.getTypesDescription(false, possibleTypes);
+ if (schemaType != null) {
+ type = ": " + schemaType;
+ }
+
+ if (preferShort) {
+ htmlDescription = "<b>" + name + "</b>" + type + apiInfo + "<br/>" + htmlDescription;
+ }
+ else {
+ htmlDescription = DocumentationMarkup.DEFINITION_START + name + type + apiInfo + DocumentationMarkup.DEFINITION_END +
+ DocumentationMarkup.CONTENT_START + htmlDescription + DocumentationMarkup.CONTENT_END;
+ }
+ return htmlDescription;
+ }
+
+ @NotNull
+ private static String getThirdPartyApiInfo(@NotNull PsiElement element,
+ @NotNull JsonSchemaObject rootSchema) {
+ JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ String apiInfo = "";
+ JsonSchemaFileProvider provider = service.getSchemaProvider(rootSchema.getSchemaFile());
+ if (provider != null) {
+ String information = provider.getThirdPartyApiInformation();
+ if (information != null) {
+ apiInfo = "&nbsp;&nbsp;<i>(" + information + ")</i>";
+ }
+ }
+ return apiInfo;
+ }
+
+ @Nullable
+ public static String getBestDocumentation(boolean preferShort, @NotNull final JsonSchemaObject schema) {
+ final String htmlDescription = schema.getHtmlDescription();
+ final String description = schema.getDescription();
+ final String title = schema.getTitle();
+ if (preferShort && !StringUtil.isEmptyOrSpaces(title)) {
+ return plainTextPostProcess(title);
+ } else if (!StringUtil.isEmptyOrSpaces(htmlDescription)) {
+ String desc = htmlDescription;
+ if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc;
+ return desc;
+ } else if (!StringUtil.isEmptyOrSpaces(description)) {
+ String desc = plainTextPostProcess(description);
+ if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc;
+ return desc;
+ }
+ return null;
+ }
+
+ @NotNull
+ private static String plainTextPostProcess(String text) {
+ return StringUtil.escapeXml(text).replace("\\n", "<br/>");
+ }
+
+ @Nullable
+ @Override
+ public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) {
+ if ((element instanceof JsonProperty || isWhitespaceOrComment(element) && element.getParent() instanceof JsonObject) && object instanceof String) {
+ return new FakeDocElement(element instanceof JsonProperty ? ((JsonProperty)element).getNameElement() : element, StringUtil.unquoteString((String)object));
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) {
+ return null;
+ }
+
+ private static class FakeDocElement extends FakePsiElement {
+ private final PsiElement myContextElement;
+ private final String myAltName;
+
+ private FakeDocElement(PsiElement context, String name) {
+ myContextElement = context;
+ myAltName = name;
+ }
+
+ @Override
+ public PsiElement getParent() {
+ return myContextElement;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getTextRangeInParent() {
+ return myContextElement.getTextRange().shiftLeft(myContextElement.getTextOffset());
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java
new file mode 100644
index 00000000..e4827358
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java
@@ -0,0 +1,171 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.JsonLexer;
+import com.intellij.json.json5.Json5FileType;
+import com.intellij.json.json5.Json5Lexer;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.indexing.*;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.EnumeratorStringDescriptor;
+import com.intellij.util.io.KeyDescriptor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JsonSchemaFileValuesIndex extends FileBasedIndexExtension<String, String> {
+ public static final ID<String, String> INDEX_ID = ID.create("json.file.root.values");
+ private static final int VERSION = 5;
+ public static final String NULL = "$NULL$";
+
+ @NotNull
+ @Override
+ public ID<String, String> getName() {
+ return INDEX_ID;
+ }
+
+ private final DataIndexer<String, String, FileContent> myIndexer =
+ new DataIndexer<String, String, FileContent>() {
+ @Override
+ @NotNull
+ public Map<String, String> map(@NotNull FileContent inputData) {
+ return readTopLevelProps(inputData.getFileType(), inputData.getContentAsText());
+ }
+ };
+
+ @NotNull
+ @Override
+ public DataIndexer<String, String, FileContent> getIndexer() {
+ return myIndexer;
+ }
+
+ @NotNull
+ @Override
+ public KeyDescriptor<String> getKeyDescriptor() {
+ return EnumeratorStringDescriptor.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public DataExternalizer<String> getValueExternalizer() {
+ return EnumeratorStringDescriptor.INSTANCE;
+ }
+
+ @Override
+ public int getVersion() {
+ return VERSION;
+ }
+
+ @NotNull
+ @Override
+ public FileBasedIndex.InputFilter getInputFilter() {
+ return file -> file.getFileType() instanceof JsonFileType;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Nullable
+ public static String getCachedValue(Project project, VirtualFile file, String requestedKey) {
+ if (project.isDisposed() || !file.isValid() || DumbService.isDumb(project)) return NULL;
+ List<String> values = FileBasedIndex.getInstance().getValues(INDEX_ID, requestedKey, GlobalSearchScope.fileScope(project, file));
+ if (values.size() == 1) {
+ return values.get(0);
+ }
+
+ return null;
+ }
+
+ @NotNull
+ static Map<String, String> readTopLevelProps(@NotNull FileType fileType, @NotNull CharSequence content) {
+ if (!(fileType instanceof JsonFileType)) return ContainerUtil.newHashMap();
+
+ Lexer lexer = fileType == Json5FileType.INSTANCE ? new Json5Lexer() : new JsonLexer();
+ final HashMap<String, String> map = ContainerUtil.newHashMap();
+ lexer.start(content);
+
+ // We only care about properties at the root level having the form of "property" : "value".
+ int nesting = 0;
+ boolean idFound = false;
+ boolean obsoleteIdFound = false;
+ boolean schemaFound = false;
+ while (!(idFound && schemaFound && obsoleteIdFound) && lexer.getCurrentPosition().getOffset() < lexer.getBufferEnd()) {
+ IElementType token = lexer.getTokenType();
+ // Nesting level can only change at curly braces.
+ if (token == JsonElementTypes.L_CURLY) {
+ nesting++;
+ }
+ else if (token == JsonElementTypes.R_CURLY) {
+ nesting--;
+ }
+ else if (nesting == 1 &&
+ (token == JsonElementTypes.DOUBLE_QUOTED_STRING
+ || token == JsonElementTypes.SINGLE_QUOTED_STRING
+ || token == JsonElementTypes.IDENTIFIER)) {
+ // We are looking for two special properties at the root level.
+ switch (lexer.getTokenText()) {
+ case "$id":
+ case "\"$id\"":
+ case "'$id'":
+ idFound |= captureValueIfString(lexer, map, JsonCachedValues.ID_CACHE_KEY);
+ break;
+ case "id":
+ case "\"id\"":
+ case "'id'":
+ obsoleteIdFound |= captureValueIfString(lexer, map, JsonCachedValues.OBSOLETE_ID_CACHE_KEY);
+ break;
+ case "$schema":
+ case "\"$schema\"":
+ case "'$schema'":
+ schemaFound |= captureValueIfString(lexer, map, JsonCachedValues.URL_CACHE_KEY);
+ break;
+ }
+ }
+ lexer.advance();
+ }
+ if (!map.containsKey(JsonCachedValues.ID_CACHE_KEY)) map.put(JsonCachedValues.ID_CACHE_KEY, NULL);
+ if (!map.containsKey(JsonCachedValues.OBSOLETE_ID_CACHE_KEY)) map.put(JsonCachedValues.OBSOLETE_ID_CACHE_KEY, NULL);
+ if (!map.containsKey(JsonCachedValues.URL_CACHE_KEY)) map.put(JsonCachedValues.URL_CACHE_KEY, NULL);
+ return map;
+ }
+
+ private static boolean captureValueIfString(@NotNull Lexer lexer, @NotNull HashMap<String, String> destMap, @NotNull String key) {
+ IElementType token;
+ lexer.advance();
+ token = skipWhitespacesAndGetTokenType(lexer);
+ if (token == JsonElementTypes.COLON) {
+ lexer.advance();
+ token = skipWhitespacesAndGetTokenType(lexer);
+ if (token == JsonElementTypes.DOUBLE_QUOTED_STRING || token == JsonElementTypes.SINGLE_QUOTED_STRING) {
+ destMap.put(key, lexer.getTokenText().substring(1, lexer.getTokenText().length() - 1));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static IElementType skipWhitespacesAndGetTokenType(@NotNull Lexer lexer) {
+ while (lexer.getTokenType() == TokenType.WHITE_SPACE ||
+ lexer.getTokenType() == JsonElementTypes.LINE_COMMENT ||
+ lexer.getTokenType() == JsonElementTypes.BLOCK_COMMENT) {
+ lexer.advance();
+ }
+ return lexer.getTokenType();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java
new file mode 100644
index 00000000..072bd5f5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java
@@ -0,0 +1,48 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtilCore;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonSchemaGotoDeclarationHandler implements GotoDeclarationHandler {
+ @Nullable
+ @Override
+ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) {
+ final IElementType elementType = PsiUtilCore.getElementType(sourceElement);
+ if (elementType != JsonElementTypes.DOUBLE_QUOTED_STRING && elementType != JsonElementTypes.SINGLE_QUOTED_STRING) return null;
+ final JsonStringLiteral literal = PsiTreeUtil.getParentOfType(sourceElement, JsonStringLiteral.class);
+ if (literal == null) return null;
+ final PsiElement parent = literal.getParent();
+ if (parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(literal.getProject());
+ final PsiFile containingFile = literal.getContainingFile();
+ final VirtualFile file = containingFile.getVirtualFile();
+ if (file == null || !service.isApplicableToFile(file)) return null;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonOriginalPsiWalker.INSTANCE.findPosition(literal, true);
+ if (steps == null) return null;
+ final JsonSchemaObject schemaObject = service.getSchemaObject(file);
+ if (schemaObject != null) {
+ final PsiElement target = new JsonSchemaResolver(schemaObject, false, steps)
+ .findNavigationTarget(false, ((JsonProperty)parent).getValue(),
+ JsonSchemaService.isSchemaFile(containingFile));
+ if (target != null) {
+ return new PsiElement[] {target};
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java
new file mode 100644
index 00000000..998542b6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.jsonSchema.extension.JsonSchemaEnabler;
+
+public class JsonSchemaInJsonFilesEnabler implements JsonSchemaEnabler {
+ @Override
+ public boolean isEnabledForFile(VirtualFile file) {
+ return JsonUtil.isJsonFile(file);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java
new file mode 100644
index 00000000..8976179e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java
@@ -0,0 +1,1180 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.intellij.json.psi.JsonContainer;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.ContainerUtilRt;
+import com.jetbrains.jsonSchema.JsonSchemaVfsListener;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+
+/**
+ * @author Irina.Chernushina on 8/28/2015.
+ */
+public class JsonSchemaObject {
+ private static final Logger LOG = Logger.getInstance(JsonSchemaObject.class);
+
+ @NonNls public static final String DEFINITIONS = "definitions";
+ @NonNls public static final String PROPERTIES = "properties";
+ @NonNls public static final String ITEMS = "items";
+ @NonNls public static final String ADDITIONAL_ITEMS = "additionalItems";
+ @NonNls public static final String X_INTELLIJ_HTML_DESCRIPTION = "x-intellij-html-description";
+ @Nullable private final JsonContainer myJsonObject;
+ @Nullable private Map<String, JsonSchemaObject> myDefinitionsMap;
+ @NotNull private static final JsonSchemaObject NULL_OBJ = new JsonSchemaObject();
+ @NotNull private final ConcurrentMap<String, JsonSchemaObject> myComputedRefs = new ConcurrentHashMap<>();
+ @NotNull private final AtomicBoolean mySubscribed = new AtomicBoolean(false);
+ @NotNull private Map<String, JsonSchemaObject> myProperties;
+
+ @Nullable private PatternProperties myPatternProperties;
+ @Nullable private PropertyNamePattern myPattern;
+
+ @Nullable private String myId;
+ @Nullable private String mySchema;
+
+ @Nullable private String myTitle;
+ @Nullable private String myDescription;
+ @Nullable private String myHtmlDescription;
+
+ @Nullable private JsonSchemaType myType;
+ @Nullable private Object myDefault;
+ @Nullable private String myRef;
+ @Nullable private String myFormat;
+ @Nullable private Set<JsonSchemaType> myTypeVariants;
+ @Nullable private Number myMultipleOf;
+ @Nullable private Number myMaximum;
+ private boolean myExclusiveMaximum;
+ @Nullable private Number myExclusiveMaximumNumber;
+ @Nullable private Number myMinimum;
+ private boolean myExclusiveMinimum;
+ @Nullable private Number myExclusiveMinimumNumber;
+ @Nullable private Integer myMaxLength;
+ @Nullable private Integer myMinLength;
+
+ @Nullable private Boolean myAdditionalPropertiesAllowed;
+ @Nullable private JsonSchemaObject myAdditionalPropertiesSchema;
+ @Nullable private JsonSchemaObject myPropertyNamesSchema;
+
+ @Nullable private Boolean myAdditionalItemsAllowed;
+ @Nullable private JsonSchemaObject myAdditionalItemsSchema;
+
+ @Nullable private JsonSchemaObject myItemsSchema;
+ @Nullable private JsonSchemaObject myContainsSchema;
+ @Nullable private List<JsonSchemaObject> myItemsSchemaList;
+
+ @Nullable private Integer myMaxItems;
+ @Nullable private Integer myMinItems;
+
+ @Nullable private Boolean myUniqueItems;
+
+ @Nullable private Integer myMaxProperties;
+ @Nullable private Integer myMinProperties;
+ @Nullable private Set<String> myRequired;
+
+ @Nullable private Map<String, List<String>> myPropertyDependencies;
+ @Nullable private Map<String, JsonSchemaObject> mySchemaDependencies;
+
+ @Nullable private List<Object> myEnum;
+
+ @Nullable private List<JsonSchemaObject> myAllOf;
+ @Nullable private List<JsonSchemaObject> myAnyOf;
+ @Nullable private List<JsonSchemaObject> myOneOf;
+ @Nullable private JsonSchemaObject myNot;
+ @Nullable private JsonSchemaObject myIf;
+ @Nullable private JsonSchemaObject myThen;
+ @Nullable private JsonSchemaObject myElse;
+ private boolean myShouldValidateAgainstJSType;
+
+ public boolean isValidByExclusion() {
+ return myIsValidByExclusion;
+ }
+
+ private boolean myIsValidByExclusion = true;
+
+ public JsonSchemaObject(@NotNull JsonContainer object) {
+ myJsonObject = object;
+ myProperties = new HashMap<>();
+ }
+
+ private JsonSchemaObject() {
+ myJsonObject = null;
+ myProperties = new HashMap<>();
+ }
+
+ @Nullable
+ private static JsonSchemaType getSubtypeOfBoth(@NotNull JsonSchemaType selfType,
+ @NotNull JsonSchemaType otherType) {
+ if (otherType == JsonSchemaType._any) return selfType;
+ if (selfType == JsonSchemaType._any) return otherType;
+ switch (selfType) {
+ case _string:
+ return otherType == JsonSchemaType._string || otherType == JsonSchemaType._string_number ? JsonSchemaType._string : null;
+ case _number:
+ if (otherType == JsonSchemaType._integer) return JsonSchemaType._integer;
+ return otherType == JsonSchemaType._number || otherType == JsonSchemaType._string_number ? JsonSchemaType._number : null;
+ case _integer:
+ return otherType == JsonSchemaType._number
+ || otherType == JsonSchemaType._string_number
+ || otherType == JsonSchemaType._integer ? JsonSchemaType._integer : null;
+ case _object:
+ return otherType == JsonSchemaType._object ? JsonSchemaType._object : null;
+ case _array:
+ return otherType == JsonSchemaType._array ? JsonSchemaType._array : null;
+ case _boolean:
+ return otherType == JsonSchemaType._boolean ? JsonSchemaType._boolean : null;
+ case _null:
+ return otherType == JsonSchemaType._null ? JsonSchemaType._null : null;
+ case _string_number:
+ return otherType == JsonSchemaType._integer
+ || otherType == JsonSchemaType._number
+ || otherType == JsonSchemaType._string
+ || otherType == JsonSchemaType._string_number ? otherType : null;
+ }
+ return otherType;
+ }
+
+ @Nullable
+ private JsonSchemaType mergeTypes(@Nullable JsonSchemaType selfType,
+ @Nullable JsonSchemaType otherType,
+ @Nullable Set<JsonSchemaType> otherTypeVariants) {
+ if (selfType == null) return otherType;
+ if (otherType == null) {
+ if (otherTypeVariants != null && !otherTypeVariants.isEmpty()) {
+ Set<JsonSchemaType> filteredVariants = ContainerUtil.newHashSet(otherTypeVariants.size());
+ for (JsonSchemaType variant : otherTypeVariants) {
+ JsonSchemaType subtype = getSubtypeOfBoth(selfType, variant);
+ if (subtype != null) filteredVariants.add(subtype);
+ }
+ if (filteredVariants.size() == 0) {
+ myIsValidByExclusion = false;
+ return selfType;
+ }
+ if (filteredVariants.size() == 1) {
+ return filteredVariants.iterator().next();
+ }
+ return null; // will be handled by variants
+ }
+ return selfType;
+ }
+
+ JsonSchemaType subtypeOfBoth = getSubtypeOfBoth(selfType, otherType);
+ if (subtypeOfBoth == null){
+ myIsValidByExclusion = false;
+ return otherType;
+ }
+ return subtypeOfBoth;
+ }
+
+ private Set<JsonSchemaType> mergeTypeVariantSets(@Nullable Set<JsonSchemaType> self, @Nullable Set<JsonSchemaType> other) {
+ if (self == null) return other;
+ if (other == null) return self;
+
+ Set<JsonSchemaType> resultSet = ContainerUtil.newHashSet(self.size());
+ for (JsonSchemaType type : self) {
+ JsonSchemaType merged = mergeTypes(type, null, other);
+ if (merged != null) resultSet.add(merged);
+ }
+
+ if (resultSet.isEmpty()) {
+ myIsValidByExclusion = false;
+ return other;
+ }
+
+ return resultSet;
+ }
+
+ // peer pointer is not merged!
+ public void mergeValues(@NotNull JsonSchemaObject other) {
+ // we do not copy id, schema
+ mergeProperties(this, other);
+ myDefinitionsMap = copyMap(myDefinitionsMap, other.myDefinitionsMap);
+ final Map<String, JsonSchemaObject> map = copyMap(myPatternProperties == null ? null : myPatternProperties.mySchemasMap,
+ other.myPatternProperties == null ? null : other.myPatternProperties.mySchemasMap);
+ myPatternProperties = map == null ? null : new PatternProperties(map);
+
+ if (!StringUtil.isEmptyOrSpaces(other.myTitle)) {
+ myTitle = other.myTitle;
+ }
+ if (!StringUtil.isEmptyOrSpaces(other.myDescription)) {
+ myDescription = other.myDescription;
+ }
+ if (!StringUtil.isEmptyOrSpaces(other.myHtmlDescription)) {
+ myHtmlDescription = other.myHtmlDescription;
+ }
+
+ myType = mergeTypes(myType, other.myType, other.myTypeVariants);
+
+ if (other.myDefault != null) myDefault = other.myDefault;
+ if (other.myRef != null) myRef = other.myRef;
+ if (other.myFormat != null) myFormat = other.myFormat;
+ myTypeVariants = mergeTypeVariantSets(myTypeVariants, other.myTypeVariants);
+ if (other.myMultipleOf != null) myMultipleOf = other.myMultipleOf;
+ if (other.myMaximum != null) myMaximum = other.myMaximum;
+ if (other.myExclusiveMaximumNumber != null) myExclusiveMaximumNumber = other.myExclusiveMaximumNumber;
+ myExclusiveMaximum |= other.myExclusiveMaximum;
+ if (other.myMinimum != null) myMinimum = other.myMinimum;
+ if (other.myExclusiveMinimumNumber != null) myExclusiveMinimumNumber = other.myExclusiveMinimumNumber;
+ myExclusiveMinimum |= other.myExclusiveMinimum;
+ if (other.myMaxLength != null) myMaxLength = other.myMaxLength;
+ if (other.myMinLength != null) myMinLength = other.myMinLength;
+ if (other.myPattern != null) myPattern = other.myPattern;
+ if (other.myAdditionalPropertiesAllowed != null) myAdditionalPropertiesAllowed = other.myAdditionalPropertiesAllowed;
+ if (other.myAdditionalPropertiesSchema != null) myAdditionalPropertiesSchema = other.myAdditionalPropertiesSchema;
+ if (other.myPropertyNamesSchema != null) myPropertyNamesSchema = other.myPropertyNamesSchema;
+ if (other.myAdditionalItemsAllowed != null) myAdditionalItemsAllowed = other.myAdditionalItemsAllowed;
+ if (other.myAdditionalItemsSchema != null) myAdditionalItemsSchema = other.myAdditionalItemsSchema;
+ if (other.myItemsSchema != null) myItemsSchema = other.myItemsSchema;
+ if (other.myContainsSchema != null) myContainsSchema = other.myContainsSchema;
+ myItemsSchemaList = copyList(myItemsSchemaList, other.myItemsSchemaList);
+ if (other.myMaxItems != null) myMaxItems = other.myMaxItems;
+ if (other.myMinItems != null) myMinItems = other.myMinItems;
+ if (other.myUniqueItems != null) myUniqueItems = other.myUniqueItems;
+ if (other.myMaxProperties != null) myMaxProperties = other.myMaxProperties;
+ if (other.myMinProperties != null) myMinProperties = other.myMinProperties;
+ if (myRequired != null && other.myRequired != null) {
+ myRequired.addAll(other.myRequired);
+ }
+ else if (other.myRequired != null) {
+ myRequired = other.myRequired;
+ }
+ myPropertyDependencies = copyMap(myPropertyDependencies, other.myPropertyDependencies);
+ mySchemaDependencies = copyMap(mySchemaDependencies, other.mySchemaDependencies);
+ if (other.myEnum != null) myEnum = other.myEnum;
+ myAllOf = copyList(myAllOf, other.myAllOf);
+ myAnyOf = copyList(myAnyOf, other.myAnyOf);
+ myOneOf = copyList(myOneOf, other.myOneOf);
+ if (other.myNot != null) myNot = other.myNot;
+ if (other.myIf != null) myIf = other.myIf;
+ if (other.myThen != null) myThen = other.myThen;
+ if (other.myElse != null) myElse = other.myElse;
+ myShouldValidateAgainstJSType |= other.myShouldValidateAgainstJSType;
+ }
+
+ private static void mergeProperties(@NotNull JsonSchemaObject thisObject, @NotNull JsonSchemaObject otherObject) {
+ for (Map.Entry<String, JsonSchemaObject> prop: otherObject.myProperties.entrySet()) {
+ String key = prop.getKey();
+ JsonSchemaObject otherProp = prop.getValue();
+ if (!thisObject.myProperties.containsKey(key)) {
+ thisObject.myProperties.put(key, otherProp);
+ }
+ else {
+ JsonSchemaObject existingProp = thisObject.myProperties.get(key);
+ thisObject.myProperties.put(key, JsonSchemaVariantsTreeBuilder.merge(existingProp, otherProp, otherProp));
+ }
+ }
+ }
+
+ public void shouldValidateAgainstJSType() {
+ myShouldValidateAgainstJSType = true;
+ }
+
+ public boolean isShouldValidateAgainstJSType() {
+ return myShouldValidateAgainstJSType;
+ }
+
+ @Nullable
+ private static <T> List<T> copyList(@Nullable List<T> target, @Nullable List<T> source) {
+ if (source == null || source.isEmpty()) return target;
+ if (target == null) target = ContainerUtil.newArrayListWithCapacity(source.size());
+ target.addAll(source);
+ return target;
+ }
+
+ @Nullable
+ private static <K, V> Map<K, V> copyMap(@Nullable Map<K, V> target, @Nullable Map<K, V> source) {
+ if (source == null || source.isEmpty()) return target;
+ if (target == null) target = ContainerUtilRt.newHashMap(source.size());
+ target.putAll(source);
+ return target;
+ }
+
+ @NotNull
+ public VirtualFile getSchemaFile() {
+ assert myJsonObject != null;
+ return myJsonObject.getContainingFile().getViewProvider().getVirtualFile();
+ }
+
+ @NotNull
+ public JsonContainer getJsonObject() {
+ assert myJsonObject != null;
+ return myJsonObject;
+ }
+
+ @Nullable
+ public Map<String, JsonSchemaObject> getDefinitionsMap() {
+ return myDefinitionsMap;
+ }
+
+ public void setDefinitionsMap(@NotNull Map<String, JsonSchemaObject> definitionsMap) {
+ myDefinitionsMap = definitionsMap;
+ }
+
+ @NotNull
+ public Map<String, JsonSchemaObject> getProperties() {
+ return myProperties;
+ }
+
+ public void setProperties(@NotNull Map<String, JsonSchemaObject> properties) {
+ myProperties = properties;
+ }
+
+ public boolean hasPatternProperties() {
+ return myPatternProperties != null;
+ }
+
+ public void setPatternProperties(@NotNull Map<String, JsonSchemaObject> patternProperties) {
+ myPatternProperties = new PatternProperties(patternProperties);
+ }
+
+ @Nullable
+ public JsonSchemaType getType() {
+ return myType;
+ }
+
+ public void setType(@Nullable JsonSchemaType type) {
+ myType = type;
+ }
+
+ @Nullable
+ public Number getMultipleOf() {
+ return myMultipleOf;
+ }
+
+ public void setMultipleOf(@Nullable Number multipleOf) {
+ myMultipleOf = multipleOf;
+ }
+
+ @Nullable
+ public Number getMaximum() {
+ return myMaximum;
+ }
+
+ public void setMaximum(@Nullable Number maximum) {
+ myMaximum = maximum;
+ }
+
+ public boolean isExclusiveMaximum() {
+ return myExclusiveMaximum;
+ }
+
+ @Nullable
+ public Number getExclusiveMaximumNumber() {
+ return myExclusiveMaximumNumber;
+ }
+
+ public void setExclusiveMaximumNumber(@Nullable Number exclusiveMaximumNumber) {
+ myExclusiveMaximumNumber = exclusiveMaximumNumber;
+ }
+
+ @Nullable
+ public Number getExclusiveMinimumNumber() {
+ return myExclusiveMinimumNumber;
+ }
+
+ public void setExclusiveMinimumNumber(@Nullable Number exclusiveMinimumNumber) {
+ myExclusiveMinimumNumber = exclusiveMinimumNumber;
+ }
+
+ public void setExclusiveMaximum(boolean exclusiveMaximum) {
+ myExclusiveMaximum = exclusiveMaximum;
+ }
+
+ @Nullable
+ public Number getMinimum() {
+ return myMinimum;
+ }
+
+ public void setMinimum(@Nullable Number minimum) {
+ myMinimum = minimum;
+ }
+
+ public boolean isExclusiveMinimum() {
+ return myExclusiveMinimum;
+ }
+
+ public void setExclusiveMinimum(boolean exclusiveMinimum) {
+ myExclusiveMinimum = exclusiveMinimum;
+ }
+
+ @Nullable
+ public Integer getMaxLength() {
+ return myMaxLength;
+ }
+
+ public void setMaxLength(@Nullable Integer maxLength) {
+ myMaxLength = maxLength;
+ }
+
+ @Nullable
+ public Integer getMinLength() {
+ return myMinLength;
+ }
+
+ public void setMinLength(@Nullable Integer minLength) {
+ myMinLength = minLength;
+ }
+
+ @Nullable
+ public String getPattern() {
+ return myPattern == null ? null : myPattern.getPattern();
+ }
+
+ public void setPattern(@Nullable String pattern) {
+ myPattern = pattern == null ? null : new PropertyNamePattern(pattern);
+ }
+
+ @Nullable
+ public Boolean getAdditionalPropertiesAllowed() {
+ return myAdditionalPropertiesAllowed == null || myAdditionalPropertiesAllowed;
+ }
+
+ public void setAdditionalPropertiesAllowed(@Nullable Boolean additionalPropertiesAllowed) {
+ myAdditionalPropertiesAllowed = additionalPropertiesAllowed;
+ }
+
+ @Nullable
+ public JsonSchemaObject getPropertyNamesSchema() {
+ return myPropertyNamesSchema;
+ }
+
+ public void setPropertyNamesSchema(@Nullable JsonSchemaObject propertyNamesSchema) {
+ myPropertyNamesSchema = propertyNamesSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getAdditionalPropertiesSchema() {
+ return myAdditionalPropertiesSchema;
+ }
+
+ public void setAdditionalPropertiesSchema(@Nullable JsonSchemaObject additionalPropertiesSchema) {
+ myAdditionalPropertiesSchema = additionalPropertiesSchema;
+ }
+
+ @Nullable
+ public Boolean getAdditionalItemsAllowed() {
+ return myAdditionalItemsAllowed == null || myAdditionalItemsAllowed;
+ }
+
+ public void setAdditionalItemsAllowed(@Nullable Boolean additionalItemsAllowed) {
+ myAdditionalItemsAllowed = additionalItemsAllowed;
+ }
+
+ @Nullable
+ public JsonSchemaObject getAdditionalItemsSchema() {
+ return myAdditionalItemsSchema;
+ }
+
+ public void setAdditionalItemsSchema(@Nullable JsonSchemaObject additionalItemsSchema) {
+ myAdditionalItemsSchema = additionalItemsSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getItemsSchema() {
+ return myItemsSchema;
+ }
+
+ public void setItemsSchema(@Nullable JsonSchemaObject itemsSchema) {
+ myItemsSchema = itemsSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getContainsSchema() {
+ return myContainsSchema;
+ }
+
+ public void setContainsSchema(@Nullable JsonSchemaObject containsSchema) {
+ myContainsSchema = containsSchema;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getItemsSchemaList() {
+ return myItemsSchemaList;
+ }
+
+ public void setItemsSchemaList(@Nullable List<JsonSchemaObject> itemsSchemaList) {
+ myItemsSchemaList = itemsSchemaList;
+ }
+
+ @Nullable
+ public Integer getMaxItems() {
+ return myMaxItems;
+ }
+
+ public void setMaxItems(@Nullable Integer maxItems) {
+ myMaxItems = maxItems;
+ }
+
+ @Nullable
+ public Integer getMinItems() {
+ return myMinItems;
+ }
+
+ public void setMinItems(@Nullable Integer minItems) {
+ myMinItems = minItems;
+ }
+
+ public boolean isUniqueItems() {
+ return Boolean.TRUE.equals(myUniqueItems);
+ }
+
+ public void setUniqueItems(boolean uniqueItems) {
+ myUniqueItems = uniqueItems;
+ }
+
+ @Nullable
+ public Integer getMaxProperties() {
+ return myMaxProperties;
+ }
+
+ public void setMaxProperties(@Nullable Integer maxProperties) {
+ myMaxProperties = maxProperties;
+ }
+
+ @Nullable
+ public Integer getMinProperties() {
+ return myMinProperties;
+ }
+
+ public void setMinProperties(@Nullable Integer minProperties) {
+ myMinProperties = minProperties;
+ }
+
+ @Nullable
+ public Set<String> getRequired() {
+ return myRequired;
+ }
+
+ public void setRequired(@Nullable Set<String> required) {
+ myRequired = required;
+ }
+
+ @Nullable
+ public Map<String, List<String>> getPropertyDependencies() {
+ return myPropertyDependencies;
+ }
+
+ public void setPropertyDependencies(@Nullable Map<String, List<String>> propertyDependencies) {
+ myPropertyDependencies = propertyDependencies;
+ }
+
+ @Nullable
+ public Map<String, JsonSchemaObject> getSchemaDependencies() {
+ return mySchemaDependencies;
+ }
+
+ public void setSchemaDependencies(@Nullable Map<String, JsonSchemaObject> schemaDependencies) {
+ mySchemaDependencies = schemaDependencies;
+ }
+
+ @Nullable
+ public List<Object> getEnum() {
+ return myEnum;
+ }
+
+ public void setEnum(@Nullable List<Object> anEnum) {
+ myEnum = anEnum;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getAllOf() {
+ return myAllOf;
+ }
+
+ public void setAllOf(@Nullable List<JsonSchemaObject> allOf) {
+ myAllOf = allOf;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getAnyOf() {
+ return myAnyOf;
+ }
+
+ public void setAnyOf(@Nullable List<JsonSchemaObject> anyOf) {
+ myAnyOf = anyOf;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getOneOf() {
+ return myOneOf;
+ }
+
+ public void setOneOf(@Nullable List<JsonSchemaObject> oneOf) {
+ myOneOf = oneOf;
+ }
+
+ @Nullable
+ public JsonSchemaObject getNot() {
+ return myNot;
+ }
+
+ public void setNot(@Nullable JsonSchemaObject not) {
+ myNot = not;
+ }
+
+ @Nullable
+ public JsonSchemaObject getIf() {
+ return myIf;
+ }
+
+ public void setIf(@Nullable JsonSchemaObject anIf) {
+ myIf = anIf;
+ }
+
+ @Nullable
+ public JsonSchemaObject getThen() {
+ return myThen;
+ }
+
+ public void setThen(@Nullable JsonSchemaObject then) {
+ myThen = then;
+ }
+
+ @Nullable
+ public JsonSchemaObject getElse() {
+ return myElse;
+ }
+
+ public void setElse(@Nullable JsonSchemaObject anElse) {
+ myElse = anElse;
+ }
+
+ @Nullable
+ public Set<JsonSchemaType> getTypeVariants() {
+ return myTypeVariants;
+ }
+
+ public void setTypeVariants(@Nullable Set<JsonSchemaType> typeVariants) {
+ myTypeVariants = typeVariants;
+ }
+
+ @Nullable
+ public String getRef() {
+ return myRef;
+ }
+
+ public void setRef(@Nullable String ref) {
+ myRef = ref;
+ }
+
+ @Nullable
+ public Object getDefault() {
+ if (JsonSchemaType._integer.equals(myType)) return myDefault instanceof Number ? ((Number)myDefault).intValue() : myDefault;
+ return myDefault;
+ }
+
+ public void setDefault(@Nullable Object aDefault) {
+ myDefault = aDefault;
+ }
+
+ @Nullable
+ public String getFormat() {
+ return myFormat;
+ }
+
+ public void setFormat(@Nullable String format) {
+ myFormat = format;
+ }
+
+ @Nullable
+ public String getId() {
+ return myId;
+ }
+
+ public void setId(@Nullable String id) {
+ myId = id;
+ }
+
+ @Nullable
+ public String getSchema() {
+ return mySchema;
+ }
+
+ public void setSchema(@Nullable String schema) {
+ mySchema = schema;
+ }
+
+ @Nullable
+ public String getDescription() {
+ return myDescription;
+ }
+
+ public void setDescription(@NotNull String description) {
+ myDescription = unescapeJsonString(description);
+ }
+
+ @Nullable
+ public String getHtmlDescription() {
+ return myHtmlDescription;
+ }
+
+ public void setHtmlDescription(@NotNull String htmlDescription) {
+ myHtmlDescription = unescapeJsonString(htmlDescription);
+ }
+
+ @Nullable
+ public String getTitle() {
+ return myTitle;
+ }
+
+ public void setTitle(@NotNull String title) {
+ myTitle = unescapeJsonString(title);
+ }
+
+ private static String unescapeJsonString(@NotNull final String text) {
+ try {
+ final String object = String.format("{\"prop\": \"%s\"}", text);
+ return new Gson().fromJson(object, JsonObject.class).get("prop").getAsString();
+ } catch (JsonParseException e) {
+ return text;
+ }
+ }
+
+ @Nullable
+ public JsonSchemaObject getMatchingPatternPropertySchema(@NotNull String name) {
+ if (myPatternProperties == null) return null;
+ return myPatternProperties.getPatternPropertySchema(name);
+ }
+
+ public boolean checkByPattern(@NotNull String value) {
+ return myPattern != null && myPattern.checkByPattern(value);
+ }
+
+ @Nullable
+ public String getPatternError() {
+ return myPattern == null ? null : myPattern.getPatternError();
+ }
+
+ @Nullable
+ public Map<JsonContainer, String> getInvalidPatternProperties() {
+ if (myPatternProperties != null) {
+ final Map<String, String> patterns = myPatternProperties.getInvalidPatterns();
+
+ return patterns.entrySet().stream().map(entry -> {
+ final JsonSchemaObject object = myPatternProperties.getSchemaForPattern(entry.getKey());
+ assert object != null;
+ return Pair.create(object.getJsonObject(), entry.getValue());
+ }).collect(Collectors.toMap(o -> o.getFirst(), o -> o.getSecond()));
+ }
+ return null;
+ }
+
+ @Nullable
+ public JsonSchemaObject findRelativeDefinition(@NotNull String ref) {
+ if (isSelfReference(ref)) {
+ return this;
+ }
+ if (!ref.startsWith("#/")) {
+ return null;
+ }
+ ref = ref.substring(2);
+ final List<String> parts = split(ref);
+ JsonSchemaObject current = this;
+ for (int i = 0; i < parts.size(); i++) {
+ if (current == null) return null;
+ final String part = parts.get(i);
+ if (DEFINITIONS.equals(part)) {
+ if (i == (parts.size() - 1)) return null;
+ //noinspection AssignmentToForLoopParameter
+ final String nextPart = parts.get(++i);
+ current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(unescapeJsonPointerPart(nextPart));
+ continue;
+ }
+ if (PROPERTIES.equals(part)) {
+ if (i == (parts.size() - 1)) return null;
+ //noinspection AssignmentToForLoopParameter
+ current = current.getProperties().get(unescapeJsonPointerPart(parts.get(++i)));
+ continue;
+ }
+ if (ITEMS.equals(part)) {
+ if (i == (parts.size() - 1)) {
+ current = current.getItemsSchema();
+ }
+ else {
+ //noinspection AssignmentToForLoopParameter
+ Integer next = tryParseInt(parts.get(++i));
+ List<JsonSchemaObject> itemsSchemaList = current.getItemsSchemaList();
+ if (itemsSchemaList != null && next != null && next < itemsSchemaList.size()) {
+ current = itemsSchemaList.get(next);
+ }
+ }
+ continue;
+ }
+ if (ADDITIONAL_ITEMS.equals(part)) {
+ if (i == (parts.size() - 1)) {
+ current = current.getAdditionalItemsSchema();
+ }
+ continue;
+ }
+
+ current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(part);
+ }
+ return current;
+ }
+
+ @Nullable
+ private static Integer tryParseInt(String s) {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (Exception __) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JsonSchemaObject object = (JsonSchemaObject)o;
+
+ assert myJsonObject != null;
+ return myJsonObject.equals(object.myJsonObject);
+ }
+
+ @Override
+ public int hashCode() {
+ assert myJsonObject != null;
+ return myJsonObject.hashCode();
+ }
+
+ @NotNull
+ private static String adaptSchemaPattern(String pattern) {
+ pattern = pattern.startsWith("^") || pattern.startsWith("*") || pattern.startsWith(".") ? pattern : (".*" + pattern);
+ pattern = pattern.endsWith("+") || pattern.endsWith("*") || pattern.endsWith("$") ? pattern : (pattern + ".*");
+ pattern = pattern.replace("\\\\", "\\");
+ return pattern;
+ }
+
+
+ private static Pair<Pattern, String> compilePattern(@NotNull final String pattern) {
+ try {
+ return Pair.create(Pattern.compile(adaptSchemaPattern(pattern)), null);
+ } catch (PatternSyntaxException e) {
+ return Pair.create(null, e.getMessage());
+ }
+ }
+
+ public static boolean matchPattern(@NotNull final Pattern pattern, @NotNull final String s) {
+ try {
+ return pattern.matcher(StringUtil.newBombedCharSequence(s, 300)).matches();
+ } catch (ProcessCanceledException e) {
+ // something wrong with the pattern, infinite cycle?
+ Logger.getInstance(JsonSchemaObject.class).info("Pattern matching canceled");
+ return false;
+ } catch (Exception e) {
+ // catch exceptions around to prevent things like:
+ // https://bugs.openjdk.java.net/browse/JDK-6984178
+ Logger.getInstance(JsonSchemaObject.class).info(e);
+ return false;
+ }
+ }
+
+ @Nullable
+ public String getTypeDescription(boolean shortDesc) {
+ JsonSchemaType type = getType();
+ if (type != null) return type.getDescription();
+
+ Set<JsonSchemaType> possibleTypes = getTypeVariants();
+
+ String description = getTypesDescription(shortDesc, possibleTypes);
+ if (description != null) return description;
+
+ List<Object> anEnum = getEnum();
+ if (anEnum != null) {
+ return shortDesc ? "enum" : anEnum.stream().map(o -> o.toString()).collect(Collectors.joining(" | "));
+ }
+
+ JsonSchemaType guessedType = guessType();
+ if (guessedType != null) {
+ return guessedType.getDescription();
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public JsonSchemaType guessType() {
+ // if we have an explicit type, here we are
+ JsonSchemaType type = getType();
+ if (type != null) return type;
+
+ // process type variants before heuristic type detection
+ final Set<JsonSchemaType> typeVariants = getTypeVariants();
+ if (typeVariants != null) {
+ final int size = typeVariants.size();
+ if (size == 1) {
+ return typeVariants.iterator().next();
+ }
+ else if (size >= 2) {
+ return null;
+ }
+ }
+
+ // heuristic type detection based on the set of applied constraints
+ boolean hasObjectChecks = hasObjectChecks();
+ boolean hasNumericChecks = hasNumericChecks();
+ boolean hasStringChecks = hasStringChecks();
+ boolean hasArrayChecks = hasArrayChecks();
+
+ if (hasObjectChecks && !hasNumericChecks && !hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._object;
+ }
+ if (!hasObjectChecks && hasNumericChecks && !hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._number;
+ }
+ if (!hasObjectChecks && !hasNumericChecks && hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._string;
+ }
+ if (!hasObjectChecks && !hasNumericChecks && !hasStringChecks && hasArrayChecks) {
+ return JsonSchemaType._array;
+ }
+ return null;
+ }
+
+ public boolean hasNumericChecks() {
+ return getMultipleOf() != null
+ || getExclusiveMinimumNumber() != null
+ || getExclusiveMaximumNumber() != null
+ || getMaximum() != null
+ || getMinimum() != null;
+ }
+
+ public boolean hasStringChecks() {
+ return getPattern() != null || getFormat() != null;
+ }
+
+ public boolean hasArrayChecks() {
+ return isUniqueItems()
+ || getContainsSchema() != null
+ || getItemsSchema() != null
+ || getItemsSchemaList() != null
+ || getMinItems() != null
+ || getMaxItems() != null;
+ }
+
+ public boolean hasObjectChecks() {
+ return !getProperties().isEmpty()
+ || getPropertyNamesSchema() != null
+ || getPropertyDependencies() != null
+ || hasPatternProperties()
+ || getRequired() != null
+ || getMinProperties() != null
+ || getMaxProperties() != null;
+ }
+
+ @Nullable
+ static String getTypesDescription(boolean shortDesc, @Nullable Collection<JsonSchemaType> possibleTypes) {
+ if (possibleTypes == null || possibleTypes.size() == 0) return null;
+ if (possibleTypes.size() == 1) return possibleTypes.iterator().next().getDescription();
+ if (possibleTypes.contains(JsonSchemaType._any)) return JsonSchemaType._any.getDescription();
+
+ Stream<String> typeDescriptions = possibleTypes.stream().map(t -> t.getDescription()).distinct().sorted();
+ boolean isShort = false;
+ if (shortDesc) {
+ typeDescriptions = typeDescriptions.limit(3);
+ if (possibleTypes.size() > 3) isShort = true;
+ }
+ return typeDescriptions.collect(Collectors.joining(" | ", "", isShort ? "| ..." : ""));
+ }
+
+ @Nullable
+ public JsonSchemaObject resolveRefSchema(@NotNull JsonSchemaService service) {
+ final String ref = getRef();
+ assert !StringUtil.isEmptyOrSpaces(ref);
+ if (!myComputedRefs.containsKey(ref)){
+ JsonSchemaObject value = fetchSchemaFromRefDefinition(ref, this, service);
+ if (!mySubscribed.get()) {
+ getJsonObject().getProject().getMessageBus().connect().subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> myComputedRefs.clear());
+ mySubscribed.set(true);
+ }
+ if (!JsonFileResolver.isHttpPath(ref)) {
+ service.registerReference(ref);
+ }
+ else if (value != null) {
+ // our aliases - if http ref actually refers to a local file with specific ID
+ PsiFile file = value.getJsonObject().getContainingFile();
+ if (file != null) {
+ VirtualFile virtualFile = file.getVirtualFile();
+ if (virtualFile != null && !(virtualFile instanceof HttpVirtualFile)) {
+ service.registerReference(virtualFile.getName());
+ }
+ }
+ }
+ myComputedRefs.put(ref, value == null ? NULL_OBJ : value);
+ }
+ JsonSchemaObject object = myComputedRefs.getOrDefault(ref, null);
+ return object == NULL_OBJ ? null : object;
+ }
+
+ @Nullable
+ private static JsonSchemaObject fetchSchemaFromRefDefinition(@NotNull String ref,
+ @NotNull final JsonSchemaObject schema,
+ @NotNull JsonSchemaService service) {
+
+ final VirtualFile schemaFile = schema.getSchemaFile();
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(ref);
+ String schemaId = splitter.getSchemaId();
+ if (schemaId != null) {
+ final JsonSchemaObject refSchema = resolveSchemaByReference(service, schemaFile, schemaId);
+ if (refSchema == null) return null;
+ return findRelativeDefinition(refSchema, splitter);
+ }
+ final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (rootSchema == null) {
+ LOG.debug(String.format("Schema object not found for %s", schemaFile.getPath()));
+ return null;
+ }
+ return findRelativeDefinition(rootSchema, splitter);
+ }
+
+ @Nullable
+ private static JsonSchemaObject resolveSchemaByReference(@NotNull JsonSchemaService service,
+ @NotNull VirtualFile schemaFile,
+ @NotNull String schemaId) {
+ final VirtualFile refFile = service.findSchemaFileByReference(schemaId, schemaFile);
+ if (refFile == null) {
+ LOG.debug(String.format("Schema file not found by reference: '%s' from %s", schemaId, schemaFile.getPath()));
+ return null;
+ }
+ final JsonSchemaObject refSchema = service.getSchemaObjectForSchemaFile(refFile);
+ if (refSchema == null) {
+ LOG.debug(String.format("Schema object not found by reference: '%s' from %s", schemaId, schemaFile.getPath()));
+ return null;
+ }
+ return refSchema;
+ }
+
+ private static JsonSchemaObject findRelativeDefinition(@NotNull final JsonSchemaObject schema,
+ @NotNull final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter) {
+ final String path = splitter.getRelativePath();
+ if (StringUtil.isEmptyOrSpaces(path)) {
+ final String id = splitter.getSchemaId();
+ if (isSelfReference(id)) {
+ return schema;
+ }
+ if (id != null && id.startsWith("#")) {
+ final String resolvedId = JsonCachedValues.resolveId(schema.getJsonObject().getContainingFile(), id);
+ if (resolvedId == null || id.equals("#" + resolvedId)) return null;
+ return findRelativeDefinition(schema, new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter("#" + resolvedId));
+ }
+ return schema;
+ }
+ final JsonSchemaObject definition = schema.findRelativeDefinition(path);
+ if (definition == null) {
+ LOG.debug(String.format("Definition not found by reference: '%s' in file %s", path, schema.getSchemaFile().getPath()));
+ }
+ return definition;
+ }
+
+ private static class PropertyNamePattern {
+ @NotNull private final String myPattern;
+ @Nullable private final Pattern myCompiledPattern;
+ @Nullable private final String myPatternError;
+ @NotNull private final Map<String, Boolean> myValuePatternCache;
+
+ PropertyNamePattern(@NotNull String pattern) {
+ myPattern = StringUtil.unescapeBackSlashes(pattern);
+ final Pair<Pattern, String> pair = compilePattern(pattern);
+ myPatternError = pair.getSecond();
+ myCompiledPattern = pair.getFirst();
+ myValuePatternCache = ContainerUtil.createConcurrentWeakKeyWeakValueMap();
+ }
+
+ @Nullable
+ public String getPatternError() {
+ return myPatternError;
+ }
+
+ boolean checkByPattern(@NotNull final String name) {
+ if (myPatternError != null) return true;
+ if (Boolean.TRUE.equals(myValuePatternCache.get(name))) return true;
+ assert myCompiledPattern != null;
+ boolean matches = matchPattern(myCompiledPattern, name);
+ myValuePatternCache.put(name, matches);
+ return matches;
+ }
+
+ @NotNull
+ public String getPattern() {
+ return myPattern;
+ }
+ }
+
+ private static class PatternProperties {
+ @NotNull private final Map<String, JsonSchemaObject> mySchemasMap;
+ @NotNull private final Map<String, Pattern> myCachedPatterns;
+ @NotNull private final Map<String, String> myCachedPatternProperties;
+ @NotNull private final Map<String, String> myInvalidPatterns;
+
+ PatternProperties(@NotNull final Map<String, JsonSchemaObject> schemasMap) {
+ mySchemasMap = new HashMap<>();
+ schemasMap.keySet().forEach(key -> mySchemasMap.put(StringUtil.unescapeBackSlashes(key), schemasMap.get(key)));
+ myCachedPatterns = new HashMap<>();
+ myCachedPatternProperties = ContainerUtil.createConcurrentWeakKeyWeakValueMap();
+ myInvalidPatterns = new HashMap<>();
+ mySchemasMap.keySet().forEach(key -> {
+ final Pair<Pattern, String> pair = compilePattern(key);
+ if (pair.getSecond() != null) {
+ myInvalidPatterns.put(key, pair.getSecond());
+ } else {
+ assert pair.getFirst() != null;
+ myCachedPatterns.put(key, pair.getFirst());
+ }
+ });
+ }
+
+ @Nullable
+ public JsonSchemaObject getPatternPropertySchema(@NotNull final String name) {
+ String value = myCachedPatternProperties.get(name);
+ if (value != null) {
+ assert mySchemasMap.containsKey(value);
+ return mySchemasMap.get(value);
+ }
+
+ value = myCachedPatterns.keySet().stream()
+ .filter(key -> matchPattern(myCachedPatterns.get(key), name))
+ .findFirst()
+ .orElse(null);
+ if (value != null) {
+ myCachedPatternProperties.put(name, value);
+ assert mySchemasMap.containsKey(value);
+ return mySchemasMap.get(value);
+ }
+ return null;
+ }
+
+ @NotNull
+ public Map<String, String> getInvalidPatterns() {
+ return myInvalidPatterns;
+ }
+
+ public JsonSchemaObject getSchemaForPattern(@NotNull String key) {
+ return mySchemasMap.get(key);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java
new file mode 100644
index 00000000..c93c65c0
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java
@@ -0,0 +1,511 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.*;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.PairConsumer;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 1/13/2017.
+ */
+public class JsonSchemaReader {
+ private static final int MAX_SCHEMA_LENGTH = FileUtilRt.MEGABYTE;
+ public static final Logger LOG = Logger.getInstance(JsonSchemaReader.class);
+ public static final NotificationGroup ERRORS_NOTIFICATION = NotificationGroup.logOnlyGroup("JSON Schema");
+
+ private final Map<String, JsonSchemaObject> myIds = new HashMap<>();
+ private final ArrayDeque<JsonSchemaObject> myQueue;
+
+ private static final Map<String, MyReader> READERS_MAP = new HashMap<>();
+ static {
+ fillMap();
+ }
+
+ public JsonSchemaReader() {
+ myQueue = new ArrayDeque<>();
+ }
+
+ @NotNull
+ public static JsonSchemaObject readFromFile(@NotNull Project project, @NotNull VirtualFile file) throws Exception {
+ if (!file.isValid()) {
+ throw new Exception(String.format("Can not load JSON Schema file '%s'", file.getName()));
+ }
+
+ final PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+ if (!(psiFile instanceof JsonFile)) {
+ throw new Exception(String.format("Can not load code model for JSON Schema file '%s'", file.getName()));
+ }
+
+ final JsonObject value = ObjectUtils.tryCast(((JsonFile)psiFile).getTopLevelValue(), JsonObject.class);
+ if (value == null) {
+ throw new Exception(String.format("JSON Schema file '%s' must contain only one top-level object", file.getName()));
+ }
+ return new JsonSchemaReader().read(value);
+ }
+
+ @Nullable
+ public static String checkIfValidJsonSchema(@NotNull final Project project, @NotNull final VirtualFile file) {
+ final long length = file.getLength();
+ final String fileName = file.getName();
+ if (length > MAX_SCHEMA_LENGTH) {
+ return String.format("JSON schema was not loaded from '%s' because it's too large (file size is %d bytes).", fileName, length);
+ }
+ if (length == 0) {
+ return String.format("JSON schema was not loaded from '%s'. File is empty.", fileName);
+ }
+ try {
+ readFromFile(project, file);
+ } catch (Exception e) {
+ final String message = String.format("JSON Schema not found or contain error in '%s': %s", fileName, e.getMessage());
+ LOG.info(message);
+ return message;
+ }
+ return null;
+ }
+
+ public JsonSchemaObject read(@NotNull final JsonObject object) {
+ final JsonSchemaObject root = new JsonSchemaObject(object);
+ myQueue.add(root);
+ while (!myQueue.isEmpty()) {
+ final JsonSchemaObject currentSchema = myQueue.removeFirst();
+
+ final JsonContainer jsonObject = currentSchema.getJsonObject();
+ if (jsonObject instanceof JsonObject) {
+ final List<JsonProperty> list = ((JsonObject)jsonObject).getPropertyList();
+ for (JsonProperty property : list) {
+ if (property.getValue() == null) continue;
+ final MyReader reader = READERS_MAP.get(property.getName());
+ if (reader != null) {
+ reader.read(property.getValue(), currentSchema, myQueue);
+ }
+ else {
+ readSingleDefinition(property.getName(), property.getValue(), currentSchema);
+ }
+ }
+ }
+ else if (jsonObject instanceof JsonArray) {
+ List<JsonValue> values = ((JsonArray)jsonObject).getValueList();
+ for (int i = 0; i < values.size(); i++) {
+ readSingleDefinition(String.valueOf(i), values.get(i), currentSchema);
+ }
+ }
+
+ if (currentSchema.getId() != null) myIds.put(currentSchema.getId(), currentSchema);
+ }
+ return root;
+ }
+
+ public Map<String, JsonSchemaObject> getIds() {
+ return myIds;
+ }
+
+ private void readSingleDefinition(@NotNull String name, @NotNull JsonValue value, @NotNull JsonSchemaObject schema) {
+ if (value instanceof JsonContainer) {
+ final JsonSchemaObject defined = new JsonSchemaObject((JsonContainer)value);
+ myQueue.add(defined);
+ Map<String, JsonSchemaObject> definitions = schema.getDefinitionsMap();
+ if (definitions == null) schema.setDefinitionsMap(definitions = new HashMap<>());
+ definitions.put(name, defined);
+ }
+ }
+
+ private static void fillMap() {
+ READERS_MAP.put("$id", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("id", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("$schema", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setSchema(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("description", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setDescription(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.X_INTELLIJ_HTML_DESCRIPTION, (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setHtmlDescription(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("title", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setTitle(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("$ref", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setRef(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("default", createDefault());
+ READERS_MAP.put("format", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setFormat(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.DEFINITIONS, createDefinitionsConsumer());
+ READERS_MAP.put(JsonSchemaObject.PROPERTIES, createPropertiesConsumer());
+ READERS_MAP.put("multipleOf", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMultipleOf(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("maximum", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaximum(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minimum", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinimum(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("exclusiveMaximum", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setExclusiveMaximum(((JsonBooleanLiteral)element).getValue());
+ if (element instanceof JsonNumberLiteral) object.setExclusiveMaximumNumber(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("exclusiveMinimum", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setExclusiveMinimum(((JsonBooleanLiteral)element).getValue());
+ if (element instanceof JsonNumberLiteral) object.setExclusiveMinimumNumber(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("maxLength", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxLength((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minLength", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinLength((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("pattern", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setPattern(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.ADDITIONAL_ITEMS, createAdditionalItems());
+ READERS_MAP.put(JsonSchemaObject.ITEMS, createItems());
+ READERS_MAP.put("contains", createContains());
+ READERS_MAP.put("maxItems", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxItems((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minItems", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinItems((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("uniqueItems", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setUniqueItems(((JsonBooleanLiteral)element).getValue());
+ });
+ READERS_MAP.put("maxProperties", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxProperties((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minProperties", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinProperties((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("required", createRequired());
+ READERS_MAP.put("additionalProperties", createAdditionalProperties());
+ READERS_MAP.put("propertyNames", createPropertyNames());
+ READERS_MAP.put("patternProperties", createPatternProperties());
+ READERS_MAP.put("dependencies", createDependencies());
+ READERS_MAP.put("enum", createEnum());
+ READERS_MAP.put("const", (element, object, queue) -> {
+ if (element instanceof JsonValue) object.setEnum(ContainerUtil.createMaybeSingletonList(readEnumValue((JsonValue)element)));
+ });
+ READERS_MAP.put("type", createType());
+ READERS_MAP.put("allOf", createContainer((object, members) -> object.setAllOf(members)));
+ READERS_MAP.put("anyOf", createContainer((object, members) -> object.setAnyOf(members)));
+ READERS_MAP.put("oneOf", createContainer((object, members) -> object.setOneOf(members)));
+ READERS_MAP.put("not", createNot());
+ READERS_MAP.put("if", createIf());
+ READERS_MAP.put("then", createThen());
+ READERS_MAP.put("else", createElse());
+ READERS_MAP.put("instanceof", ((element, object, queue) -> object.shouldValidateAgainstJSType()));
+ READERS_MAP.put("typeof", ((element, object, queue) -> object.shouldValidateAgainstJSType()));
+ }
+
+ private static MyReader createIf() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setIf(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createThen() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setThen(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createElse() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setElse(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createNot() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject not = new JsonSchemaObject((JsonObject)element);
+ queue.add(not);
+ object.setNot(not);
+ }
+ };
+ }
+
+ private static MyReader createContainer(@NotNull final PairConsumer<JsonSchemaObject, List<JsonSchemaObject>> delegate) {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ final List<JsonValue> list = ((JsonArray)element).getValueList();
+ final List<JsonSchemaObject> members = list.stream().filter(el -> el instanceof JsonObject)
+ .map(el -> {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)el);
+ queue.add(child);
+ return child;
+ }).collect(Collectors.toList());
+ delegate.consume(object, members);
+ }
+ };
+ }
+
+ private static MyReader createType() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) {
+ final JsonSchemaType type = parseType(StringUtil.unquoteString(element.getText()));
+ if (type != null) object.setType(type);
+ } else if (element instanceof JsonArray) {
+ final Set<JsonSchemaType> typeList = ((JsonArray)element).getValueList().stream()
+ .filter(notEmptyString()).map(el -> parseType(StringUtil.unquoteString(el.getText())))
+ .filter(el -> el != null).collect(Collectors.toSet());
+ if (!typeList.isEmpty()) object.setTypeVariants(typeList);
+ }
+ };
+ }
+
+ @Nullable
+ private static JsonSchemaType parseType(@NotNull final String typeString) {
+ try {
+ return JsonSchemaType.valueOf("_" + typeString);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ private static Object readEnumValue(JsonValue value) {
+ if (value instanceof JsonStringLiteral) {
+ return "\"" + StringUtil.unquoteString(((JsonStringLiteral)value).getValue()) + "\"";
+ } else if (value instanceof JsonNumberLiteral) {
+ return getNumber((JsonNumberLiteral)value);
+ } else if (value instanceof JsonBooleanLiteral) {
+ return ((JsonBooleanLiteral)value).getValue();
+ } else if (value instanceof JsonNullLiteral) {
+ return "null";
+ } else if (value instanceof JsonArray) {
+ return new EnumArrayValueWrapper(((JsonArray)value).getValueList().stream().map(v -> readEnumValue(v)).filter(v -> v != null).toArray());
+ } else if (value instanceof JsonObject) {
+ return new EnumObjectValueWrapper(((JsonObject)value).getPropertyList().stream()
+ .map(p -> Pair.create(p.getName(), readEnumValue(p.getValue())))
+ .filter(p -> p.second != null)
+ .collect(Collectors.toMap(p -> p.first, p -> p.second)));
+ }
+ return null;
+ }
+
+ private static MyReader createEnum() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ final List<Object> objects = new ArrayList<>();
+ final List<JsonValue> list = ((JsonArray)element).getValueList();
+ for (JsonValue value : list) {
+ Object enumValue = readEnumValue(value);
+ if (enumValue == null) return; // don't validate if we have unsupported entity kinds
+ objects.add(enumValue);
+ }
+ object.setEnum(objects);
+ }
+ };
+ }
+
+ @NotNull
+ private static Number getNumber(@NotNull JsonNumberLiteral value) {
+ Number numberValue;
+ try {
+ numberValue = Integer.parseInt(value.getText());
+ } catch (NumberFormatException e) {
+ numberValue = value.getValue();
+ }
+ return numberValue;
+ }
+
+ private static MyReader createDependencies() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final HashMap<String, List<String>> propertyDependencies = new HashMap<>();
+ final HashMap<String, JsonSchemaObject> schemaDependencies = new HashMap<>();
+
+ final List<JsonProperty> list = ((JsonObject)element).getPropertyList();
+ for (JsonProperty property : list) {
+ if (property.getValue() == null) continue;
+ if (property.getValue() instanceof JsonArray) {
+ final List<String> dependencies = ((JsonArray)property.getValue()).getValueList().stream()
+ .filter(notEmptyString())
+ .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toList());
+ if (!dependencies.isEmpty()) propertyDependencies.put(property.getName(), dependencies);
+ } else if (property.getValue() instanceof JsonObject) {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue());
+ queue.add(child);
+ schemaDependencies.put(property.getName(), child);
+ }
+ }
+
+ object.setPropertyDependencies(propertyDependencies);
+ object.setSchemaDependencies(schemaDependencies);
+ }
+ };
+ }
+
+ @NotNull
+ private static Predicate<JsonValue> notEmptyString() {
+ return el -> el instanceof JsonStringLiteral && !StringUtil.isEmptyOrSpaces(el.getText());
+ }
+
+ private static MyReader createPatternProperties() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ object.setPatternProperties(readInnerObject((JsonObject)element, queue));
+ }
+ };
+ }
+
+ private static MyReader createAdditionalProperties() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) {
+ object.setAdditionalPropertiesAllowed(((JsonBooleanLiteral)element).getValue());
+ } else if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setAdditionalPropertiesSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createPropertyNames() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setPropertyNamesSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createRequired() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ object.setRequired(((JsonArray)element).getValueList().stream()
+ .filter(notEmptyString())
+ .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toSet()));
+ }
+ };
+ }
+
+ private static MyReader createItems() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setItemsSchema(schema);
+ } else if (element instanceof JsonArray) {
+ final List<JsonSchemaObject> list = new ArrayList<>();
+ final List<JsonValue> values = ((JsonArray)element).getValueList();
+ for (JsonValue value : values) {
+ if (value instanceof JsonObject) {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)value);
+ queue.add(child);
+ list.add(child);
+ }
+ }
+ object.setItemsSchemaList(list);
+ }
+ };
+ }
+
+ private static MyReader createContains() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setContainsSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createAdditionalItems() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) {
+ object.setAdditionalItemsAllowed(((JsonBooleanLiteral)element).getValue());
+ } else if (element instanceof JsonObject) {
+ final JsonSchemaObject additionalItemsSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(additionalItemsSchema);
+ object.setAdditionalItemsSchema(additionalItemsSchema);
+ }
+ };
+ }
+
+ private static MyReader createPropertiesConsumer() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ object.setProperties(readInnerObject((JsonObject)element, queue));
+ }
+ };
+ }
+
+ private static MyReader createDefinitionsConsumer() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonObject definitions = (JsonObject)element;
+ object.setDefinitionsMap(readInnerObject(definitions, queue));
+ }
+ };
+ }
+
+ @NotNull
+ private static Map<String, JsonSchemaObject> readInnerObject(@NotNull JsonObject element,
+ @NotNull Collection<JsonSchemaObject> queue) {
+ final List<JsonProperty> properties = element.getPropertyList();
+ final Map<String, JsonSchemaObject> map = new HashMap<>();
+ for (JsonProperty property : properties) {
+ if (!(property.getValue() instanceof JsonObject)) continue;
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue());
+ queue.add(child);
+ map.put(property.getName(), child);
+ }
+ return map;
+ }
+
+ private static MyReader createDefault() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schemaObject = new JsonSchemaObject((JsonObject)element);
+ queue.add(schemaObject);
+ object.setDefault(schemaObject);
+ } else if (element instanceof JsonStringLiteral) {
+ object.setDefault(StringUtil.unquoteString(((JsonStringLiteral)element).getValue()));
+ } else if (element instanceof JsonNumberLiteral) {
+ object.setDefault(getNumber((JsonNumberLiteral) element));
+ } else if (element instanceof JsonBooleanLiteral) {
+ object.setDefault(((JsonBooleanLiteral)element).getValue());
+ }
+ };
+ }
+
+ private interface MyReader {
+ void read(@NotNull JsonElement source, @NotNull JsonSchemaObject target, @NotNull Collection<JsonSchemaObject> processingQueue);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java
new file mode 100644
index 00000000..9ff7243c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.json.psi.*;
+import com.intellij.patterns.PlatformPatterns;
+import com.intellij.patterns.PsiElementPattern;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReferenceContributor;
+import com.intellij.psi.PsiReferenceRegistrar;
+import com.intellij.psi.filters.ElementFilter;
+import com.intellij.psi.filters.position.FilterPattern;
+import com.intellij.util.ObjectUtils;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 3/31/2016.
+ */
+public class JsonSchemaReferenceContributor extends PsiReferenceContributor {
+ public static final PsiElementPattern.Capture<JsonValue> REF_PATTERN = createPropertyValuePattern("$ref", true, false);
+ public static final PsiElementPattern.Capture<JsonValue> SCHEMA_PATTERN = createPropertyValuePattern("$schema", false, true);
+ public static final PsiElementPattern.Capture<JsonStringLiteral> REQUIRED_PROP_PATTERN = createRequiredPropPattern();
+
+ @Override
+ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
+ registrar.registerReferenceProvider(REF_PATTERN, new JsonPointerReferenceProvider(false));
+ registrar.registerReferenceProvider(SCHEMA_PATTERN, new JsonPointerReferenceProvider(true));
+ registrar.registerReferenceProvider(REQUIRED_PROP_PATTERN, new JsonRequiredPropsReferenceProvider());
+ }
+
+ private static PsiElementPattern.Capture<JsonValue> createPropertyValuePattern(
+ @SuppressWarnings("SameParameterValue") @NotNull final String propertyName, boolean schemaOnly, boolean rootOnly) {
+
+ return PlatformPatterns.psiElement(JsonValue.class).and(new FilterPattern(new ElementFilter() {
+ @Override
+ public boolean isAcceptable(Object element, @Nullable PsiElement context) {
+ if (element instanceof JsonValue) {
+ final JsonValue value = (JsonValue) element;
+ if (schemaOnly && !JsonSchemaService.isSchemaFile(CompletionUtil.getOriginalOrSelf(value.getContainingFile()))) return false;
+
+ final JsonProperty property = ObjectUtils.tryCast(value.getParent(), JsonProperty.class);
+ if (property != null && property.getValue() == element) {
+ final PsiFile file = property.getContainingFile();
+ if (rootOnly && (!(file instanceof JsonFile) || ((JsonFile)file).getTopLevelValue() != property.getParent())) return false;
+ return propertyName.equals(property.getName());
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isClassAcceptable(Class hintClass) {
+ return true;
+ }
+ }));
+ }
+
+ private static PsiElementPattern.Capture<JsonStringLiteral> createRequiredPropPattern() {
+ return PlatformPatterns.psiElement(JsonStringLiteral.class).and(new FilterPattern(new ElementFilter() {
+ @Override
+ public boolean isAcceptable(Object element, @Nullable PsiElement context) {
+ if (!(element instanceof JsonStringLiteral)) return false;
+ if (!JsonSchemaService.isSchemaFile(((JsonStringLiteral)element).getContainingFile())) return false;
+ final PsiElement parent = ((JsonStringLiteral)element).getParent();
+ if (!(parent instanceof JsonArray)) return false;
+ PsiElement property = parent.getParent();
+ if (!(property instanceof JsonProperty)) return false;
+ return "required".equals(((JsonProperty)property).getName());
+ }
+
+ @Override
+ public boolean isClassAcceptable(Class hintClass) {
+ return true;
+ }
+ }));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java
new file mode 100644
index 00000000..588551ca
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java
@@ -0,0 +1,68 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.injection.MultiHostInjector;
+import com.intellij.lang.injection.MultiHostRegistrar;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.intellij.lang.regexp.ecmascript.EcmaScriptRegexpLanguage;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class JsonSchemaRegexInjector implements MultiHostInjector {
+ @Override
+ public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
+ if (!JsonSchemaService.isSchemaFile(context.getContainingFile())) return;
+ JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER;
+ if (!(context instanceof JsonStringLiteral)) return;
+ ThreeState isName = walker.isName(context);
+ List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(context, isName == ThreeState.NO);
+ if (position == null || position.isEmpty()) return;
+ if (isName == ThreeState.YES) {
+ JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position);
+ if (lastStep != null && "patternProperties".equals(lastStep.getName())) {
+ if (isNestedInPropertiesList(position)) return;
+ injectForHost(registrar, (JsonStringLiteral)context);
+ }
+ }
+ else if (isName == ThreeState.NO) {
+ JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position);
+ if (lastStep != null && "pattern".equals(lastStep.getName())) {
+ if (isNestedInPropertiesList(position)) return;
+ injectForHost(registrar, (JsonStringLiteral)context);
+ }
+ }
+ }
+
+ private static boolean isNestedInPropertiesList(List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ if (position.size() >= 2) {
+ JsonSchemaVariantsTreeBuilder.Step prev = position.get(position.size() - 2);
+ if ("properties".equals(prev.getName())) return true;
+ }
+ return false;
+ }
+
+ private static void injectForHost(@NotNull MultiHostRegistrar registrar, @NotNull JsonStringLiteral host) {
+ List<Pair<TextRange, String>> fragments = host.getTextFragments();
+ if (fragments.isEmpty()) return;
+ registrar.startInjecting(EcmaScriptRegexpLanguage.INSTANCE);
+ for (Pair<TextRange, String> fragment : fragments) {
+ registrar.addPlace(null, null, (PsiLanguageInjectionHost)host, fragment.first);
+ }
+ registrar.doneInjecting();
+ }
+
+ @NotNull
+ @Override
+ public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
+ return ContainerUtil.list(JsonStringLiteral.class);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java
new file mode 100644
index 00000000..269b94af
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.*;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.impl.JsonSchemaAnnotatorChecker.areSchemaTypesCompatible;
+
+/**
+ * @author Irina.Chernushina on 4/24/2017.
+ */
+public class JsonSchemaResolver {
+ @NotNull private final JsonSchemaObject mySchema;
+ private final boolean myIsName;
+ @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> myPosition;
+
+ public JsonSchemaResolver(@NotNull JsonSchemaObject schema, boolean isName, @NotNull List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ mySchema = schema;
+ myIsName = isName;
+ myPosition = position;
+ }
+
+ public JsonSchemaResolver(@NotNull JsonSchemaObject schema) {
+ mySchema = schema;
+ myIsName = true;
+ myPosition = Collections.emptyList();
+ }
+
+ public MatchResult detailedResolve() {
+ final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder.buildTree(mySchema, myPosition, false, false, !myIsName);
+ return MatchResult.create(node);
+ }
+
+ @NotNull
+ public Collection<JsonSchemaObject> resolve() {
+ final MatchResult result = detailedResolve();
+ final List<JsonSchemaObject> list = new ArrayList<>(result.mySchemas);
+ list.addAll(result.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList()));
+ return list;
+ }
+
+ @Nullable
+ public PsiElement findNavigationTarget(boolean literalResolve,
+ @Nullable final JsonValue element,
+ boolean acceptAdditionalPropertiesSchema) {
+ final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder
+ .buildTree(mySchema, myPosition, true, literalResolve, acceptAdditionalPropertiesSchema || !myIsName);
+ return getSchemaNavigationItem(selectSchema(node, element, myPosition.isEmpty()));
+ }
+
+ @Nullable
+ private static JsonSchemaObject selectSchema(@NotNull final JsonSchemaTreeNode resolveRoot,
+ @Nullable final JsonValue element, boolean topLevelSchema) {
+ final MatchResult matchResult = MatchResult.create(resolveRoot);
+ List<JsonSchemaObject> schemas = new ArrayList<>(matchResult.mySchemas);
+ schemas.addAll(matchResult.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList()));
+
+ final JsonSchemaObject firstSchema = getFirstValidSchema(schemas);
+ if (element == null || schemas.size() == 1 || firstSchema == null) {
+ return firstSchema;
+ }
+ // actually we pass any schema here
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, firstSchema);
+ JsonValueAdapter adapter;
+ if (walker == null || (adapter = walker.createValueAdapter(element)) == null) return null;
+
+ final JsonValueAdapter parentAdapter;
+ if (topLevelSchema) {
+ parentAdapter = null;
+ } else {
+ final JsonValue parentValue = PsiTreeUtil.getParentOfType(PsiTreeUtil.getParentOfType(element, JsonProperty.class),
+ JsonObject.class, JsonArray.class);
+ if (parentValue == null || (parentAdapter = walker.createValueAdapter(parentValue)) == null) return null;
+ }
+
+ final Ref<JsonSchemaObject> schemaRef = new Ref<>();
+ MatchResult.iterateTree(resolveRoot, node -> {
+ final JsonSchemaTreeNode parent = node.getParent();
+ if (node.getSchema() == null || parentAdapter != null && parent != null && parent.isNothing()) return true;
+ if (!isCorrect(adapter, node.getSchema())) return true;
+ if (parentAdapter == null ||
+ parent == null ||
+ parent.getSchema() == null ||
+ parent.isAny() ||
+ isCorrect(parentAdapter, parent.getSchema())) {
+ schemaRef.set(node.getSchema());
+ return false;
+ }
+ return true;
+ });
+ return schemaRef.get();
+ }
+
+ @Nullable
+ private static JsonSchemaObject getFirstValidSchema(List<JsonSchemaObject> schemas) {
+ return schemas.stream().filter(schema -> schema.getJsonObject().isValid()).findFirst().orElse(null);
+ }
+
+ private static boolean isCorrect(@NotNull final JsonValueAdapter value, @NotNull final JsonSchemaObject schema) {
+ if (!schema.getJsonObject().isValid()) return false;
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ if (type == null) return true;
+ if (!areSchemaTypesCompatible(schema, type)) return false;
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK);
+ checker.checkByScheme(value, schema);
+ return checker.isCorrect();
+ }
+
+ @Nullable
+ private static JsonValue getSchemaNavigationItem(@Nullable final JsonSchemaObject schema) {
+ if (schema == null) return null;
+ final JsonContainer jsonObject = schema.getJsonObject();
+ if (jsonObject.getParent() instanceof JsonProperty) {
+ return ((JsonProperty)jsonObject.getParent()).getNameElement();
+ }
+ return jsonObject;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java
new file mode 100644
index 00000000..0c07625c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java
@@ -0,0 +1,488 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.json.JsonUtil;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ClearableLazyValue;
+import com.intellij.openapi.util.Factory;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.containers.ConcurrentList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.util.messages.MessageBusConnection;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.JsonSchemaVfsListener;
+import com.jetbrains.jsonSchema.extension.*;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import com.jetbrains.jsonSchema.remote.JsonSchemaCatalogManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+public class JsonSchemaServiceImpl implements JsonSchemaService {
+ @NotNull private final Project myProject;
+ @NotNull private final MyState myState;
+ @NotNull private final ClearableLazyValue<Set<String>> myBuiltInSchemaIds;
+ @NotNull private final Set<String> myRefs = ContainerUtil.newConcurrentSet();
+ private final AtomicLong myAnyChangeCount = new AtomicLong(0);
+ private final ModificationTracker myAnySchemaChangeTracker;
+
+ @NotNull private final JsonSchemaCatalogManager myCatalogManager;
+
+ public JsonSchemaServiceImpl(@NotNull Project project) {
+ myProject = project;
+ myState = new MyState(() -> getProvidersFromFactories(), myProject);
+ myBuiltInSchemaIds = new ClearableLazyValue<Set<String>>() {
+ @NotNull
+ @Override
+ protected Set<String> compute() {
+ return myState.getFiles().stream().map(f -> JsonCachedValues.getSchemaId(f, myProject)).collect(Collectors.toSet());
+ }
+ };
+ myAnySchemaChangeTracker = () -> myAnyChangeCount.get();
+ myCatalogManager = new JsonSchemaCatalogManager(myProject);
+
+ MessageBusConnection connection = project.getMessageBus().connect();
+ connection.subscribe(JsonSchemaVfsListener.JSON_SCHEMA_CHANGED, myAnyChangeCount::incrementAndGet);
+ connection.subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> {
+ myRefs.clear();
+ myAnyChangeCount.incrementAndGet();
+ });
+ JsonSchemaVfsListener.startListening(project, this, connection);
+ myCatalogManager.startUpdates();
+ }
+
+ @Override
+ public ModificationTracker getAnySchemaChangeTracker() {
+ return myAnySchemaChangeTracker;
+ }
+
+ private List<JsonSchemaFileProvider> getProvidersFromFactories() {
+ List<JsonSchemaFileProvider> providers = new ArrayList<>();
+ for (JsonSchemaProviderFactory factory : getProviderFactories()) {
+ try {
+ providers.addAll(factory.getProviders(myProject));
+ }
+ catch (Exception e) {
+ Logger.getInstance(JsonSchemaService.class).error(e);
+ }
+ }
+ return providers;
+ }
+
+ @NotNull
+ protected JsonSchemaProviderFactory[] getProviderFactories() {
+ return JsonSchemaProviderFactory.EP_NAME.getExtensions();
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaFileProvider getSchemaProvider(@NotNull VirtualFile schemaFile) {
+ return myState.getProvider(schemaFile);
+ }
+
+ @Override
+ public void reset() {
+ myState.reset();
+ myBuiltInSchemaIds.drop();
+ myAnyChangeCount.incrementAndGet();
+ for (Runnable action: myResetActions) {
+ action.run();
+ }
+ DaemonCodeAnalyzer.getInstance(myProject).restart();
+ }
+
+ @Override
+ @Nullable
+ public VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent) {
+ final Optional<VirtualFile> optional = findBuiltInSchemaByReference(reference);
+ return optional.orElseGet(() -> {
+ if (reference.startsWith("#")) return referent;
+ return JsonFileResolver.resolveSchemaByReference(referent, JsonSchemaService.normalizeId(reference));
+ });
+ }
+
+ private Optional<VirtualFile> findBuiltInSchemaByReference(@NotNull String reference) {
+ String id = JsonSchemaService.normalizeId(reference);
+ if (!myBuiltInSchemaIds.getValue().contains(id)) return Optional.empty();
+ return myState.getFiles().stream()
+ .filter(file -> id.equals(JsonCachedValues.getSchemaId(file, myProject)))
+ .findFirst();
+ }
+
+ @Override
+ @NotNull
+ public Collection<VirtualFile> getSchemaFilesForFile(@NotNull final VirtualFile file) {
+ return getSchemasForFile(file, false, false);
+ }
+
+ @NotNull
+ public Collection<VirtualFile> getSchemasForFile(@NotNull VirtualFile file, boolean single, boolean onlyUserSchemas) {
+ String schemaUrl = null;
+ if (!onlyUserSchemas) {
+ // prefer schema-schema if it is specified in "$schema" property
+ schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ if (isSchemaUrl(schemaUrl)) {
+ final VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ }
+ }
+
+
+ List<JsonSchemaFileProvider> providers = getProvidersForFile(file);
+
+ // proper priority:
+ // 1) user providers
+ // 2) $schema property
+ // 3) built-in providers
+ // 4) schema catalog
+
+ boolean checkSchemaProperty = true;
+ if (!onlyUserSchemas && providers.stream().noneMatch(p -> p.getSchemaType() == SchemaType.userSchema)) {
+ if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ checkSchemaProperty = false;
+ }
+
+ if (!single) {
+ List<VirtualFile> files = ContainerUtil.newArrayList();
+ for (JsonSchemaFileProvider provider : providers) {
+ VirtualFile schemaFile = getSchemaForProvider(myProject, provider);
+ if (schemaFile != null) {
+ files.add(schemaFile);
+ }
+ }
+ if (!files.isEmpty()) {
+ return files;
+ }
+ }
+ else if (!providers.isEmpty()) {
+ final JsonSchemaFileProvider selected;
+ if (providers.size() > 2) return ContainerUtil.emptyList();
+ if (providers.size() > 1) {
+ final Optional<JsonSchemaFileProvider> userSchema =
+ providers.stream().filter(provider -> SchemaType.userSchema.equals(provider.getSchemaType())).findFirst();
+ if (!userSchema.isPresent()) return ContainerUtil.emptyList();
+ selected = userSchema.get();
+ } else selected = providers.get(0);
+ VirtualFile schemaFile = getSchemaForProvider(myProject, selected);
+ return ContainerUtil.createMaybeSingletonList(schemaFile);
+ }
+
+ if (onlyUserSchemas) {
+ return ContainerUtil.emptyList();
+ }
+
+ if (checkSchemaProperty) {
+ if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ }
+
+ return ContainerUtil.createMaybeSingletonList(resolveSchemaFromOtherSources(file));
+ }
+
+ @NotNull
+ public List<JsonSchemaFileProvider> getProvidersForFile(@NotNull VirtualFile file) {
+ return myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider)).collect(
+ Collectors.toList());
+ }
+
+ @Nullable
+ private VirtualFile resolveFromSchemaProperty(@Nullable String schemaUrl, @NotNull VirtualFile file) {
+ if (schemaUrl != null) {
+ VirtualFile virtualFile = findSchemaFileByReference(schemaUrl, file);
+ if (virtualFile != null) return virtualFile;
+ }
+ return null;
+ }
+
+ @Override
+ public List<JsonSchemaInfo> getAllUserVisibleSchemas() {
+ List<String> schemas = myCatalogManager.getAllCatalogSchemas();
+ Collection<? extends JsonSchemaFileProvider> providers = myState.getProviders();
+ List<JsonSchemaInfo> results = ContainerUtil.newArrayListWithCapacity(schemas.size() + providers.size());
+ Set<String> processedRemotes = ContainerUtil.newHashSet();
+ for (JsonSchemaFileProvider provider: providers) {
+ if (provider.isUserVisible()) {
+ if (provider.getRemoteSource() != null) {
+ if (processedRemotes.add(provider.getRemoteSource())) {
+ results.add(new JsonSchemaInfo(provider));
+ }
+ }
+ else {
+ results.add(new JsonSchemaInfo(provider));
+ }
+ }
+ }
+
+ for (String schema: schemas) {
+ if (processedRemotes.add(schema)) {
+ results.add(new JsonSchemaInfo(schema));
+ }
+ }
+ return results;
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaObject getSchemaObject(@NotNull final VirtualFile file) {
+ Collection<VirtualFile> schemas = getSchemasForFile(file, true, false);
+ if (schemas.size() == 0) return null;
+ assert schemas.size() == 1;
+ VirtualFile schemaFile = schemas.iterator().next();
+ return JsonCachedValues.getSchemaObject(replaceHttpFileWithBuiltinIfNeeded(schemaFile), myProject);
+ }
+
+ public VirtualFile replaceHttpFileWithBuiltinIfNeeded(VirtualFile schemaFile) {
+ // this hack is needed to handle user-defined mappings via urls
+ // we cannot perform that inside corresponding provider, because it leads to recursive component dependency
+ // this way we're preventing http files when a built-in schema exists
+ if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas()
+ && schemaFile instanceof HttpVirtualFile) {
+ String url = schemaFile.getUrl();
+ VirtualFile first1 = getLocalSchemaByUrl(url);
+ return first1 != null ? first1 : schemaFile;
+ }
+ return schemaFile;
+ }
+
+ @Nullable
+ public VirtualFile getLocalSchemaByUrl(String url) {
+ return myState.getFiles().stream()
+ .filter(f -> {
+ JsonSchemaFileProvider prov = getSchemaProvider(f);
+ return prov != null && !(prov.getSchemaFile() instanceof HttpVirtualFile)
+ && (url.equals(prov.getRemoteSource()) || JsonFileResolver.replaceUnsafeSchemaStoreUrls(url).equals(prov.getRemoteSource())
+ || url.equals(JsonFileResolver.replaceUnsafeSchemaStoreUrls(prov.getRemoteSource())));
+ }).findFirst().orElse(null);
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile) {
+ return JsonCachedValues.getSchemaObject(schemaFile, myProject);
+ }
+
+ @Override
+ public boolean isSchemaFile(@NotNull VirtualFile file) {
+ return JsonUtil.isJsonFile(file) && (isMappedSchema(file)
+ || isSchemaByProvider(file)
+ || hasSchemaSchema(file));
+ }
+
+ private boolean isMappedSchema(@NotNull VirtualFile file) {
+ return isMappedSchema(file, true);
+ }
+
+ public boolean isMappedSchema(@NotNull VirtualFile file, boolean canRecompute) {
+ return (canRecompute || myState.isComputed()) && myState.getFiles().contains(file);
+ }
+
+ private boolean isSchemaByProvider(@NotNull VirtualFile file) {
+ JsonSchemaFileProvider provider = myState.getProvider(file);
+ if (provider == null) {
+ for (JsonSchemaFileProvider stateProvider: myState.getProviders()) {
+ if (isSchemaProvider(stateProvider) && stateProvider.isAvailable(file))
+ return true;
+ }
+ return false;
+ }
+ return isSchemaProvider(provider);
+ }
+
+ private static boolean isSchemaProvider(JsonSchemaFileProvider provider) {
+ VirtualFile schemaFile = provider.getSchemaFile();
+ if (!(schemaFile instanceof HttpVirtualFile)) return false;
+ String url = schemaFile.getUrl();
+ return isSchemaUrl(url);
+ }
+
+ private static boolean isSchemaUrl(@Nullable String url) {
+ return url != null && url.startsWith("http://json-schema.org/") && (url.endsWith("/schema") || url.endsWith("/schema#"));
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file) {
+ if (isMappedSchema(file)) {
+ JsonSchemaFileProvider provider = myState.getProvider(file);
+ if (provider != null) {
+ return provider.getSchemaVersion();
+ }
+ }
+
+ return getSchemaVersionFromSchemaUrl(file);
+ }
+
+ @Nullable
+ private JsonSchemaVersion getSchemaVersionFromSchemaUrl(@NotNull VirtualFile file) {
+ Ref<String> res = Ref.create(null);
+ //noinspection CodeBlock2Expr
+ ApplicationManager.getApplication().runReadAction(() -> {
+ res.set(JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject));
+ });
+ if (res.isNull()) return null;
+ return JsonSchemaVersion.byId(res.get());
+ }
+
+ private boolean hasSchemaSchema(VirtualFile file) {
+ return getSchemaVersionFromSchemaUrl(file) != null;
+ }
+
+ private static boolean isProviderAvailable(@NotNull final VirtualFile file, @NotNull JsonSchemaFileProvider provider) {
+ return provider.isAvailable(file);
+ }
+
+ @Nullable
+ private VirtualFile resolveSchemaFromOtherSources(@NotNull VirtualFile file) {
+ return myCatalogManager.getSchemaFileForFile(file);
+ }
+
+ @Override
+ public void registerRemoteUpdateCallback(Runnable callback) {
+ myCatalogManager.registerCatalogUpdateCallback(callback);
+ }
+
+ @Override
+ public void unregisterRemoteUpdateCallback(Runnable callback) {
+ myCatalogManager.unregisterCatalogUpdateCallback(callback);
+ }
+
+ private final ConcurrentList<Runnable> myResetActions = ContainerUtil.createConcurrentList();
+
+ @Override
+ public void registerResetAction(Runnable action) {
+ myResetActions.add(action);
+ }
+
+ @Override
+ public void unregisterResetAction(Runnable action) {
+ myResetActions.remove(action);
+ }
+
+ @Override
+ public void registerReference(String ref) {
+ int index = StringUtil.lastIndexOfAny(ref, "\\/");
+ if (index >= 0) {
+ ref = ref.substring(index + 1);
+ }
+ myRefs.add(ref);
+ }
+
+ @Override
+ public boolean possiblyHasReference(String ref) {
+ return myRefs.contains(ref);
+ }
+
+ @Override
+ public void triggerUpdateRemote() {
+ myCatalogManager.triggerUpdateCatalog(myProject);
+ }
+
+ @Override
+ public boolean isApplicableToFile(@Nullable VirtualFile file) {
+ if (file == null) return false;
+ for (JsonSchemaEnabler e : JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensionList()) {
+ if (e.isEnabledForFile(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class MyState {
+ @NotNull private final Factory<List<JsonSchemaFileProvider>> myFactory;
+ @NotNull private final Project myProject;
+ @NotNull private final ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>> myData;
+ private final AtomicBoolean myIsComputed = new AtomicBoolean(false);
+
+ private MyState(@NotNull final Factory<List<JsonSchemaFileProvider>> factory, @NotNull Project project) {
+ myFactory = factory;
+ myProject = project;
+ myData = new ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>>() {
+ @NotNull
+ @Override
+ public MultiMap<VirtualFile, JsonSchemaFileProvider> compute() {
+ myIsComputed.set(true);
+ return createFileProviderMap(myFactory.create(), myProject);
+ }
+
+ @NotNull
+ @Override
+ public final synchronized MultiMap<VirtualFile, JsonSchemaFileProvider> getValue() {
+ return super.getValue();
+ }
+
+ @Override
+ public final synchronized void drop() {
+ myIsComputed.set(false);
+ super.drop();
+ }
+ };
+ }
+
+ public void reset() {
+ myData.drop();
+ }
+
+ @NotNull
+ public Collection<? extends JsonSchemaFileProvider> getProviders() {
+ return myData.getValue().values();
+ }
+
+ @NotNull
+ public Set<VirtualFile> getFiles() {
+ return myData.getValue().keySet();
+ }
+
+ @Nullable
+ public JsonSchemaFileProvider getProvider(@NotNull final VirtualFile file) {
+ final Collection<JsonSchemaFileProvider> providers = myData.getValue().get(file);
+ return providers.stream().filter(p -> p.getSchemaType() == SchemaType.userSchema).findFirst().orElse(providers.stream().findFirst().orElse(null));
+ }
+
+ public boolean isComputed() {
+ return myIsComputed.get();
+ }
+
+ @NotNull
+ private static MultiMap<VirtualFile, JsonSchemaFileProvider> createFileProviderMap(@NotNull final List<JsonSchemaFileProvider> list,
+ @NotNull Project project) {
+ // if there are different providers with the same schema files,
+ // stream API does not allow to collect same keys with Collectors.toMap(): throws duplicate key
+ final MultiMap<VirtualFile, JsonSchemaFileProvider> map = MultiMap.create();
+ for (JsonSchemaFileProvider provider : list) {
+ VirtualFile schemaFile = getSchemaForProvider(project, provider);
+ if (schemaFile != null) {
+ map.putValue(schemaFile, provider);
+ }
+ }
+ return map;
+ }
+ }
+
+ @Nullable
+ private static VirtualFile getSchemaForProvider(@NotNull Project project, @NotNull JsonSchemaFileProvider provider) {
+ if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isPreferRemoteSchemas()) {
+ final String source = provider.getRemoteSource();
+ if (source != null && !source.endsWith("!")) {
+ return VirtualFileManager.getInstance().findFileByUrl(source);
+ }
+ }
+ return provider.getSchemaFile();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java
new file mode 100644
index 00000000..76774c46
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.util.SmartList;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 4/20/2017.
+ */
+public class JsonSchemaTreeNode {
+ private boolean myAny;
+ private boolean myNothing;
+ private int myExcludingGroupNumber = -1;
+ @NotNull private SchemaResolveState myResolveState = SchemaResolveState.normal;
+
+ @Nullable private final JsonSchemaObject mySchema;
+ @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> mySteps = new SmartList<>();
+
+ @Nullable private final JsonSchemaTreeNode myParent;
+ @NotNull private final List<JsonSchemaTreeNode> myChildren = new ArrayList<>();
+
+ public JsonSchemaTreeNode(@Nullable JsonSchemaTreeNode parent,
+ @Nullable JsonSchemaObject schema) {
+ assert schema != null || parent != null;
+ myParent = parent;
+ mySchema = schema;
+ if (parent != null && !parent.getSteps().isEmpty()) {
+ mySteps.addAll(parent.getSteps().subList(1, parent.getSteps().size()));
+ }
+ }
+
+ public void anyChild() {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myAny = true;
+ myChildren.add(node);
+ }
+
+ public void nothingChild() {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myNothing = true;
+ myChildren.add(node);
+ }
+
+ public void createChildrenFromOperation(@NotNull JsonSchemaVariantsTreeBuilder.Operation operation) {
+ if (!SchemaResolveState.normal.equals(operation.myState)) {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myResolveState = operation.myState;
+ myChildren.add(node);
+ return;
+ }
+ if (!operation.myAnyOfGroup.isEmpty()) {
+ myChildren.addAll(convertToNodes(operation.myAnyOfGroup));
+ }
+ if (!operation.myOneOfGroup.isEmpty()) {
+ for (int i = 0; i < operation.myOneOfGroup.size(); i++) {
+ final List<JsonSchemaObject> group = operation.myOneOfGroup.get(i);
+ final List<JsonSchemaTreeNode> children = convertToNodes(group);
+ final int number = i;
+ children.forEach(c -> c.myExcludingGroupNumber = number);
+ myChildren.addAll(children);
+ }
+ }
+ }
+
+ private List<JsonSchemaTreeNode> convertToNodes(List<JsonSchemaObject> children) {
+ List<JsonSchemaTreeNode> nodes = ContainerUtil.newArrayListWithCapacity(children.size());
+ for (JsonSchemaObject child: children) {
+ nodes.add(new JsonSchemaTreeNode(this, child));
+ }
+ return nodes;
+ }
+
+ @NotNull
+ public SchemaResolveState getResolveState() {
+ return myResolveState;
+ }
+
+ public boolean isAny() {
+ return myAny;
+ }
+
+ public boolean isNothing() {
+ return myNothing;
+ }
+
+
+ public void setChild(@NotNull final JsonSchemaObject schema) {
+ myChildren.add(new JsonSchemaTreeNode(this, schema));
+ }
+
+ @Nullable
+ public JsonSchemaObject getSchema() {
+ return mySchema;
+ }
+
+ @NotNull
+ public List<JsonSchemaVariantsTreeBuilder.Step> getSteps() {
+ return mySteps;
+ }
+
+ @Nullable
+ public JsonSchemaTreeNode getParent() {
+ return myParent;
+ }
+
+ @NotNull
+ public List<JsonSchemaTreeNode> getChildren() {
+ return myChildren;
+ }
+
+ public int getExcludingGroupNumber() {
+ return myExcludingGroupNumber;
+ }
+
+ public void setSteps(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> steps) {
+ mySteps.clear();
+ mySteps.addAll(steps);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JsonSchemaTreeNode node = (JsonSchemaTreeNode)o;
+
+ if (myAny != node.myAny) return false;
+ if (myNothing != node.myNothing) return false;
+ if (myResolveState != node.myResolveState) return false;
+ if (mySchema != null ? !mySchema.equals(node.mySchema) : node.mySchema != null) return false;
+ //noinspection RedundantIfStatement
+ if (!mySteps.equals(node.mySteps)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (myAny ? 1 : 0);
+ result = 31 * result + (myNothing ? 1 : 0);
+ result = 31 * result + myResolveState.hashCode();
+ result = 31 * result + (mySchema != null ? mySchema.hashCode() : 0);
+ result = 31 * result + mySteps.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("NODE#" + hashCode() + "\n");
+ sb.append(mySteps.stream().map(Object::toString).collect(Collectors.joining("->", "steps: <", ">")));
+ sb.append("\n");
+ if (myExcludingGroupNumber >= 0) sb.append("in excluding group\n");
+ if (myAny) sb.append("any");
+ else if (myNothing) sb.append("nothing");
+ else if (!SchemaResolveState.normal.equals(myResolveState)) sb.append(myResolveState.name());
+ else {
+ assert mySchema != null;
+ final String name = mySchema.getSchemaFile().getName();
+ sb.append("schema from file: ").append(name).append("\n");
+ if (mySchema.getRef() != null) sb.append("$ref: ").append(mySchema.getRef()).append("\n");
+ else if (!mySchema.getProperties().isEmpty()) {
+ sb.append("properties: ");
+ sb.append(String.join(", ", mySchema.getProperties().keySet())).append("\n");
+ }
+ if (!myChildren.isEmpty()) {
+ sb.append("OR children of NODE#").append(hashCode()).append(":\n----------------\n")
+ .append(myChildren.stream().map(Object::toString).collect(Collectors.joining("\n")))
+ .append("\n=================\n");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java
new file mode 100644
index 00000000..98351d18
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java
@@ -0,0 +1,84 @@
+package com.jetbrains.jsonSchema.impl;
+
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 7/15/2015.
+ */
+public enum JsonSchemaType {
+ _string, _number, _integer, _object, _array, _boolean, _null, _any, _string_number;
+
+ public String getName() {
+ return name().substring(1);
+ }
+
+ public String getDefaultValue() {
+ switch (this) {
+ case _string:
+ return "\"\"";
+ case _number:
+ case _integer:
+ case _string_number:
+ return "0";
+ case _object:
+ return "{}";
+ case _array:
+ return "[]";
+ case _boolean:
+ return "false";
+ case _null:
+ return "null";
+ case _any:
+ default:
+ return "";
+ }
+ }
+
+ public boolean isSimple() {
+ switch (this) {
+ case _string:
+ case _number:
+ case _integer:
+ case _boolean:
+ case _null:
+ return true;
+ case _object:
+ case _array:
+ case _any:
+ default:
+ return false;
+ }
+ }
+
+ @Nullable
+ static JsonSchemaType getType(@NotNull final JsonValueAdapter value) {
+ if (value.isNull()) return _null;
+ if (value.isBooleanLiteral()) return _boolean;
+ if (value.isStringLiteral()) {
+ return value.isNumberLiteral() ? _string_number : _string;
+ }
+ if (value.isArray()) return _array;
+ if (value.isObject()) return _object;
+ if (value.isNumberLiteral()) {
+ return isInteger(value.getDelegate().getText()) ? _integer : _number;
+ }
+ return null;
+ }
+
+ public static boolean isInteger(@NotNull String text) {
+ try {
+ Integer.parseInt(text);
+ return true;
+ }
+ catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public String getDescription() {
+ if (this == _any) return "*";
+ return getName();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java
new file mode 100644
index 00000000..98ed82cd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsageTriggerCollector;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonSchemaUsageTriggerCollector extends ApplicationUsageTriggerCollector {
+ @NotNull
+ @Override
+ public String getGroupId() {
+ return "statistics.json.schema";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java
new file mode 100644
index 00000000..1e717be8
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonContainer;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.SmartList;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+
+/**
+ * @author Irina.Chernushina on 4/20/2017.
+ */
+public class JsonSchemaVariantsTreeBuilder {
+
+ public static JsonSchemaTreeNode buildTree(@NotNull final JsonSchemaObject schema,
+ @Nullable final List<Step> position,
+ final boolean skipLastExpand,
+ final boolean literalResolve,
+ final boolean acceptAdditional) {
+ final JsonSchemaTreeNode root = new JsonSchemaTreeNode(null, schema);
+ JsonSchemaService service = JsonSchemaService.Impl.get(schema.getJsonObject().getProject());
+ expandChildSchema(root, schema, service);
+ // set root's position since this children are just variants of root
+ for (JsonSchemaTreeNode treeNode : root.getChildren()) {
+ treeNode.setSteps(ContainerUtil.notNullize(position));
+ }
+
+ final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren());
+
+ while (!queue.isEmpty()) {
+ final JsonSchemaTreeNode node = queue.removeFirst();
+ if (node.isAny() || node.isNothing() || node.getSteps().isEmpty() || node.getSchema() == null) continue;
+ final Step step = node.getSteps().get(0);
+ if (!typeMatches(step.isFromObject(), node.getSchema())) {
+ node.nothingChild();
+ continue;
+ }
+ if (literalResolve) step.myLiteralResolve = true;
+ final Pair<ThreeState, JsonSchemaObject> pair = step.step(node.getSchema(), acceptAdditional);
+ if (ThreeState.NO.equals(pair.getFirst())) node.nothingChild();
+ else if (ThreeState.YES.equals(pair.getFirst())) node.anyChild();
+ else {
+ // process step results
+ assert pair.getSecond() != null;
+ if (node.getSteps().size() > 1 || !skipLastExpand) expandChildSchema(node, pair.getSecond(), service);
+ else node.setChild(pair.getSecond());
+ }
+
+ queue.addAll(node.getChildren());
+ }
+
+ return root;
+ }
+
+ private static boolean typeMatches(final boolean isObject, @NotNull final JsonSchemaObject schema) {
+ final JsonSchemaType requiredType = isObject ? JsonSchemaType._object : JsonSchemaType._array;
+ if (schema.getType() != null) {
+ return requiredType.equals(schema.getType());
+ }
+ if (schema.getTypeVariants() != null) {
+ for (JsonSchemaType schemaType : schema.getTypeVariants()) {
+ if (requiredType.equals(schemaType)) return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private static void expandChildSchema(@NotNull JsonSchemaTreeNode node, @NotNull JsonSchemaObject childSchema, @NotNull JsonSchemaService service) {
+ final JsonContainer element = childSchema.getJsonObject();
+ if (interestingSchema(childSchema)) {
+ final Operation operation =
+ CachedValuesManager.getManager(element.getProject())
+ .createParameterizedCachedValue((JsonSchemaObject param) -> {
+ final Operation expand = new ProcessDefinitionsOperation(param, service);
+ expand.doMap(new HashSet<>());
+ expand.doReduce();
+ return CachedValueProvider.Result.create(expand, element.getContainingFile(),
+ service.getAnySchemaChangeTracker());
+ }, false).getValue(childSchema);
+ node.createChildrenFromOperation(operation);
+ }
+ else {
+ node.setChild(childSchema);
+ }
+ }
+
+ public static List<Step> buildSteps(@NotNull String nameInSchema) {
+ final List<String> chain = split(normalizeSlashes(JsonSchemaService.normalizeId(nameInSchema)));
+ List<Step> steps = ContainerUtil.newArrayListWithCapacity(chain.size());
+ for (String s: chain) {
+ try {
+ steps.add(Step.createArrayElementStep(Integer.parseInt(s)));
+ }
+ catch (NumberFormatException e) {
+ steps.add(Step.createPropertyStep(unescapeJsonPointerPart(s)));
+ }
+ }
+ return steps;
+ }
+
+ static abstract class Operation {
+ @NotNull final List<JsonSchemaObject> myAnyOfGroup = new SmartList<>();
+ @NotNull final List<List<JsonSchemaObject>> myOneOfGroup = new SmartList<>();
+ @NotNull protected final List<Operation> myChildOperations;
+ @NotNull protected final JsonSchemaObject mySourceNode;
+ protected SchemaResolveState myState = SchemaResolveState.normal;
+
+ protected Operation(@NotNull JsonSchemaObject sourceNode) {
+ mySourceNode = sourceNode;
+ myChildOperations = new ArrayList<>();
+ }
+
+ protected abstract void map(@NotNull Set<JsonContainer> visited);
+ protected abstract void reduce();
+
+ public void doMap(@NotNull final Set<JsonContainer> visited) {
+ map(visited);
+ for (Operation operation : myChildOperations) {
+ operation.doMap(visited);
+ }
+ }
+
+ public void doReduce() {
+ if (!SchemaResolveState.normal.equals(myState)) {
+ myChildOperations.clear();
+ myAnyOfGroup.clear();
+ myOneOfGroup.clear();
+ return;
+ }
+
+ // lets do that to make the returned object smaller
+ myAnyOfGroup.forEach(Operation::clearVariants);
+ myOneOfGroup.forEach(list -> list.forEach(Operation::clearVariants));
+
+ for (Operation myChildOperation : myChildOperations) {
+ myChildOperation.doReduce();
+ }
+ reduce();
+ myChildOperations.clear();
+ }
+
+ private static void clearVariants(@NotNull JsonSchemaObject object) {
+ object.setAllOf(null);
+ object.setAnyOf(null);
+ object.setOneOf(null);
+ }
+
+ @Nullable
+ protected Operation createExpandOperation(@NotNull final JsonSchemaObject schema,
+ @NotNull JsonSchemaService service) {
+ if (conflictingSchema(schema)) {
+ final Operation operation = new AnyOfOperation(schema, service);
+ operation.myState = SchemaResolveState.conflict;
+ return operation;
+ }
+ if (schema.getAnyOf() != null) return new AnyOfOperation(schema, service);
+ if (schema.getOneOf() != null) return new OneOfOperation(schema, service);
+ if (schema.getAllOf() != null) return new AllOfOperation(schema, service);
+ return null;
+ }
+
+ protected static List<JsonSchemaObject> mergeOneOf(Operation op) {
+ return op.myOneOfGroup.stream().flatMap(List::stream).collect(Collectors.toList());
+ }
+ }
+
+ // even if there are no definitions to expand, this object may work as an intermediate node in a tree,
+ // connecting oneOf and allOf expansion, for example
+ private static class ProcessDefinitionsOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected ProcessDefinitionsOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ JsonSchemaObject current = mySourceNode;
+ while (!StringUtil.isEmptyOrSpaces(current.getRef())) {
+ final JsonSchemaObject definition = current.resolveRefSchema(myService);
+ if (definition == null) {
+ myState = SchemaResolveState.brokenDefinition;
+ return;
+ }
+ // this definition was already expanded; do not cycle
+ if (!visited.add(definition.getJsonObject())) break;
+ current = merge(current, definition, current);
+ }
+ final Operation expandOperation = createExpandOperation(current, myService);
+ if (expandOperation != null) myChildOperations.add(expandOperation);
+ else myAnyOfGroup.add(current);
+ }
+
+ @Override
+ public void reduce() {
+ if (!myChildOperations.isEmpty()) {
+ assert myChildOperations.size() == 1;
+ final Operation operation = myChildOperations.get(0);
+ myAnyOfGroup.addAll(operation.myAnyOfGroup);
+ myOneOfGroup.addAll(operation.myOneOfGroup);
+ }
+ }
+ }
+
+ private static class AllOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected AllOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> allOf = mySourceNode.getAllOf();
+ assert allOf != null;
+ myChildOperations.addAll(ContainerUtil.map(allOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ private static <T> int maxSize(List<List<T>> items) {
+ if (items.size() == 0) return 0;
+ int maxsize = -1;
+ for (List<T> item: items) {
+ int size = item.size();
+ if (maxsize < size) maxsize = size;
+ }
+ return maxsize;
+ }
+
+ @Override
+ public void reduce() {
+ myAnyOfGroup.add(mySourceNode);
+
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+
+ final List<JsonSchemaObject> mergedAny = andGroups(op.myAnyOfGroup, myAnyOfGroup);
+
+ final List<List<JsonSchemaObject>> mergedExclusive =
+ ContainerUtil.newArrayListWithCapacity(
+ op.myAnyOfGroup.size() * maxSize(myOneOfGroup) +
+ myAnyOfGroup.size() * maxSize(op.myOneOfGroup) +
+ maxSize(myOneOfGroup) * maxSize(op.myOneOfGroup)
+ );
+
+ for (List<JsonSchemaObject> objects : myOneOfGroup) {
+ mergedExclusive.add(andGroups(op.myAnyOfGroup, objects));
+ }
+ for (List<JsonSchemaObject> objects : op.myOneOfGroup) {
+ mergedExclusive.add(andGroups(objects, myAnyOfGroup));
+ }
+ for (List<JsonSchemaObject> group : op.myOneOfGroup) {
+ for (List<JsonSchemaObject> otherGroup : myOneOfGroup) {
+ mergedExclusive.add(andGroups(group, otherGroup));
+ }
+ }
+
+ myAnyOfGroup.clear();
+ myOneOfGroup.clear();
+ myAnyOfGroup.addAll(mergedAny);
+ myOneOfGroup.addAll(mergedExclusive);
+ }
+ }
+ }
+
+ private static List<JsonSchemaObject> andGroups(@NotNull List<JsonSchemaObject> g1,
+ @NotNull List<JsonSchemaObject> g2) {
+ List<JsonSchemaObject> result = ContainerUtil.newArrayListWithCapacity(g1.size() * g2.size());
+ for (JsonSchemaObject s: g1) {
+ result.addAll(andGroup(s, g2));
+ }
+ return result;
+ }
+
+ // here is important, which pointer gets the result: lets make them all different, otherwise two schemas of branches of oneOf would be equal
+ private static List<JsonSchemaObject> andGroup(@NotNull JsonSchemaObject object, @NotNull List<JsonSchemaObject> group) {
+ List<JsonSchemaObject> list = ContainerUtil.newArrayListWithCapacity(group.size());
+ for (JsonSchemaObject s: group) {
+ JsonSchemaObject schemaObject = merge(object, s, s);
+ if (schemaObject.isValidByExclusion()) {
+ list.add(schemaObject);
+ }
+ }
+ return list;
+ }
+
+ private static class OneOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected OneOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> oneOf = mySourceNode.getOneOf();
+ assert oneOf != null;
+ myChildOperations.addAll(ContainerUtil.map(oneOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ @Override
+ public void reduce() {
+ final List<JsonSchemaObject> oneOf = new SmartList<>();
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+ oneOf.addAll(andGroup(mySourceNode, op.myAnyOfGroup));
+ oneOf.addAll(andGroup(mySourceNode, mergeOneOf(op)));
+ }
+ // here it is not a mistake - all children of this node come to oneOf group
+ myOneOfGroup.add(oneOf);
+ }
+ }
+
+ private static class AnyOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected AnyOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> anyOf = mySourceNode.getAnyOf();
+ assert anyOf != null;
+ myChildOperations.addAll(ContainerUtil.map(anyOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ @Override
+ public void reduce() {
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+
+ myAnyOfGroup.addAll(andGroup(mySourceNode, op.myAnyOfGroup));
+ for (List<JsonSchemaObject> group : op.myOneOfGroup) {
+ myOneOfGroup.add(andGroup(mySourceNode, group));
+ }
+ }
+ }
+ }
+
+ @NotNull
+ public static JsonSchemaObject merge(@NotNull JsonSchemaObject base,
+ @NotNull JsonSchemaObject other,
+ @NotNull JsonSchemaObject pointTo) {
+ final JsonSchemaObject object = new JsonSchemaObject(pointTo.getJsonObject());
+ object.mergeValues(other);
+ object.mergeValues(base);
+ object.setRef(other.getRef());
+ return object;
+ }
+
+ private static boolean conflictingSchema(JsonSchemaObject schema) {
+ int cnt = 0;
+ if (schema.getAllOf() != null) ++cnt;
+ if (schema.getAnyOf() != null) ++cnt;
+ if (schema.getOneOf() != null) ++cnt;
+ return cnt > 1;
+ }
+
+ private static boolean interestingSchema(@NotNull JsonSchemaObject schema) {
+ return schema.getAnyOf() != null || schema.getOneOf() != null || schema.getAllOf() != null || schema.getRef() != null
+ || schema.getIf() != null;
+ }
+
+ public static class Step {
+ @Nullable private final String myName;
+ private final int myIdx;
+ private boolean myLiteralResolve;
+
+ private Step(@Nullable String name, int idx) {
+ myName = name;
+ myIdx = idx;
+ }
+
+ public static Step createPropertyStep(@NotNull final String name) {
+ return new Step(name, -1);
+ }
+
+ public static Step createArrayElementStep(final int idx) {
+ assert idx >= 0;
+ return new Step(null, idx);
+ }
+
+ public boolean isFromObject() {
+ return myName != null;
+ }
+
+ public boolean isFromArray() {
+ return myName == null;
+ }
+
+ @Nullable
+ public String getName() {
+ return myName;
+ }
+
+ public int getIdx() {
+ return myIdx;
+ }
+
+ @NotNull
+ public Pair<ThreeState, JsonSchemaObject> step(@NotNull JsonSchemaObject parent, boolean acceptAdditionalPropertiesSchemas) {
+ if (myName != null) {
+ return propertyStep(parent, acceptAdditionalPropertiesSchemas);
+ } else {
+ assert myIdx >= 0;
+ return arrayOrNumericPropertyElementStep(parent, acceptAdditionalPropertiesSchemas);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String format = "?%s";
+ if (myName != null) format = "{%s}";
+ if (myIdx >= 0) format = "[%s]";
+ return String.format(format, myName != null ? myName : (myIdx >= 0 ? String.valueOf(myIdx) : "null"));
+ }
+
+ @NotNull
+ private Pair<ThreeState, JsonSchemaObject> propertyStep(@NotNull JsonSchemaObject parent,
+ boolean acceptAdditionalPropertiesSchemas) {
+ assert myName != null;
+ if (JsonSchemaObject.DEFINITIONS.equals(myName) &&
+ parent.getDefinitionsMap() != null && (!isInMainSchema(parent) || myLiteralResolve)) {
+ // definitions pointer here is fictive so lets find any
+ final Map<String, JsonSchemaObject> definitionsMap = parent.getDefinitionsMap();
+ final JsonObject anyDefinitions = definitionsMap.values().stream()
+ .filter(def -> {
+ final JsonProperty parentObj = ObjectUtils.tryCast(def.getJsonObject().getParent(), JsonProperty.class);
+ return parentObj != null && parentObj.isValid() && parentObj.getValue() instanceof JsonObject;
+ })
+ .map(def -> (JsonObject)((JsonProperty) def.getJsonObject().getParent()).getValue())
+ .findFirst().orElse(null);
+ if (anyDefinitions == null) return Pair.create(ThreeState.NO, null);
+ final JsonSchemaObject object = new JsonSchemaObject(anyDefinitions);
+ object.setProperties(definitionsMap);
+ return Pair.create(ThreeState.UNSURE, object);
+ }
+ final JsonSchemaObject child = parent.getProperties().get(myName);
+ if (child != null) {
+ return Pair.create(ThreeState.UNSURE, child);
+ }
+ final JsonSchemaObject schema = parent.getMatchingPatternPropertySchema(myName);
+ if (schema != null) {
+ return Pair.create(ThreeState.UNSURE, schema);
+ }
+ if (acceptAdditionalPropertiesSchemas) {
+ if (parent.getAdditionalPropertiesSchema() != null) {
+ return Pair.create(ThreeState.UNSURE, parent.getAdditionalPropertiesSchema());
+ }
+
+ // resolve inside V7 if-then-else conditionals
+ if (parent.getIf() != null) {
+ JsonSchemaObject childObject;
+
+ // NOTE: do not resolve inside 'if' itself - it is just a condition, but not an actual validation!
+ // only 'then' and 'else' branches provide actual validation sources, but not the 'if' branch
+
+ if (parent.getThen() != null) {
+ childObject = parent.getThen().getProperties().get(myName);
+ if (childObject != null) {
+ return Pair.create(ThreeState.UNSURE, childObject);
+ }
+ }
+ if (parent.getElse() != null) {
+ childObject = parent.getElse().getProperties().get(myName);
+ if (childObject != null) {
+ return Pair.create(ThreeState.UNSURE, childObject);
+ }
+ }
+ }
+ }
+ if (Boolean.FALSE.equals(parent.getAdditionalPropertiesAllowed())) {
+ return Pair.create(ThreeState.NO, null);
+ }
+ // by default, additional properties are allowed
+ return Pair.create(ThreeState.YES, null);
+ }
+
+ private static boolean isInMainSchema(@NotNull JsonSchemaObject parent) {
+ final VirtualFile schemaFile = parent.getSchemaFile();
+ final JsonSchemaService service = JsonSchemaService.Impl.get(parent.getJsonObject().getProject());
+ if (!service.isApplicableToFile(schemaFile) || !service.isSchemaFile(schemaFile)) return false;
+
+ final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (rootSchema == null) return false;
+
+ return JsonSchemaVersion.isSchemaSchemaId(rootSchema.getId());
+ }
+
+ @NotNull
+ private Pair<ThreeState, JsonSchemaObject> arrayOrNumericPropertyElementStep(@NotNull JsonSchemaObject parent,
+ boolean acceptAdditionalPropertiesSchemas) {
+ if (parent.getItemsSchema() != null) {
+ return Pair.create(ThreeState.UNSURE, parent.getItemsSchema());
+ }
+ if (parent.getItemsSchemaList() != null) {
+ final List<JsonSchemaObject> list = parent.getItemsSchemaList();
+ if (myIdx >= 0 && myIdx < list.size()) {
+ return Pair.create(ThreeState.UNSURE, list.get(myIdx));
+ }
+ }
+ final String keyAsString = String.valueOf(myIdx);
+ if (parent.getProperties().containsKey(keyAsString)) {
+ return Pair.create(ThreeState.UNSURE, parent.getProperties().get(keyAsString));
+ }
+ final JsonSchemaObject matchingPatternPropertySchema = parent.getMatchingPatternPropertySchema(keyAsString);
+ if (matchingPatternPropertySchema != null) {
+ return Pair.create(ThreeState.UNSURE, matchingPatternPropertySchema);
+ }
+ if (parent.getAdditionalItemsSchema() != null && acceptAdditionalPropertiesSchemas) {
+ return Pair.create(ThreeState.UNSURE, parent.getAdditionalItemsSchema());
+ }
+ if (Boolean.FALSE.equals(parent.getAdditionalItemsAllowed())) {
+ return Pair.create(ThreeState.NO, null);
+ }
+ return Pair.create(ThreeState.YES, null);
+ }
+ }
+
+ public static class SchemaUrlSplitter {
+ @Nullable
+ private final String mySchemaId;
+ @NotNull
+ private final String myRelativePath;
+
+ public SchemaUrlSplitter(@NotNull final String ref) {
+ if (isSelfReference(ref)) {
+ mySchemaId = null;
+ myRelativePath = "";
+ return;
+ }
+ if (!ref.startsWith("#/")) {
+ int idx = ref.indexOf("#/");
+ if (idx == -1) {
+ mySchemaId = ref.endsWith("#") ? ref.substring(0, ref.length() - 1) : ref;
+ myRelativePath = "";
+ } else {
+ mySchemaId = ref.substring(0, idx);
+ myRelativePath = ref.substring(idx);
+ }
+ } else {
+ mySchemaId = null;
+ myRelativePath = ref;
+ }
+ }
+
+ public boolean isAbsolute() {
+ return mySchemaId != null;
+ }
+
+ @Nullable
+ public String getSchemaId() {
+ return mySchemaId;
+ }
+
+ @NotNull
+ public String getRelativePath() {
+ return myRelativePath;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java
new file mode 100644
index 00000000..5b0f5507
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java
@@ -0,0 +1,60 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.util.text.StringUtil;
+import kotlin.NotImplementedError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public enum JsonSchemaVersion {
+ SCHEMA_4,
+ SCHEMA_6,
+ SCHEMA_7;
+
+ private static final String ourSchemaV4Schema = "http://json-schema.org/draft-04/schema";
+ private static final String ourSchemaV6Schema = "http://json-schema.org/draft-06/schema";
+ private static final String ourSchemaV7Schema = "http://json-schema.org/draft-07/schema";
+ private static final String ourSchemaVLatestSchema = "http://json-schema.org/schema";
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SCHEMA_4:
+ return "JSON schema version 4";
+ case SCHEMA_6:
+ return "JSON schema version 6";
+ case SCHEMA_7:
+ return "JSON schema version 7";
+ }
+
+ throw new NotImplementedError("Unknown version: " + this);
+ }
+
+
+ @Nullable
+ public static JsonSchemaVersion byId(@NotNull String id) {
+ switch (StringUtil.trimEnd(id, '#')) {
+ case ourSchemaV4Schema:
+ return SCHEMA_4;
+ case ourSchemaV6Schema:
+ return SCHEMA_6;
+ case ourSchemaV7Schema:
+ case ourSchemaVLatestSchema:
+ return SCHEMA_7;
+ }
+
+ return null;
+ }
+
+ public static boolean isSchemaSchemaId(@Nullable String id) {
+ if (id == null) return false;
+ switch (StringUtil.trimEnd(id, '#')) {
+ case ourSchemaV4Schema:
+ case ourSchemaV6Schema:
+ case ourSchemaV7Schema:
+ case ourSchemaVLatestSchema:
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java
new file mode 100644
index 00000000..fa13577c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java
@@ -0,0 +1,163 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.fixes.AddMissingPropertyFix;
+import com.jetbrains.jsonSchema.impl.fixes.RemoveProhibitedPropertyFix;
+import com.jetbrains.jsonSchema.impl.fixes.SuggestEnumValuesFix;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+
+public class JsonValidationError {
+
+ public IssueData getIssueData() {
+ return myIssueData;
+ }
+
+ public JsonErrorPriority getPriority() {
+ return myPriority;
+ }
+
+ public enum FixableIssueKind {
+ MissingProperty,
+ MissingOneOfProperty,
+ MissingAnyOfProperty,
+ ProhibitedProperty,
+ NonEnumValue,
+ ProhibitedType,
+ TypeMismatch,
+ None
+ }
+
+ public interface IssueData {
+
+ }
+
+ public static class MissingOneOfPropsIssueData implements IssueData {
+ public final Collection<MissingMultiplePropsIssueData> myExclusiveOptions;
+
+ public MissingOneOfPropsIssueData(Collection<MissingMultiplePropsIssueData> options) {
+ myExclusiveOptions = options;
+ }
+ }
+
+ public static class MissingMultiplePropsIssueData implements IssueData {
+ public final Collection<MissingPropertyIssueData> myMissingPropertyIssues;
+
+ public MissingMultiplePropsIssueData(Collection<MissingPropertyIssueData> missingPropertyIssues) {
+ myMissingPropertyIssues = missingPropertyIssues;
+ }
+
+ private static String getPropertyNameWithComment(MissingPropertyIssueData prop) {
+ String comment = "";
+ if (prop.enumItemsCount == 1) {
+ comment = " = " + prop.defaultValue.toString();
+ }
+ return "'" + prop.propertyName + "'" + comment;
+ }
+
+ public String getMessage(boolean trimIfNeeded) {
+ if (myMissingPropertyIssues.size() == 1) {
+ MissingPropertyIssueData prop = myMissingPropertyIssues.iterator().next();
+ return "property " + getPropertyNameWithComment(prop);
+ }
+
+ Collection<MissingPropertyIssueData> namesToDisplay = myMissingPropertyIssues;
+ boolean trimmed = false;
+ if (trimIfNeeded && namesToDisplay.size() > 3) {
+ namesToDisplay = ContainerUtil.newArrayList();
+ Iterator<MissingPropertyIssueData> iterator = myMissingPropertyIssues.iterator();
+ for (int i = 0; i < 3; i++) {
+ namesToDisplay.add(iterator.next());
+ }
+ trimmed = true;
+ }
+ String allNames = myMissingPropertyIssues.stream().map(
+ MissingMultiplePropsIssueData::getPropertyNameWithComment).sorted((s1, s2) -> {
+ boolean firstHasEq = s1.contains("=");
+ boolean secondHasEq = s2.contains("=");
+ if (firstHasEq == secondHasEq) {
+ return s1.compareTo(s2);
+ }
+ return firstHasEq ? -1 : 1;
+ }).collect(Collectors.joining(", "));
+ if (trimmed) allNames += ", ...";
+ return "properties " + allNames;
+ }
+ }
+
+ public static class MissingPropertyIssueData implements IssueData {
+ public final String propertyName;
+ public final JsonSchemaType propertyType;
+ public final Object defaultValue;
+ public final int enumItemsCount;
+
+ public MissingPropertyIssueData(String propertyName, JsonSchemaType propertyType, Object defaultValue, int enumItemsCount) {
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ this.defaultValue = defaultValue;
+ this.enumItemsCount = enumItemsCount;
+ }
+ }
+
+ public static class ProhibitedPropertyIssueData implements IssueData {
+ public final String propertyName;
+
+ public ProhibitedPropertyIssueData(String propertyName) {
+ this.propertyName = propertyName;
+ }
+ }
+
+ public static class TypeMismatchIssueData implements IssueData {
+ public final JsonSchemaType[] expectedTypes;
+
+ public TypeMismatchIssueData(JsonSchemaType[] expectedTypes) {
+ this.expectedTypes = expectedTypes;
+ }
+ }
+
+ private final String myMessage;
+ private final FixableIssueKind myFixableIssueKind;
+ private final IssueData myIssueData;
+ private final JsonErrorPriority myPriority;
+
+ public JsonValidationError(String message, FixableIssueKind fixableIssueKind, IssueData issueData,
+ JsonErrorPriority priority) {
+ myMessage = message;
+ myFixableIssueKind = fixableIssueKind;
+ myIssueData = issueData;
+ myPriority = priority;
+ }
+
+ public String getMessage() {
+ return myMessage;
+ }
+
+ public FixableIssueKind getFixableIssueKind() {
+ return myFixableIssueKind;
+ }
+
+ @NotNull
+ public LocalQuickFix[] createFixes(@Nullable JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ if (quickFixAdapter == null) return LocalQuickFix.EMPTY_ARRAY;
+ switch (myFixableIssueKind) {
+ case MissingProperty:
+ return new AddMissingPropertyFix[]{new AddMissingPropertyFix((MissingMultiplePropsIssueData)myIssueData, quickFixAdapter)};
+ case MissingOneOfProperty:
+ case MissingAnyOfProperty:
+ return ((MissingOneOfPropsIssueData)myIssueData).myExclusiveOptions.stream().map(d -> new AddMissingPropertyFix(d, quickFixAdapter)).toArray(LocalQuickFix[]::new);
+ case ProhibitedProperty:
+ return new RemoveProhibitedPropertyFix[]{new RemoveProhibitedPropertyFix((ProhibitedPropertyIssueData)myIssueData, quickFixAdapter)};
+ case NonEnumValue:
+ return new SuggestEnumValuesFix[]{new SuggestEnumValuesFix(quickFixAdapter)};
+ default:
+ return LocalQuickFix.EMPTY_ARRAY;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java
new file mode 100644
index 00000000..910313e7
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.util.Processor;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * @author Irina.Chernushina on 4/22/2017.
+ */
+public class MatchResult {
+ public final List<JsonSchemaObject> mySchemas;
+ public final List<Collection<? extends JsonSchemaObject>> myExcludingSchemas;
+
+ private MatchResult(@NotNull final List<JsonSchemaObject> schemas, @NotNull final List<Collection<? extends JsonSchemaObject>> excludingSchemas) {
+ mySchemas = Collections.unmodifiableList(schemas);
+ myExcludingSchemas = Collections.unmodifiableList(excludingSchemas);
+ }
+
+ public static MatchResult create(@NotNull JsonSchemaTreeNode root) {
+ List<JsonSchemaObject> schemas = new ArrayList<>();
+ MultiMap<Integer, JsonSchemaObject> oneOfGroups = MultiMap.create();
+ iterateTree(root, node -> {
+ if (node.isAny()) return true;
+ int groupNumber = node.getExcludingGroupNumber();
+ if (groupNumber < 0) {
+ schemas.add(node.getSchema());
+ }
+ else {
+ oneOfGroups.putValue(groupNumber, node.getSchema());
+ }
+ return true;
+ });
+ List<Collection<? extends JsonSchemaObject>> result = oneOfGroups.isEmpty()
+ ? ContainerUtil.emptyList()
+ : ContainerUtil.newArrayListWithCapacity(oneOfGroups.keySet().size());
+ for (Map.Entry<Integer, Collection<JsonSchemaObject>> entry: oneOfGroups.entrySet()) {
+ result.add(entry.getValue());
+ }
+ return new MatchResult(schemas, result);
+ }
+
+ public static void iterateTree(@NotNull JsonSchemaTreeNode root,
+ @NotNull final Processor<? super JsonSchemaTreeNode> processor) {
+ final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren());
+ while (!queue.isEmpty()) {
+ final JsonSchemaTreeNode node = queue.removeFirst();
+ if (node.getChildren().isEmpty()) {
+ if (!node.isNothing() && SchemaResolveState.normal.equals(node.getResolveState()) && !processor.process(node)) {
+ break;
+ }
+ } else {
+ queue.addAll(node.getChildren());
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java
new file mode 100644
index 00000000..2c83bcb2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+/**
+ * @author Irina.Chernushina on 4/24/2017.
+ */
+enum SchemaResolveState {
+ normal, conflict, brokenDefinition, cyclicDefinition
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java
new file mode 100644
index 00000000..5ad2927a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl.adapters;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonArrayAdapter implements JsonArrayValueAdapter {
+ @NotNull private final JsonArray myArray;
+
+ public JsonJsonArrayAdapter(@NotNull JsonArray array) {myArray = array;}
+
+ @Override
+ public boolean isObject() {
+ return false;
+ }
+
+ @Override
+ public boolean isArray() {
+ return true;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myArray;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return this;
+ }
+
+ @NotNull
+ @Override
+ public List<JsonValueAdapter> getElements() {
+ return myArray.getValueList().stream().filter(e -> e != null).map(e -> JsonJsonPropertyAdapter.createAdapterByType(e)).collect(
+ Collectors.toList());
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java
new file mode 100644
index 00000000..fe84e7eb
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl.adapters;
+
+import com.intellij.json.psi.*;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonGenericValueAdapter implements JsonValueAdapter {
+ @NotNull private final JsonValue myValue;
+
+ public JsonJsonGenericValueAdapter(@NotNull JsonValue value) {myValue = value;}
+
+ @Override
+ public boolean isObject() {
+ return false;
+ }
+
+ @Override
+ public boolean isArray() {
+ return false;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return myValue instanceof JsonStringLiteral;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return myValue instanceof JsonNumberLiteral;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return myValue instanceof JsonBooleanLiteral;
+ }
+
+ @Override
+ public boolean isNull() {
+ return myValue instanceof JsonNullLiteral;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myValue;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java
new file mode 100644
index 00000000..db19cd1c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl.adapters;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonObjectAdapter implements JsonObjectValueAdapter {
+ @NotNull private final JsonObject myValue;
+
+ public JsonJsonObjectAdapter(@NotNull JsonObject value) {myValue = value;}
+
+ @Override
+ public boolean isObject() {
+ return true;
+ }
+
+ @Override
+ public boolean isArray() {
+ return false;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myValue;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return this;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public List<JsonPropertyAdapter> getPropertyList() {
+ return myValue.getPropertyList().stream().filter(p -> p != null)
+ .map(p -> new JsonJsonPropertyAdapter(p)).collect(Collectors.toList());
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java
new file mode 100644
index 00000000..f3871c35
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl.adapters;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonPropertyAdapter implements JsonPropertyAdapter {
+ @NotNull private final JsonProperty myProperty;
+
+ public JsonJsonPropertyAdapter(@NotNull JsonProperty property) {
+ myProperty = property;
+ }
+
+ @Nullable
+ @Override
+ public String getName() {
+ return myProperty.getName();
+ }
+
+ @NotNull
+ @Override
+ public Collection<JsonValueAdapter> getValues() {
+ return myProperty.getValue() == null ? ContainerUtil.emptyList() : Collections.singletonList(createAdapterByType(myProperty.getValue()));
+ }
+
+ @Nullable
+ @Override
+ public JsonValueAdapter getNameValueAdapter() {
+ return createAdapterByType(myProperty.getNameElement());
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myProperty;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getParentObject() {
+ return myProperty.getParent() instanceof JsonObject ? new JsonJsonObjectAdapter((JsonObject)myProperty.getParent()) : null;
+ }
+
+ @NotNull
+ public static JsonValueAdapter createAdapterByType(@NotNull JsonValue value) {
+ if (value instanceof JsonObject) return new JsonJsonObjectAdapter((JsonObject)value);
+ if (value instanceof JsonArray) return new JsonJsonArrayAdapter((JsonArray)value);
+ return new JsonJsonGenericValueAdapter(value);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java
new file mode 100644
index 00000000..8dbcd601
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java
@@ -0,0 +1,156 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInsight.template.Template;
+import com.intellij.codeInsight.template.TemplateBuilderImpl;
+import com.intellij.codeInsight.template.TemplateManager;
+import com.intellij.codeInsight.template.impl.ConstantNode;
+import com.intellij.codeInsight.template.impl.EmptyNode;
+import com.intellij.codeInsight.template.impl.MacroCallNode;
+import com.intellij.codeInsight.template.macro.CompleteMacro;
+import com.intellij.codeInspection.*;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.ex.util.EditorUtil;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.DocumentUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaType;
+import com.jetbrains.jsonSchema.impl.JsonValidationError;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AddMissingPropertyFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> {
+ private final JsonValidationError.MissingMultiplePropsIssueData myData;
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public AddMissingPropertyFix(JsonValidationError.MissingMultiplePropsIssueData data,
+ JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myData = data;
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Add missing properties";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return "Add missing " + myData.getMessage(true);
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement element = descriptor.getPsiElement();
+ Ref<Boolean> hadComma = Ref.create(false);
+ PsiElement newElement = performFix(element, hadComma);
+ // if we have more than one property, don't expand templates and don't move the caret
+ if (newElement == null) return;
+
+ PsiElement value = myQuickFixAdapter.getPropertyValue(newElement);
+ FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile());
+ EditorEx editor = EditorUtil.getEditorEx(fileEditor);
+ assert editor != null;
+ if (value == null) {
+ WriteAction.run(() -> editor.getCaretModel().moveToOffset(newElement.getTextRange().getEndOffset()));
+ return;
+ }
+ TemplateManager templateManager = TemplateManager.getInstance(project);
+ TemplateBuilderImpl builder = new TemplateBuilderImpl(newElement);
+ String text = value.getText();
+ boolean isEmptyArray = StringUtil.equalsIgnoreWhitespaces(text, "[]");
+ boolean isEmptyObject = StringUtil.equalsIgnoreWhitespaces(text, "{}");
+ boolean goInside = isEmptyArray || isEmptyObject || StringUtil.isQuotedString(text);
+ TextRange range = goInside ? TextRange.create(1, text.length() - 1) : TextRange.create(0, text.length());
+ builder.replaceElement(value, range, myData.myMissingPropertyIssues.iterator().next().enumItemsCount > 1 || isEmptyObject
+ ? new MacroCallNode(new CompleteMacro())
+ : isEmptyArray ? new EmptyNode() : new ConstantNode(goInside ? StringUtil.unquoteString(text) : text));
+ editor.getCaretModel().moveToOffset(newElement.getTextRange().getStartOffset());
+ builder.setEndVariableAfter(newElement);
+ WriteAction.run(() -> {
+ Template template = builder.buildInlineTemplate();
+ template.setToReformat(true);
+ templateManager.startTemplate(editor, template);
+ });
+ }
+
+ private PsiElement performFix(PsiElement element, Ref<Boolean> hadComma) {
+ Ref<PsiElement> newElementRef = Ref.create(null);
+
+ WriteAction.run(() -> {
+ boolean isSingle = myData.myMissingPropertyIssues.size() == 1;
+ for (JsonValidationError.MissingPropertyIssueData issue: myData.myMissingPropertyIssues) {
+ Object defaultValueObject = issue.defaultValue;
+ String defaultValue = defaultValueObject instanceof String ? StringUtil.wrapWithDoubleQuote(defaultValueObject.toString()) : null;
+ PsiElement newElement = element
+ .addBefore(
+ myQuickFixAdapter.createProperty(issue.propertyName, defaultValue == null ? getDefaultValueFromType(issue) : defaultValue),
+ element.getLastChild());
+ PsiElement backward = PsiTreeUtil.skipWhitespacesBackward(newElement);
+ hadComma.set(myQuickFixAdapter.ensureComma(backward, element, newElement));
+ if (isSingle) {
+ newElementRef.set(newElement);
+ }
+ }
+ });
+
+ return newElementRef.get();
+ }
+
+ @NotNull
+ private static String getDefaultValueFromType(JsonValidationError.MissingPropertyIssueData issue) {
+ JsonSchemaType propertyType = issue.propertyType;
+ return propertyType == null ? "" : propertyType.getDefaultValue();
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project,
+ @NotNull CommonProblemDescriptor[] descriptors,
+ @NotNull List<PsiElement> psiElementsToIgnore,
+ @Nullable Runnable refreshViews) {
+ List<Pair<AddMissingPropertyFix, PsiElement>> propFixes = ContainerUtil.newArrayList();
+ for (CommonProblemDescriptor descriptor: descriptors) {
+ if (!(descriptor instanceof ProblemDescriptor)) continue;
+ QuickFix[] fixes = descriptor.getFixes();
+ if (fixes == null) continue;
+ AddMissingPropertyFix fix = getWorkingQuickFix(fixes);
+ if (fix == null) continue;
+ propFixes.add(Pair.create(fix, ((ProblemDescriptor)descriptor).getPsiElement()));
+ }
+
+ DocumentUtil.writeInRunUndoTransparentAction(() -> propFixes.forEach(fix ->
+ fix.first.performFix(fix.second, Ref.create(false))));
+ }
+
+ @Nullable
+ private static AddMissingPropertyFix getWorkingQuickFix(@NotNull QuickFix[] fixes) {
+ for (QuickFix fix : fixes) {
+ if (fix instanceof AddMissingPropertyFix) {
+ return (AddMissingPropertyFix)fix;
+ }
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java
new file mode 100644
index 00000000..fb211e4c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java
@@ -0,0 +1,46 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonValidationError;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+public class RemoveProhibitedPropertyFix implements LocalQuickFix {
+ private final JsonValidationError.ProhibitedPropertyIssueData myData;
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public RemoveProhibitedPropertyFix(JsonValidationError.ProhibitedPropertyIssueData data,
+ JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myData = data;
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Remove prohibited property";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return getFamilyName() + " '" + myData.propertyName + "'";
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement element = descriptor.getPsiElement();
+ assert myData.propertyName.equals(myQuickFixAdapter.getPropertyName(element));
+ PsiElement forward = PsiTreeUtil.skipWhitespacesForward(element);
+ element.delete();
+ myQuickFixAdapter.removeIfComma(forward);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java
new file mode 100644
index 00000000..3d00f1f2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java
@@ -0,0 +1,89 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInsight.completion.CodeCompletionHandlerBase;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.codeInsight.hint.HintManager;
+import com.intellij.codeInspection.BatchQuickFix;
+import com.intellij.codeInspection.CommonProblemDescriptor;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.ex.util.EditorUtil;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiWhiteSpace;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> {
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public SuggestEnumValuesFix(JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Replace with allowed value";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return getFamilyName();
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement initialElement = descriptor.getPsiElement();
+ PsiElement element = myQuickFixAdapter.adjustValue(initialElement);
+ FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile());
+ boolean whitespaceBefore = false;
+ if (element.getPrevSibling() instanceof PsiWhiteSpace) {
+ whitespaceBefore = true;
+ }
+ WriteAction.run(() -> element.delete());
+ EditorEx editor = EditorUtil.getEditorEx(fileEditor);
+ assert editor != null;
+ if (myQuickFixAdapter.fixWhitespaceBefore(initialElement, element) && whitespaceBefore) {
+ WriteAction.run(() -> {
+ int offset = editor.getCaretModel().getOffset();
+ editor.getDocument().insertString(offset, " ");
+ editor.getCaretModel().moveToOffset(offset + 1);
+ });
+ }
+ CodeCompletionHandlerBase.createHandler(CompletionType.BASIC).invokeCompletion(project, editor);
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project,
+ @NotNull CommonProblemDescriptor[] descriptors,
+ @NotNull List<PsiElement> psiElementsToIgnore,
+ @Nullable Runnable refreshViews) {
+ Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
+ if (editor != null) {
+ HintManager.getInstance().showErrorHint(editor, "Sorry, this fix is not available in batch mode");
+ }
+ else {
+ Messages.showErrorDialog(project, "Sorry, this fix is not available in batch mode", "Not Applicable in Batch Mode");
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java
new file mode 100644
index 00000000..0a47e699
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java
@@ -0,0 +1,46 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeHighlighting.HighlightDisplayLevel;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ObjectUtils;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class JsonSchemaBasedInspectionBase extends LocalInspectionTool {
+ @NotNull
+ @Override
+ public HighlightDisplayLevel getDefaultLevel() {
+ return HighlightDisplayLevel.WARNING;
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) {
+ PsiFile file = holder.getFile();
+ JsonValue root = file instanceof JsonFile ? ObjectUtils.tryCast(file.getFirstChild(), JsonValue.class) : null;
+ if (root == null) return PsiElementVisitor.EMPTY_VISITOR;
+
+ JsonSchemaService service = JsonSchemaService.Impl.get(file.getProject());
+ VirtualFile virtualFile = file.getViewProvider().getVirtualFile();
+ if (!service.isApplicableToFile(virtualFile)) return PsiElementVisitor.EMPTY_VISITOR;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile);
+
+ return doBuildVisitor(root, rootSchema, service, holder, session);
+ }
+
+ protected abstract PsiElementVisitor doBuildVisitor(@NotNull JsonValue root,
+ @Nullable JsonSchemaObject schema,
+ @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java
new file mode 100644
index 00000000..0ff94732
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java
@@ -0,0 +1,67 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonComplianceCheckerOptions;
+import com.jetbrains.jsonSchema.impl.JsonSchemaComplianceChecker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public class JsonSchemaComplianceInspection extends JsonSchemaBasedInspectionBase {
+ public boolean myCaseInsensitiveEnum = false;
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("json.schema.inspection.compliance.name");
+ }
+
+ @Override
+ protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root, @Nullable JsonSchemaObject schema, @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session) {
+ if (schema == null) return PsiElementVisitor.EMPTY_VISITOR;
+ JsonComplianceCheckerOptions options = new JsonComplianceCheckerOptions(myCaseInsensitiveEnum);
+
+ return new JsonElementVisitor() {
+ @Override
+ public void visitElement(PsiElement element) {
+ if (element == root) {
+ // perform this only for the root element, because the checker traverses the hierarchy itself
+ annotate(element, schema, holder, session, options);
+ }
+ super.visitElement(element);
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public JComponent createOptionsPanel() {
+ final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this);
+ optionsPanel.addCheckbox(JsonBundle.message("json.schema.inspection.case.insensitive.enum"), "myCaseInsensitiveEnum");
+ return optionsPanel;
+ }
+
+ private static void annotate(@NotNull PsiElement element,
+ @NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session,
+ JsonComplianceCheckerOptions options) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema);
+ if (walker == null) return;
+ new JsonSchemaComplianceChecker(rootSchema, holder, walker, session, options).annotate(element);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java
new file mode 100644
index 00000000..395b9429
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java
@@ -0,0 +1,93 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.paths.WebReference;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonPointerReferenceProvider;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonSchemaRefReferenceInspection extends JsonSchemaBasedInspectionBase {
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("json.schema.ref.refs.inspection.name");
+ }
+
+ @Override
+ protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root,
+ @Nullable JsonSchemaObject schema,
+ @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session) {
+ boolean checkRefs = schema != null && service.isSchemaFile(schema.getSchemaFile());
+ return new JsonElementVisitor() {
+ @Override
+ public void visitElement(PsiElement element) {
+ if (element == root) {
+ if (element instanceof JsonObject) {
+ final JsonProperty schemaProp = ((JsonObject)element).findProperty("$schema");
+ if (schemaProp != null) {
+ doCheck(schemaProp.getValue());
+ }
+ }
+ }
+ super.visitElement(element);
+ }
+
+ @Override
+ public void visitProperty(@NotNull JsonProperty o) {
+ if (!checkRefs) return;
+ if ("$ref".equals(o.getName())) {
+ doCheck(o.getValue());
+ }
+ super.visitProperty(o);
+ }
+
+ private void doCheck(JsonValue value) {
+ if (!(value instanceof JsonStringLiteral)) return;
+ for (PsiReference reference : value.getReferences()) {
+ if (reference instanceof WebReference) continue;
+ final PsiElement resolved = reference.resolve();
+ if (resolved == null) {
+ holder.registerProblem(reference, getReferenceErrorDesc(reference), ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
+ }
+ }
+ }
+
+ private String getReferenceErrorDesc(PsiReference reference) {
+ final String text = reference.getCanonicalText();
+ if (reference instanceof FileReference) {
+ final int hash = text.indexOf('#');
+ return JsonBundle.message("json.schema.ref.file.not.found", hash == -1 ? text : text.substring(0, hash));
+ }
+ if (reference instanceof JsonPointerReferenceProvider.JsonSchemaIdReference) {
+ return JsonBundle.message("json.schema.ref.cannot.resolve.id", text);
+ }
+ final int lastSlash = text.lastIndexOf('/');
+ if (lastSlash == -1) {
+ return JsonBundle.message("json.schema.ref.cannot.resolve.path", text);
+ }
+ final String substring = text.substring(text.lastIndexOf('/') + 1);
+
+ try {
+ Integer.parseInt(substring);
+ return JsonBundle.message("json.schema.ref.no.array.element", substring);
+ }
+ catch (Exception e) {
+ return JsonBundle.message("json.schema.ref.no.property", substring);
+ }
+ }
+ };
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java
new file mode 100644
index 00000000..76cbff16
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java
@@ -0,0 +1,92 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Couple;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.vfs.impl.http.RemoteFileState;
+import com.intellij.util.UriUtil;
+import com.intellij.util.Url;
+import com.intellij.util.Urls;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+public class JsonFileResolver {
+ public static boolean isRemoteEnabled(Project project) {
+ return !ApplicationManager.getApplication().isUnitTestMode() &&
+ JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled();
+ }
+
+ @Nullable
+ public static VirtualFile urlToFile(@NotNull String urlString) {
+ return VirtualFileManager.getInstance().findFileByUrl(replaceUnsafeSchemaStoreUrls(urlString));
+ }
+
+ @Nullable
+ @Contract("null -> null; !null -> !null")
+ public static String replaceUnsafeSchemaStoreUrls(@Nullable String urlString) {
+ if (urlString == null) return null;
+ if (urlString.equals(JsonSchemaCatalogManager.DEFAULT_CATALOG)) {
+ return JsonSchemaCatalogManager.DEFAULT_CATALOG_HTTPS;
+ }
+ if (StringUtil.startsWithIgnoreCase(urlString, JsonSchemaRemoteContentProvider.STORE_URL_PREFIX_HTTP)) {
+ String newUrl = StringUtil.replace(urlString, "http://json.schemastore.org/", "https://schemastore.azurewebsites.net/schemas/json/");
+ return newUrl.endsWith(".json") ? newUrl : newUrl + ".json";
+ }
+ return urlString;
+ }
+
+ @Nullable
+ public static VirtualFile resolveSchemaByReference(@Nullable VirtualFile currentFile,
+ @Nullable String schemaUrl) {
+ if (schemaUrl == null) return null;
+
+ boolean isHttpPath = isHttpPath(schemaUrl);
+
+ if (StringUtil.startsWithChar(schemaUrl, '.') || !isHttpPath) {
+ // relative path
+ VirtualFile parent = currentFile == null ? null : currentFile.getParent();
+ schemaUrl = parent == null ? null : VfsUtilCore.pathToUrl(parent.getPath() + File.separator + schemaUrl);
+ }
+
+ if (schemaUrl != null) {
+ VirtualFile virtualFile = urlToFile(schemaUrl);
+ // validate the URL before returning the file
+ if (virtualFile instanceof HttpVirtualFile) {
+ String url = virtualFile.getUrl();
+ Url parse = Urls.parse(url, false);
+ if (parse == null || StringUtil.isEmpty(parse.getAuthority()) || StringUtil.isEmpty(parse.getPath())) return null;
+ }
+ if (virtualFile != null) return virtualFile;
+ }
+
+ return null;
+ }
+
+ public static void startFetchingHttpFileIfNeeded(@Nullable VirtualFile path, Project project) {
+ if (!(path instanceof HttpVirtualFile)) return;
+
+ // don't resolve http paths in tests
+ if (!isRemoteEnabled(project)) return;
+
+ RemoteFileInfo info = ((HttpVirtualFile)path).getFileInfo();
+ if (info == null || info.getState() == RemoteFileState.DOWNLOADING_NOT_STARTED) {
+ path.refresh(true, false);
+ }
+ }
+
+ public static boolean isHttpPath(@NotNull String schemaFieldText) {
+ Couple<String> couple = UriUtil.splitScheme(schemaFieldText);
+ return couple.first.startsWith("http");
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java
new file mode 100644
index 00000000..f5ec8fa5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Disables JSON schema download from schema store for particular files.
+ * Not intended to be used except to suppress JSON schema for JSCS
+ */
+@ApiStatus.Experimental
+public interface JsonSchemaCatalogExclusion {
+
+ ExtensionPointName<JsonSchemaCatalogExclusion> EP_NAME = ExtensionPointName.create("com.intellij.json.catalog.exclusion");
+
+ boolean isExcluded(@NotNull VirtualFile file);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java
new file mode 100644
index 00000000..461e94cc
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java
@@ -0,0 +1,173 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.vfs.impl.http.RemoteFileManager;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonCachedValues;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+public class JsonSchemaCatalogManager {
+ static final String DEFAULT_CATALOG = "http://schemastore.org/api/json/catalog.json";
+ static final String DEFAULT_CATALOG_HTTPS = "https://schemastore.azurewebsites.net/api/json/catalog.json";
+ @NotNull private final Project myProject;
+ @NotNull private final JsonSchemaRemoteContentProvider myRemoteContentProvider;
+ @Nullable private VirtualFile myCatalog = null;
+ @NotNull private final ConcurrentMap<String, String> myResolvedMappings = ContainerUtil.newConcurrentMap();
+ private static final String NO_CACHE = "$_$_WS_NO_CACHE_$_$";
+ private static final String EMPTY = "$_$_WS_EMPTY_$_$";
+
+ public JsonSchemaCatalogManager(@NotNull Project project) {
+ myProject = project;
+ myRemoteContentProvider = new JsonSchemaRemoteContentProvider();
+ }
+
+ public void startUpdates() {
+ JsonSchemaCatalogProjectConfiguration.getInstance(myProject).addChangeHandler(() -> {
+ update();
+ JsonSchemaService.Impl.get(myProject).reset();
+ });
+ RemoteFileManager instance = RemoteFileManager.getInstance();
+ instance.addRemoteContentProvider(myRemoteContentProvider);
+ update();
+ }
+
+ private void update() {
+ // ignore schema catalog when remote activity is disabled (when we're in tests or it is off in settings)
+ myCatalog = !JsonFileResolver.isRemoteEnabled(myProject) ? null : JsonFileResolver.urlToFile(DEFAULT_CATALOG);
+ }
+
+ @Nullable
+ public VirtualFile getSchemaFileForFile(@NotNull VirtualFile file) {
+ if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isCatalogEnabled()) return null;
+ for (JsonSchemaCatalogExclusion exclusion : JsonSchemaCatalogExclusion.EP_NAME.getExtensions()) {
+ if (exclusion.isExcluded(file)) {
+ return null;
+ }
+ }
+
+ String name = file.getName();
+ if (myResolvedMappings.containsKey(name)) {
+ String urlString = myResolvedMappings.get(name);
+ if (EMPTY.equals(urlString)) return null;
+ return JsonFileResolver.resolveSchemaByReference(file, urlString);
+ }
+
+ if (myCatalog != null) {
+ String urlString = resolveSchemaFile(file, myCatalog, myProject);
+ if (NO_CACHE.equals(urlString)) return null;
+ myResolvedMappings.put(name, urlString == null ? EMPTY : urlString);
+ return JsonFileResolver.resolveSchemaByReference(file, urlString);
+ }
+
+ return null;
+ }
+
+ public List<String> getAllCatalogSchemas() {
+ if (myCatalog != null) {
+ List<Pair<Collection<String>, String>> catalog = JsonCachedValues.getSchemaCatalog(myCatalog, myProject);
+ if (catalog == null) return ContainerUtil.emptyList();
+ List<String> results = ContainerUtil.newArrayListWithCapacity(catalog.size());
+ for (Pair<Collection<String>, String> item: catalog) {
+ results.add(item.second);
+ }
+ return results;
+ }
+
+ return ContainerUtil.emptyList();
+ }
+
+ private final Map<Runnable, FileDownloadingAdapter> myDownloadingAdapters = ContainerUtil.createConcurrentWeakMap();
+ public void registerCatalogUpdateCallback(Runnable callback) {
+ if (myCatalog instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo();
+ if (info != null) {
+ FileDownloadingAdapter adapter = new FileDownloadingAdapter() {
+ @Override
+ public void fileDownloaded(@NotNull VirtualFile localFile) {
+ callback.run();
+ }
+ };
+ myDownloadingAdapters.put(callback, adapter);
+ info.addDownloadingListener(adapter);
+ }
+ }
+ }
+
+ public void unregisterCatalogUpdateCallback(Runnable callback) {
+ if (!myDownloadingAdapters.containsKey(callback)) return;
+
+ if (myCatalog instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo();
+ if (info != null) {
+ info.removeDownloadingListener(myDownloadingAdapters.get(callback));
+ }
+ }
+ }
+
+ public void triggerUpdateCatalog(Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(myCatalog, project);
+ }
+
+ @Nullable
+ private static String resolveSchemaFile(@NotNull VirtualFile file, @NotNull VirtualFile catalogFile, @NotNull Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(catalogFile, project);
+
+ List<Pair<Collection<String>, String>> schemaCatalog = JsonCachedValues.getSchemaCatalog(catalogFile, project);
+ if (schemaCatalog == null) return catalogFile instanceof HttpVirtualFile ? NO_CACHE : null;
+ String fileName = file.getName();
+ for (Pair<Collection<String>, String> maskAndPath: schemaCatalog) {
+ if (matches(fileName, maskAndPath.first)) {
+ return maskAndPath.second;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean matches(@NotNull String fileName, @NotNull Collection<String> masks) {
+ for (String mask: masks) {
+ if (matches(fileName, mask)) return true;
+ }
+ return false;
+ }
+
+ private static boolean matches(@NotNull String fileName, @NotNull String mask) {
+ if (mask.equals(fileName)) return true;
+ int star = mask.indexOf('*');
+
+ // no star - no match
+ if (star == -1) return false;
+
+ // *.foo.json
+ if (star == 0 && fileName.startsWith(mask.substring(1))) {
+ return true;
+ }
+
+ // foobar*
+ if (star == mask.length() - 1 && fileName.endsWith(mask.substring(0, mask.length() - 1))) {
+ return true;
+ }
+
+ String beforeStar = mask.substring(0, star);
+ String afterStar = mask.substring(star + 1);
+
+ if (fileName.startsWith(beforeStar) && fileName.endsWith(afterStar)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java
new file mode 100644
index 00000000..dad1766b
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java
@@ -0,0 +1,125 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.DefaultRemoteContentProvider;
+import com.intellij.util.Url;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.HttpRequests;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.util.List;
+
+public class JsonSchemaRemoteContentProvider extends DefaultRemoteContentProvider {
+ private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
+ private static final long UPDATE_DELAY = Duration.ofHours(4).toMillis();
+ static final String STORE_URL_PREFIX_HTTP = "http://json.schemastore.org";
+ static final String STORE_URL_PREFIX_HTTPS = "https://schemastore.azurewebsites.net";
+ private static final String SCHEMA_URL_PREFIX = "http://json-schema.org/";
+ private static final String ETAG_HEADER = "ETag";
+ private static final String LAST_MODIFIED_HEADER = "Last-Modified";
+
+ private long myLastUpdateTime = 0;
+
+ @Override
+ public boolean canProvideContent(@NotNull Url url) {
+ String externalForm = url.toExternalForm();
+ return externalForm.startsWith(STORE_URL_PREFIX_HTTP)
+ || externalForm.startsWith(STORE_URL_PREFIX_HTTPS)
+ || externalForm.startsWith(SCHEMA_URL_PREFIX)
+ || externalForm.endsWith(".json");
+ }
+
+ @Override
+ protected void saveAdditionalData(@NotNull HttpRequests.Request request, @NotNull File file) throws IOException {
+ URLConnection connection = request.getConnection();
+ if (saveTag(file, connection, ETAG_HEADER)) return;
+ saveTag(file, connection, LAST_MODIFIED_HEADER);
+ }
+
+ @Nullable
+ @Override
+ protected FileType adjustFileType(@Nullable FileType type, @NotNull Url url) {
+ if (type == null && url.toExternalForm().startsWith(SCHEMA_URL_PREFIX)) {
+ // json-schema.org doesn't provide a mime-type for schemas
+ return JsonFileType.INSTANCE;
+ }
+ return super.adjustFileType(type, url);
+ }
+
+ private static boolean saveTag(@NotNull File file, @NotNull URLConnection connection, @NotNull String header) throws IOException {
+ String tag = connection.getHeaderField(header);
+ if (tag != null) {
+ String path = file.getAbsolutePath();
+ if (!path.endsWith(".json")) path += ".json";
+ File tagFile = new File(path + "." + header);
+ saveToFile(tagFile, tag);
+ return true;
+ }
+ return false;
+ }
+
+ private static void saveToFile(@NotNull File tagFile, @NotNull String headerValue) throws IOException {
+ if (!tagFile.exists()) if (!tagFile.createNewFile()) return;
+ Files.write(tagFile.toPath(), ContainerUtil.createMaybeSingletonList(headerValue));
+ }
+
+ @Override
+ public boolean isUpToDate(@NotNull Url url, @NotNull VirtualFile local) {
+ long now = System.currentTimeMillis();
+ // don't update more frequently than once in 4 hours
+ if (now - myLastUpdateTime < UPDATE_DELAY) {
+ return true;
+ }
+
+ myLastUpdateTime = now;
+ String path = local.getPath();
+
+ if (now - new File(path).lastModified() < UPDATE_DELAY) {
+ return true;
+ }
+
+ if (checkUpToDate(url, path, ETAG_HEADER)) return true;
+ if (checkUpToDate(url, path, LAST_MODIFIED_HEADER)) return true;
+
+ return false;
+ }
+
+ private boolean checkUpToDate(@NotNull Url url, @NotNull String path, @NotNull String header) {
+ File file = new File(path + "." + header);
+ try {
+ return isUpToDate(url, file, header);
+ }
+ catch (IOException e) {
+ // in case of an error, don't bother with update for the next UPDATE_DELAY milliseconds
+ //noinspection ResultOfMethodCallIgnored
+ new File(path).setLastModified(System.currentTimeMillis());
+ return true;
+ }
+ }
+
+ @Override
+ protected int getDefaultConnectionTimeout() {
+ return DEFAULT_CONNECT_TIMEOUT;
+ }
+
+ private boolean isUpToDate(@NotNull Url url, @NotNull File file, @NotNull String header) throws IOException {
+ List<String> strings = file.exists() ? Files.readAllLines(file.toPath()) : ContainerUtil.emptyList();
+
+ String currentTag = strings.size() > 0 ? strings.get(0) : null;
+ if (currentTag == null) return false;
+
+ String remoteTag = connect(url, HttpRequests.head(url.toExternalForm()),
+ r -> r.getConnection().getHeaderField(header));
+
+ return currentTag.equals(remoteTag);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java
new file mode 100644
index 00000000..e2fc9365
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java
@@ -0,0 +1,150 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.fileChooser.ex.FileTextFieldImpl;
+import com.intellij.openapi.fileChooser.ex.LocalFsFinder;
+import com.intellij.openapi.fileChooser.impl.FileChooserFactoryImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.TextBrowseFolderListener;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ui.AbstractTableCellEditor;
+import com.intellij.util.ui.JBUI;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+
+class JsonMappingsTableCellEditor extends AbstractTableCellEditor {
+
+ final TextFieldWithBrowseButton myComponent;
+ final JPanel myWrapper;
+ private final UserDefinedJsonSchemaConfiguration.Item myItem;
+ private final Project myProject;
+ private final TreeUpdater myTreeUpdater;
+
+ JsonMappingsTableCellEditor(UserDefinedJsonSchemaConfiguration.Item item, Project project, TreeUpdater treeUpdater) {
+ myItem = item;
+ myProject = project;
+ myTreeUpdater = treeUpdater;
+ myComponent = new TextFieldWithBrowseButton() {
+ @Override
+ protected void installPathCompletion(FileChooserDescriptor fileChooserDescriptor, Disposable parent) {
+ // do nothing
+ }
+ };
+ myWrapper = new JPanel();
+ myWrapper.setBorder(JBUI.Borders.empty(-3, 0));
+ myWrapper.setLayout(new BorderLayout());
+ JLabel label = new JLabel(item.mappingKind.getPrefix().trim(), item.mappingKind.getIcon(), SwingConstants.LEFT);
+ label.setBorder(JBUI.Borders.emptyLeft(1));
+ myWrapper.add(label,
+ BorderLayout.LINE_START);
+ myWrapper.add(myComponent,
+ BorderLayout.CENTER);
+ FileChooserDescriptor descriptor = createDescriptor(item);
+ if (item.isPattern()) {
+ myComponent.getButton().setVisible(false);
+ }
+ else {
+ myComponent.addBrowseFolderListener(
+ new TextBrowseFolderListener(
+ descriptor, myProject) {
+ @NotNull
+ @Override
+ protected String chosenFileToResultingText(@NotNull VirtualFile chosenFile) {
+ String relativePath = VfsUtilCore.getRelativePath(chosenFile, myProject.getBaseDir());
+ return relativePath != null ? relativePath : chosenFile.getPath();
+ }
+ });
+ }
+
+
+ FileTextFieldImpl field = null;
+ if (!item.isPattern() && !ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) {
+ LocalFsFinder finder = new LocalFsFinder();
+ finder.setBaseDir(new File(myProject.getBaseDir().getPath()));
+ field = new MyFileTextFieldImpl(finder, descriptor, myComponent.getTextField(), myProject, myComponent);
+ }
+
+ // avoid closing the dialog by [Enter]
+ FileTextFieldImpl finalField = field;
+ myComponent.getTextField().addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER && (finalField == null || !finalField.isPopupDisplayed())) {
+ stopCellEditing();
+ }
+ }
+ });
+ }
+
+ @NotNull
+ private static FileChooserDescriptor createDescriptor(UserDefinedJsonSchemaConfiguration.Item item) {
+ return item.mappingKind == JsonMappingKind.File
+ ? FileChooserDescriptorFactory.createSingleFileDescriptor()
+ : FileChooserDescriptorFactory.createSingleFolderDescriptor();
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+ myComponent.getChildComponent().setText(myItem.getPath());
+ return myWrapper;
+ }
+
+ @Override
+ public boolean stopCellEditing() {
+ myItem.setPath(myComponent.getChildComponent().getText());
+ myTreeUpdater.updateTree(true);
+ return super.stopCellEditing();
+ }
+
+ @Override
+ public Object getCellEditorValue() {
+ return myComponent.getChildComponent().getText();
+ }
+
+ private static class MyFileTextFieldImpl extends FileTextFieldImpl {
+ private final JTextField myTextField;
+ private final Project myProject;
+
+ MyFileTextFieldImpl(LocalFsFinder finder, FileChooserDescriptor descriptor, JTextField textField, Project project, Disposable parent) {
+ super(textField, finder, new LocalFsFinder.FileChooserFilter(descriptor, true),
+ FileChooserFactoryImpl.getMacroMap(), parent);
+ myTextField = textField;
+ myProject = project;
+ myAutopopup = true;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSelectedFile() {
+ LookupFile lookupFile = getFile();
+ return lookupFile != null ? ((LocalFsFinder.VfsFile)lookupFile).getFile() : null;
+ }
+
+ @Override
+ protected void setTextToFile(LookupFile file) {
+ String path = file.getAbsolutePath();
+ VirtualFile ioFile = VfsUtil.findFileByIoFile(new File(path), false);
+ if (ioFile == null) {
+ myTextField.setText(path);
+ return;
+ }
+ String relativePath = VfsUtilCore.getRelativePath(ioFile, myProject.getBaseDir());
+ myTextField.setText(relativePath != null ? relativePath : path);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java
new file mode 100644
index 00000000..94697695
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java
@@ -0,0 +1,52 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.table.TableView;
+import com.intellij.util.ui.StatusText;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.table.TableCellEditor;
+
+class JsonMappingsTableView extends TableView<UserDefinedJsonSchemaConfiguration.Item> {
+ private final StatusText myEmptyText;
+
+ JsonMappingsTableView(JsonSchemaMappingsView.MyAddActionButtonRunnable runnable) {
+ myEmptyText = new StatusText() {
+ @Override
+ protected boolean isStatusVisible() {
+ return isEmpty();
+ }
+ };
+ myEmptyText.setText("No schema mappings defined")
+ .appendSecondaryText("Add mapping for a ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null);
+
+ JsonMappingKind[] values = JsonMappingKind.values();
+ for (int i = 0; i < values.length; i++) {
+ JsonMappingKind kind = values[i];
+ myEmptyText.appendSecondaryText(kind.getDescription(), SimpleTextAttributes.LINK_ATTRIBUTES,
+ e -> runnable.doRun(kind));
+ if (i < values.length - 1) {
+ myEmptyText.appendSecondaryText(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null);
+ }
+ }
+
+ setFocusTraversalKeysEnabled(false);
+ }
+
+ @Override
+ public void setCellEditor(TableCellEditor anEditor) {
+ super.setCellEditor(anEditor);
+ if (anEditor != null) {
+ ((JsonMappingsTableCellEditor)anEditor).myComponent.getTextField().requestFocus();
+ }
+ }
+
+ @NotNull
+ @Override
+ public StatusText getEmptyText() {
+ return myEmptyText;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java
new file mode 100644
index 00000000..a264808c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java
@@ -0,0 +1,219 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.execution.configurations.RuntimeConfigurationWarning;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.NamedConfigurable;
+import com.intellij.openapi.util.Comparing;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.Function;
+import com.intellij.util.Urls;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.impl.JsonSchemaReader;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaConfigurable extends NamedConfigurable<UserDefinedJsonSchemaConfiguration> {
+ private final Project myProject;
+ @NotNull private final String mySchemaFilePath;
+ @NotNull private final UserDefinedJsonSchemaConfiguration mySchema;
+ @Nullable private final TreeUpdater myTreeUpdater;
+ @NotNull private final Function<? super String, String> myNameCreator;
+ private JsonSchemaMappingsView myView;
+ private String myDisplayName;
+ private String myError;
+
+ public JsonSchemaConfigurable(Project project,
+ @NotNull String schemaFilePath, @NotNull UserDefinedJsonSchemaConfiguration schema,
+ @Nullable TreeUpdater updateTree,
+ @NotNull Function<? super String, String> nameCreator) {
+ super(true, () -> {
+ if (updateTree != null) {
+ updateTree.updateTree(true);
+ }
+ });
+ myProject = project;
+ mySchemaFilePath = schemaFilePath;
+ mySchema = schema;
+ myTreeUpdater = updateTree;
+ myNameCreator = nameCreator;
+ myDisplayName = mySchema.getName();
+ }
+
+ @NotNull
+ public UserDefinedJsonSchemaConfiguration getSchema() {
+ return mySchema;
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ myDisplayName = name;
+ }
+
+ @Override
+ public UserDefinedJsonSchemaConfiguration getEditableObject() {
+ return mySchema;
+ }
+
+ @Override
+ public String getBannerSlogan() {
+ return mySchema.getName();
+ }
+
+ @Override
+ public JComponent createOptionsPanel() {
+ if (myView == null) {
+ myView = new JsonSchemaMappingsView(myProject, myTreeUpdater, s -> {
+ if (myDisplayName.startsWith(JsonSchemaMappingsConfigurable.STUB_SCHEMA_NAME)) {
+ int lastSlash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
+ if (lastSlash > 0) {
+ String substring = s.substring(lastSlash + 1);
+ int dot = substring.lastIndexOf('.');
+ if (dot != -1) {
+ substring = substring.substring(0, dot);
+ }
+ setDisplayName(myNameCreator.fun(substring));
+ updateName();
+ }
+ }
+ });
+ myView.setError(myError, true);
+ }
+ return myView.getComponent();
+ }
+
+ @Nls
+ @Override
+ public String getDisplayName() {
+ return myDisplayName;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return JsonSchemaMappingsConfigurable.SETTINGS_JSON_SCHEMA;
+ }
+
+ @Override
+ public boolean isModified() {
+ if (myView == null) return false;
+ if (!FileUtil.toSystemDependentName(mySchema.getRelativePathToSchema()).equals(myView.getSchemaSubPath())) return true;
+ if (mySchema.getSchemaVersion() != myView.getSchemaVersion()) return true;
+ return !Comparing.equal(myView.getData(), mySchema.getPatterns());
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ if (myView == null) return;
+ doValidation();
+ mySchema.setName(myDisplayName);
+ mySchema.setSchemaVersion(myView.getSchemaVersion());
+ mySchema.setPatterns(myView.getData());
+ mySchema.setRelativePathToSchema(myView.getSchemaSubPath());
+ }
+
+ public static boolean isValidURL(@NotNull final String url) {
+ return JsonFileResolver.isHttpPath(url) && Urls.parse(url, false) != null;
+ }
+
+ private void doValidation() throws ConfigurationException {
+ String schemaSubPath = myView.getSchemaSubPath();
+
+ if (StringUtil.isEmptyOrSpaces(schemaSubPath)) {
+ throw new ConfigurationException((!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema path is empty");
+ }
+
+ VirtualFile vFile;
+ String filename;
+
+ if (JsonFileResolver.isHttpPath(schemaSubPath)) {
+ filename = schemaSubPath;
+
+ if (!isValidURL(schemaSubPath)) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid schema URL");
+ }
+
+ vFile = JsonFileResolver.urlToFile(schemaSubPath);
+ if (vFile == null) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid URL resource");
+ }
+ }
+ else {
+ File subPath = new File(schemaSubPath);
+ final File file = subPath.isAbsolute() ? subPath : new File(myProject.getBasePath(), schemaSubPath);
+ if (!file.exists() || (vFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)) == null) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema file does not exist");
+ }
+ filename = file.getName();
+ }
+
+ if (StringUtil.isEmptyOrSpaces(myDisplayName)) throw new ConfigurationException(filename + ": Schema name is empty");
+
+ // we don't validate remote schemas while in options dialog
+ if (vFile instanceof HttpVirtualFile) return;
+
+ final String error = JsonSchemaReader.checkIfValidJsonSchema(myProject, vFile);
+ if (error != null) {
+ logErrorForUser(error);
+ throw new RuntimeConfigurationWarning(error);
+ }
+ }
+
+ private void logErrorForUser(@NotNull final String error) {
+ JsonSchemaReader.ERRORS_NOTIFICATION.createNotification(error, MessageType.WARNING).notify(myProject);
+ }
+
+ @Override
+ public void reset() {
+ if (myView == null) return;
+ myView.setItems(mySchemaFilePath, mySchema.getSchemaVersion(), mySchema.getPatterns());
+ setDisplayName(mySchema.getName());
+ }
+
+ public UserDefinedJsonSchemaConfiguration getUiSchema() {
+ final UserDefinedJsonSchemaConfiguration info = new UserDefinedJsonSchemaConfiguration();
+ info.setApplicationDefined(mySchema.isApplicationDefined());
+ if (myView != null && myView.isInitialized()) {
+ info.setName(getDisplayName());
+ info.setSchemaVersion(myView.getSchemaVersion());
+ info.setPatterns(myView.getData());
+ info.setRelativePathToSchema(myView.getSchemaSubPath());
+ } else {
+ info.setName(mySchema.getName());
+ info.setSchemaVersion(mySchema.getSchemaVersion());
+ info.setPatterns(mySchema.getPatterns());
+ info.setRelativePathToSchema(mySchema.getRelativePathToSchema());
+ }
+ return info;
+ }
+
+ @Override
+ public void disposeUIResources() {
+ if (myView != null) Disposer.dispose(myView);
+ }
+
+ public void setError(String error, boolean showWarning) {
+ myError = error;
+ if (myView != null) {
+ myView.setError(error, showWarning);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java
new file mode 100644
index 00000000..b68e2098
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java
@@ -0,0 +1,333 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonShortcuts;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SearchableConfigurable;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.ui.MasterDetailsComponent;
+import com.intellij.ui.EditorNotifications;
+import com.intellij.util.Function;
+import com.intellij.util.IconUtil;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.MultiMap;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.tree.DefaultTreeModel;
+import java.io.File;
+import java.util.*;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaMappingsConfigurable extends MasterDetailsComponent implements SearchableConfigurable, Disposable {
+ @NonNls public static final String SETTINGS_JSON_SCHEMA = "settings.json.schema";
+ public static final String JSON_SCHEMA_MAPPINGS = "JSON Schema Mappings";
+
+ private final static Comparator<UserDefinedJsonSchemaConfiguration> COMPARATOR = (o1, o2) -> {
+ if (o1.isApplicationDefined() != o2.isApplicationDefined()) {
+ return o1.isApplicationDefined() ? 1 : -1;
+ }
+ return o1.getName().compareToIgnoreCase(o2.getName());
+ };
+ static final String STUB_SCHEMA_NAME = "New Schema";
+ private String myError;
+
+ @NotNull
+ private final Project myProject;
+ private final TreeUpdater myTreeUpdater = showWarning -> {
+ TREE_UPDATER.run();
+ updateWarningText(showWarning);
+ };
+
+ private final Function<String, String> myNameCreator = s -> createUniqueName(s);
+
+ public JsonSchemaMappingsConfigurable(@NotNull final Project project) {
+ myProject = project;
+ initTree();
+ }
+
+ @Nullable
+ @Override
+ protected String getEmptySelectionString() {
+ return myRoot.children().hasMoreElements() ? "Select JSON Schema to view" :
+ "Please add a JSON Schema file and configure its usage";
+ }
+
+ @Nullable
+ @Override
+ protected ArrayList<AnAction> createActions(boolean fromPopup) {
+ final ArrayList<AnAction> result = new ArrayList<>();
+ result.add(new DumbAwareAction("Add", "Add", IconUtil.getAddIcon()) {
+ {
+ registerCustomShortcutSet(CommonShortcuts.INSERT, myTree);
+ }
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ addProjectSchema();
+ }
+ });
+ result.add(new MyDeleteAction());
+ return result;
+ }
+
+ public UserDefinedJsonSchemaConfiguration addProjectSchema() {
+ UserDefinedJsonSchemaConfiguration configuration = new UserDefinedJsonSchemaConfiguration(createUniqueName(STUB_SCHEMA_NAME),
+ JsonSchemaVersion.SCHEMA_4, "", false, null);
+ addCreatedMappings(configuration);
+ return configuration;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @NotNull
+ private String createUniqueName(@NotNull String s) {
+ int max = -1;
+ Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object element = children.nextElement();
+ if (!(element instanceof MyNode)) continue;
+ String displayName = ((MyNode)element).getDisplayName();
+ if (displayName.startsWith(s)) {
+ String lastPart = displayName.substring(s.length()).trim();
+ if (lastPart.length() == 0 && max == -1) {
+ max = 1;
+ continue;
+ }
+ int i = tryParseInt(lastPart);
+ if (i == -1) continue;
+ max = i > max ? i : max;
+ }
+ }
+ return max == -1 ? s : (s + " " + (max + 1));
+ }
+
+ private static int tryParseInt(@NotNull String s) {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ private void addCreatedMappings(@NotNull final UserDefinedJsonSchemaConfiguration info) {
+ final JsonSchemaConfigurable configurable = new JsonSchemaConfigurable(myProject, "", info, myTreeUpdater, myNameCreator);
+ configurable.setError(myError, true);
+ final MyNode node = new MyNode(configurable);
+ addNode(node, myRoot);
+ selectNodeInTree(node, true);
+ }
+
+ private void fillTree() {
+ myRoot.removeAllChildren();
+
+ if (myProject.isDefault()) return;
+
+ final List<UserDefinedJsonSchemaConfiguration> list = getStoredList();
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ String pathToSchema = info.getRelativePathToSchema();
+ final JsonSchemaConfigurable configurable =
+ new JsonSchemaConfigurable(myProject, isHttpPath(pathToSchema) || new File(pathToSchema).isAbsolute() ? pathToSchema : new File(myProject.getBasePath(), pathToSchema).getPath(),
+ info, myTreeUpdater, myNameCreator);
+ configurable.setError(myError, true);
+ myRoot.add(new MyNode(configurable));
+ }
+ ((DefaultTreeModel) myTree.getModel()).reload(myRoot);
+ if (myRoot.children().hasMoreElements()) {
+ myTree.addSelectionRow(0);
+ }
+ }
+
+ @NotNull
+ private List<UserDefinedJsonSchemaConfiguration> getStoredList() {
+ final List<UserDefinedJsonSchemaConfiguration> list = new ArrayList<>();
+ final Map<String, UserDefinedJsonSchemaConfiguration> projectState = JsonSchemaMappingsProjectConfiguration
+ .getInstance(myProject).getStateMap();
+ if (projectState != null) {
+ list.addAll(projectState.values());
+ }
+
+ Collections.sort(list, COMPARATOR);
+ return list;
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ final List<UserDefinedJsonSchemaConfiguration> uiList = getUiList(true);
+ validate(uiList);
+ final Map<String, UserDefinedJsonSchemaConfiguration> projectMap = new HashMap<>();
+ for (UserDefinedJsonSchemaConfiguration info : uiList) {
+ projectMap.put(info.getName(), info);
+ }
+
+ JsonSchemaMappingsProjectConfiguration.getInstance(myProject).setState(projectMap);
+ final Project[] projects = ProjectManager.getInstance().getOpenProjects();
+ for (Project project : projects) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(project);
+ if (service != null) service.reset();
+ }
+ DaemonCodeAnalyzer.getInstance(myProject).restart();
+ EditorNotifications.getInstance(myProject).updateAllNotifications();
+ }
+
+ private static void validate(@NotNull List<UserDefinedJsonSchemaConfiguration> list) throws ConfigurationException {
+ final Set<String> set = new HashSet<>();
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ if (set.contains(info.getName())) {
+ throw new ConfigurationException("Duplicate schema name: '" + info.getName() + "'");
+ }
+ set.add(info.getName());
+ }
+ }
+
+ @Override
+ public boolean isModified() {
+ final List<UserDefinedJsonSchemaConfiguration> storedList = getStoredList();
+ final List<UserDefinedJsonSchemaConfiguration> uiList;
+ try {
+ uiList = getUiList(false);
+ }
+ catch (ConfigurationException e) {
+ //will not happen
+ return false;
+ }
+ return !storedList.equals(uiList);
+ }
+
+ private void updateWarningText(boolean showWarning) {
+ final MultiMap<String, UserDefinedJsonSchemaConfiguration.Item> patternsMap = new MultiMap<>();
+ final StringBuilder sb = new StringBuilder();
+ final List<UserDefinedJsonSchemaConfiguration> list;
+ try {
+ list = getUiList(false);
+ }
+ catch (ConfigurationException e) {
+ // will not happen
+ return;
+ }
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ info.refreshPatterns();
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(myProject);
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = info.getPatterns();
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : patterns) {
+ for (Map.Entry<String, Collection<UserDefinedJsonSchemaConfiguration.Item>> entry : patternsMap.entrySet()) {
+ for (UserDefinedJsonSchemaConfiguration.Item item : entry.getValue()) {
+ final ThreeState similar = comparator.isSimilar(pattern, item);
+ if (ThreeState.NO.equals(similar)) continue;
+
+ if (sb.length() > 0) sb.append('\n');
+ sb.append("'").append(pattern.getPresentation()).append("' for schema '")
+ .append(info.getName()).append("' and '").append(item.getPresentation()).append("' for schema '").append(entry.getKey())
+ .append("'");
+ }
+ }
+ }
+ patternsMap.put(info.getName(), patterns);
+ }
+ if (sb.length() > 0) {
+ myError = "Conflicting mappings:\n" + sb.toString();
+ } else {
+ myError = null;
+ }
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object o = children.nextElement();
+ if (o instanceof MyNode && ((MyNode)o).getConfigurable() instanceof JsonSchemaConfigurable) {
+ ((JsonSchemaConfigurable) ((MyNode)o).getConfigurable()).setError(myError, showWarning);
+ }
+ }
+ }
+
+ public void selectInTree(UserDefinedJsonSchemaConfiguration configuration) {
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ final MyNode node = (MyNode)children.nextElement();
+ JsonSchemaConfigurable configurable = (JsonSchemaConfigurable)node.getConfigurable();
+ if (Objects.equals(configurable.getUiSchema(), configuration)) {
+ selectNodeInTree(node);
+ }
+ }
+ }
+
+ @NotNull
+ private List<UserDefinedJsonSchemaConfiguration> getUiList(boolean applyChildren) throws ConfigurationException {
+ final List<UserDefinedJsonSchemaConfiguration> uiList = new ArrayList<>();
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ final MyNode node = (MyNode)children.nextElement();
+ if (applyChildren) {
+ node.getConfigurable().apply();
+ uiList.add(getSchemaInfo(node));
+ }
+ else {
+ uiList.add(((JsonSchemaConfigurable) node.getConfigurable()).getUiSchema());
+ }
+ }
+ Collections.sort(uiList, COMPARATOR);
+ return uiList;
+ }
+
+ @Override
+ public void reset() {
+ fillTree();
+ updateWarningText(true);
+ }
+
+ @Override
+ protected Comparator<MyNode> getNodeComparator() {
+ return (o1, o2) -> {
+ if (o1.getConfigurable() instanceof JsonSchemaConfigurable && o2.getConfigurable() instanceof JsonSchemaConfigurable) {
+ return COMPARATOR.compare(getSchemaInfo(o1), getSchemaInfo(o2));
+ }
+ return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName());
+ };
+ }
+
+ private static UserDefinedJsonSchemaConfiguration getSchemaInfo(@NotNull final MyNode node) {
+ return ((JsonSchemaConfigurable) node.getConfigurable()).getSchema();
+ }
+
+ @Nls
+ @Override
+ public String getDisplayName() {
+ return JSON_SCHEMA_MAPPINGS;
+ }
+
+
+ @Override
+ public void dispose() {
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object o = children.nextElement();
+ if (o instanceof MyNode) {
+ ((MyNode)o).getConfigurable().disposeUIResources();
+ }
+ }
+ }
+
+ @NotNull
+ @Override
+ public String getId() {
+ return SETTINGS_JSON_SCHEMA;
+ }
+
+ @Override
+ public String getHelpTopic() {
+ return SETTINGS_JSON_SCHEMA;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java
new file mode 100644
index 00000000..795a59c0
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java
@@ -0,0 +1,339 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.util.PsiNavigationSupport;
+import com.intellij.json.JsonBundle;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.CommonShortcuts;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.AnActionButtonRunnable;
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.ToolbarDecorator;
+import com.intellij.ui.awt.RelativePoint;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.ui.table.TableView;
+import com.intellij.util.ui.*;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import java.awt.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaMappingsView implements Disposable {
+ private static final String ADD_SCHEMA_MAPPING = "settings.json.schema.add.mapping";
+ private static final String EDIT_SCHEMA_MAPPING = "settings.json.schema.edit.mapping";
+ private static final String REMOVE_SCHEMA_MAPPING = "settings.json.schema.remove.mapping";
+ private final TreeUpdater myTreeUpdater;
+ private final Consumer<? super String> mySchemaPathChangedCallback;
+ private TableView<UserDefinedJsonSchemaConfiguration.Item> myTableView;
+ private JComponent myComponent;
+ private Project myProject;
+ private TextFieldWithBrowseButton mySchemaField;
+ private ComboBox<JsonSchemaVersion> mySchemaVersionComboBox;
+ private JEditorPane myError;
+ private String myErrorText;
+ private JBLabel myErrorIcon;
+ private boolean myInitialized;
+
+ public JsonSchemaMappingsView(Project project,
+ TreeUpdater treeUpdater,
+ Consumer<? super String> schemaPathChangedCallback) {
+ myTreeUpdater = treeUpdater;
+ mySchemaPathChangedCallback = schemaPathChangedCallback;
+ createUI(project);
+ }
+
+ private void createUI(final Project project) {
+ myProject = project;
+ MyAddActionButtonRunnable addActionButtonRunnable = new MyAddActionButtonRunnable();
+
+ myTableView = new JsonMappingsTableView(addActionButtonRunnable);
+ myTableView.getTableHeader().setVisible(false);
+ final ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTableView);
+ final MyEditActionButtonRunnableImpl editAction = new MyEditActionButtonRunnableImpl();
+ decorator.setRemoveAction(new MyRemoveActionButtonRunnable())
+ .setRemoveActionName(REMOVE_SCHEMA_MAPPING)
+ .setAddAction(addActionButtonRunnable)
+ .setAddActionName(JsonBundle.message(ADD_SCHEMA_MAPPING))
+ .setEditAction(editAction)
+ .setEditActionName(JsonBundle.message(EDIT_SCHEMA_MAPPING))
+ .disableUpDownActions();
+
+ JBTextField schemaFieldBacking = new JBTextField();
+ mySchemaField = new TextFieldWithBrowseButton(schemaFieldBacking);
+ SwingHelper.installFileCompletionAndBrowseDialog(myProject, mySchemaField, JsonBundle.message("json.schema.add.schema.chooser.title"),
+ FileChooserDescriptorFactory.createSingleFileDescriptor());
+ mySchemaField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
+ @Override
+ protected void textChanged(@NotNull DocumentEvent e) {
+ mySchemaPathChangedCallback.accept(mySchemaField.getText());
+ }
+ });
+ attachNavigateToSchema();
+ myError = SwingHelper.createHtmlLabel(JsonBundle.message("json.schema.conflicting.mappings"), null, s -> {
+ final BalloonBuilder builder = JBPopupFactory.getInstance().
+ createHtmlTextBalloonBuilder(myErrorText, UIUtil.getBalloonWarningIcon(), MessageType.WARNING.getPopupBackground(), null);
+ builder.setDisposable(this);
+ builder.setHideOnClickOutside(true);
+ builder.setCloseButtonEnabled(true);
+ builder.createBalloon().showInCenterOf(myError);
+ });
+
+ final FormBuilder builder = FormBuilder.createFormBuilder();
+ final JBLabel label = new JBLabel(JsonBundle.message("json.schema.file.selector.title"));
+ builder.addLabeledComponent(label, mySchemaField);
+ label.setLabelFor(mySchemaField);
+ label.setBorder(JBUI.Borders.empty(0, 10));
+ mySchemaField.setBorder(JBUI.Borders.emptyRight(10));
+ JBLabel versionLabel = new JBLabel("Schema version:");
+ mySchemaVersionComboBox = new ComboBox<>(new DefaultComboBoxModel<>(JsonSchemaVersion.values()));
+ versionLabel.setLabelFor(mySchemaVersionComboBox);
+ versionLabel.setBorder(JBUI.Borders.empty(0, 10));
+ builder.addLabeledComponent(versionLabel, mySchemaVersionComboBox);
+ final JPanel wrapper = new JPanel(new BorderLayout());
+ wrapper.setBorder(JBUI.Borders.empty(0, 10));
+ myErrorIcon = new JBLabel(UIUtil.getBalloonWarningIcon());
+ wrapper.add(myErrorIcon, BorderLayout.WEST);
+ wrapper.add(myError, BorderLayout.CENTER);
+ builder.addComponent(wrapper);
+ JPanel panel = decorator.createPanel();
+ panel.setBorder(BorderFactory.createCompoundBorder(JBUI.Borders.empty(0, 8), panel.getBorder()));
+ builder.addComponentFillVertically(panel, 5);
+ JLabel commentComponent = ComponentPanelBuilder.createCommentComponent("Path to file or directory relative to project root, or file name pattern like *.config.json", false);
+ commentComponent.setBorder(JBUI.Borders.empty(0, 8, 5, 0));
+ builder.addComponent(commentComponent);
+
+ myComponent = builder.getPanel();
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ public void setError(final String text, boolean showWarning) {
+ myErrorText = text;
+ myError.setVisible(showWarning && text != null);
+ myErrorIcon.setVisible(showWarning && text != null);
+ }
+
+ private void attachNavigateToSchema() {
+ DumbAwareAction.create(e -> {
+ String pathToSchema = mySchemaField.getText();
+ if (StringUtil.isEmptyOrSpaces(pathToSchema) || isHttpPath(pathToSchema)) return;
+ VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(pathToSchema));
+ if (virtualFile == null) {
+ BalloonBuilder balloonBuilder = JBPopupFactory.getInstance()
+ .createHtmlTextBalloonBuilder(JsonBundle.message("json.schema.file.not.found"), UIUtil.getBalloonErrorIcon(), MessageType.ERROR.getPopupBackground(), null);
+ Balloon balloon = balloonBuilder.setFadeoutTime(TimeUnit.SECONDS.toMillis(3)).createBalloon();
+ balloon.showInCenterOf(mySchemaField);
+ return;
+ }
+ PsiNavigationSupport.getInstance().createNavigatable(myProject, virtualFile, -1).navigate(true);
+ }).registerCustomShortcutSet(CommonShortcuts.getEditSource(), mySchemaField);
+ }
+
+ public List<UserDefinedJsonSchemaConfiguration.Item> getData() {
+ return Collections.unmodifiableList(
+ myTableView.getListTableModel().getItems().stream()
+ .filter(i -> i.mappingKind == JsonMappingKind.Directory || !StringUtil.isEmpty(i.path))
+ .collect(Collectors.toList()));
+ }
+
+ public void setItems(String schemaFilePath,
+ JsonSchemaVersion version,
+ final List<UserDefinedJsonSchemaConfiguration.Item> data) {
+ myInitialized = true;
+ mySchemaField.setText(schemaFilePath);
+ mySchemaVersionComboBox.setSelectedItem(version);
+ myTableView.setModelAndUpdateColumns(
+ new ListTableModel<>(createColumns(), new ArrayList<>(data)));
+ }
+
+ public boolean isInitialized() {
+ return myInitialized;
+ }
+
+ public JsonSchemaVersion getSchemaVersion() {
+ return (JsonSchemaVersion)mySchemaVersionComboBox.getSelectedItem();
+ }
+
+ public String getSchemaSubPath() {
+ String schemaFieldText = mySchemaField.getText();
+ if (isHttpPath(schemaFieldText)) return schemaFieldText;
+ return FileUtil.toSystemDependentName(JsonSchemaInfo.getRelativePath(myProject, schemaFieldText));
+ }
+
+ private ColumnInfo[] createColumns() {
+ return new ColumnInfo[] { new MappingItemColumnInfo() };
+ }
+
+ public JComponent getComponent() {
+ return myComponent;
+ }
+
+ private class MappingItemColumnInfo extends ColumnInfo<UserDefinedJsonSchemaConfiguration.Item, String> {
+ MappingItemColumnInfo() {super("");}
+
+ @Nullable
+ @Override
+ public String valueOf(UserDefinedJsonSchemaConfiguration.Item item) {
+ return item.getPresentation();
+ }
+
+ @NotNull
+ @Override
+ public TableCellRenderer getRenderer(UserDefinedJsonSchemaConfiguration.Item item) {
+ return new DefaultTableCellRenderer() {
+ @Override
+ public Component getTableCellRendererComponent(JTable table,
+ Object value,
+ boolean isSelected,
+ boolean hasFocus,
+ int row,
+ int column) {
+ JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ label.setIcon(item.mappingKind.getIcon());
+
+ String error = item.getError();
+ if (error == null) {
+ return label;
+ }
+
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ panel.add(label, BorderLayout.CENTER);
+ JLabel warning = new JLabel(AllIcons.General.Warning);
+ panel.setBackground(label.getBackground());
+ panel.setToolTipText(error);
+ panel.add(warning, BorderLayout.LINE_END);
+ return panel;
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public TableCellEditor getEditor(UserDefinedJsonSchemaConfiguration.Item item) {
+ return new JsonMappingsTableCellEditor(item, myProject, myTreeUpdater);
+ }
+
+ @Override
+ public boolean isCellEditable(UserDefinedJsonSchemaConfiguration.Item item) {
+ return true;
+ }
+ }
+
+ class MyAddActionButtonRunnable implements AnActionButtonRunnable {
+ MyAddActionButtonRunnable() {
+ super();
+ }
+
+ @Override
+ public void run(AnActionButton button) {
+ RelativePoint point = button.getPreferredPopupPoint();
+ if (point == null) {
+ point = new RelativePoint(button.getContextComponent(), new Point(0, 0));
+ }
+ JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<JsonMappingKind>(null,
+ JsonMappingKind.values()) {
+ @NotNull
+ @Override
+ public String getTextFor(JsonMappingKind value) {
+ return "Add " + StringUtil.capitalizeWords(value.getDescription(), true);
+ }
+
+ @Override
+ public Icon getIconFor(JsonMappingKind value) {
+ return value.getIcon();
+ }
+
+ @Override
+ public PopupStep onChosen(JsonMappingKind selectedValue, boolean finalChoice) {
+ if (finalChoice) {
+ return doFinalStep(() -> doRun(selectedValue));
+ }
+ return PopupStep.FINAL_CHOICE;
+ }
+ }).show(point);
+ }
+
+ void doRun(JsonMappingKind mappingKind) {
+ UserDefinedJsonSchemaConfiguration.Item currentItem = new UserDefinedJsonSchemaConfiguration.Item("", mappingKind);
+ myTableView.getListTableModel().addRow(currentItem);
+ myTableView.editCellAt(myTableView.getListTableModel().getRowCount() - 1, 0);
+
+ myTreeUpdater.updateTree(false);
+ }
+ }
+
+ private class MyEditActionButtonRunnableImpl implements AnActionButtonRunnable {
+ MyEditActionButtonRunnableImpl() {
+ super();
+ }
+
+ @Override
+ public void run(AnActionButton button) {
+ execute();
+ }
+
+ public void execute() {
+ int selectedRow = myTableView.getSelectedRow();
+ if (selectedRow == -1) return;
+ myTableView.editCellAt(selectedRow, 0);
+ }
+ }
+
+ private class MyRemoveActionButtonRunnable implements AnActionButtonRunnable {
+ @Override
+ public void run(AnActionButton button) {
+ final int[] rows = myTableView.getSelectedRows();
+ if (rows != null && rows.length > 0) {
+ int cnt = 0;
+ for (int row : rows) {
+ myTableView.getListTableModel().removeRow(row - cnt);
+ ++cnt;
+ }
+ myTableView.getListTableModel().fireTableDataChanged();
+ myTreeUpdater.updateTree(true);
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java
new file mode 100644
index 00000000..1538a549
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java
@@ -0,0 +1,104 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.BeforeAfter;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * @author Irina.Chernushina on 2/17/2016.
+ */
+public class JsonSchemaPatternComparator {
+ @NotNull
+ private final Project myProject;
+
+ public JsonSchemaPatternComparator(@NotNull Project project) {
+ myProject = project;
+ }
+
+ @NotNull
+ public ThreeState isSimilar(@NotNull UserDefinedJsonSchemaConfiguration.Item itemLeft,
+ @NotNull UserDefinedJsonSchemaConfiguration.Item itemRight) {
+ if (itemLeft.isPattern() != itemRight.isPattern()) return ThreeState.NO;
+ if (itemLeft.isPattern()) return comparePatterns(itemLeft, itemRight);
+ return comparePaths(itemLeft, itemRight);
+ }
+
+ private ThreeState comparePaths(UserDefinedJsonSchemaConfiguration.Item left, UserDefinedJsonSchemaConfiguration.Item right) {
+ String leftPath = left.getPath();
+ String rightPath = right.getPath();
+
+ if (leftPath.startsWith("mock:///") || rightPath.startsWith("mock:///")) {
+ return leftPath.equals(rightPath) ? ThreeState.YES : ThreeState.NO;
+ }
+ final File leftFile = new File(myProject.getBasePath(), leftPath);
+ final File rightFile = new File(myProject.getBasePath(), rightPath);
+
+ if (left.isDirectory()) {
+ if (FileUtil.isAncestor(leftFile, rightFile, true)) return ThreeState.YES;
+ }
+ if (right.isDirectory()) {
+ if (FileUtil.isAncestor(rightFile, leftFile, true)) return ThreeState.YES;
+ }
+ return FileUtil.filesEqual(leftFile, rightFile) && left.isDirectory() == right.isDirectory() ? ThreeState.YES : ThreeState.NO;
+ }
+
+ private static ThreeState comparePatterns(@NotNull final UserDefinedJsonSchemaConfiguration.Item leftItem,
+ @NotNull final UserDefinedJsonSchemaConfiguration.Item rightItem) {
+ if (leftItem.getPath().equals(rightItem.getPath())) return ThreeState.YES;
+ if (leftItem.getPath().indexOf(File.separatorChar) >= 0 || rightItem.getPath().indexOf(File.separatorChar) >= 0) {
+ // todo: better heuristic
+ return ThreeState.NO;
+ }
+ final BeforeAfter<String> left = getBeforeAfterAroundWildCards(leftItem.getPath());
+ final BeforeAfter<String> right = getBeforeAfterAroundWildCards(rightItem.getPath());
+ if (left == null || right == null) {
+ if (left == null && right == null) return leftItem.getPath().equals(rightItem.getPath()) ? ThreeState.YES : ThreeState.NO;
+ if (left == null) {
+ return checkOneSideWithoutWildcard(leftItem, right);
+ }
+ return checkOneSideWithoutWildcard(rightItem, left);
+ }
+ if (!StringUtil.isEmptyOrSpaces(left.getBefore()) && !StringUtil.isEmptyOrSpaces(right.getBefore())) {
+ if (left.getBefore().startsWith(right.getBefore()) || right.getBefore().startsWith(left.getBefore())) {
+ return ThreeState.YES;
+ }
+ // otherwise they are different
+ return ThreeState.NO;
+ }
+ if (!StringUtil.isEmptyOrSpaces(left.getAfter()) && !StringUtil.isEmptyOrSpaces(right.getAfter())) {
+ if (left.getAfter().endsWith(right.getAfter()) || right.getAfter().endsWith(left.getAfter())) {
+ return ThreeState.YES;
+ }
+ // otherwise they are different
+ return ThreeState.NO;
+ }
+ return ThreeState.UNSURE;
+ }
+
+ @NotNull
+ private static ThreeState checkOneSideWithoutWildcard(UserDefinedJsonSchemaConfiguration.Item item, BeforeAfter<String> beforeAfter) {
+ if (!StringUtil.isEmptyOrSpaces(beforeAfter.getBefore()) && item.getPath().startsWith(beforeAfter.getBefore())) {
+ return ThreeState.YES;
+ }
+ if (!StringUtil.isEmptyOrSpaces(beforeAfter.getAfter()) && item.getPath().endsWith(beforeAfter.getAfter())) {
+ return ThreeState.YES;
+ }
+ return ThreeState.UNSURE;
+ }
+
+ @Nullable
+ private static BeforeAfter<String> getBeforeAfterAroundWildCards(@NotNull final String pattern) {
+ final int firstIdx = pattern.indexOf('*');
+ final int lastIdx = pattern.lastIndexOf('*');
+ if (firstIdx < 0 || lastIdx < 0) return null;
+ return new BeforeAfter<>(pattern.substring(0, firstIdx), pattern.substring(lastIdx + 1));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java
new file mode 100644
index 00000000..364f730e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java
@@ -0,0 +1,6 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+public interface TreeUpdater {
+ void updateTree(boolean showWarning);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java
new file mode 100644
index 00000000..e57ccbf8
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java
@@ -0,0 +1,169 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.ListSeparator;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ui.EmptyIcon;
+import com.intellij.util.ui.JBUI;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static com.jetbrains.jsonSchema.widget.JsonSchemaStatusPopup.*;
+
+class JsonSchemaInfoPopupStep extends BaseListPopupStep<JsonSchemaInfo> {
+ private final Project myProject;
+ private final VirtualFile myVirtualFile;
+ @NotNull private final JsonSchemaService myService;
+ private static final Icon EMPTY_ICON = JBUI.scale(EmptyIcon.create(AllIcons.General.Add.getIconWidth()));
+
+ JsonSchemaInfoPopupStep(@NotNull List<JsonSchemaInfo> allSchemas, @NotNull Project project, @NotNull VirtualFile virtualFile,
+ @NotNull JsonSchemaService service) {
+ super(null, allSchemas);
+ myProject = project;
+ myVirtualFile = virtualFile;
+ myService = service;
+ }
+
+ @NotNull
+ @Override
+ public String getTextFor(JsonSchemaInfo value) {
+ return value.getDescription();
+ }
+
+ @Override
+ public Icon getIconFor(JsonSchemaInfo value) {
+ if (value == ADD_MAPPING) {
+ return AllIcons.General.Add;
+ }
+
+ if (value == EDIT_MAPPINGS) {
+ return AllIcons.Actions.Edit;
+ }
+
+ if (value == LOAD_REMOTE) {
+ return AllIcons.Actions.Refresh;
+ }
+
+ return EMPTY_ICON;
+ }
+
+ @Nullable
+ @Override
+ public ListSeparator getSeparatorAbove(JsonSchemaInfo value) {
+ List<JsonSchemaInfo> values = getValues();
+ int index = values.indexOf(value);
+ if (index - 1 >= 0) {
+ JsonSchemaInfo info = values.get(index - 1);
+ if (info == EDIT_MAPPINGS || info == ADD_MAPPING) {
+ return new ListSeparator("Registered schemas");
+ }
+ if (value.getProvider() == null && info.getProvider() != null) {
+ return new ListSeparator("SchemaStore.org schemas");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public PopupStep onChosen(JsonSchemaInfo selectedValue, boolean finalChoice) {
+ if (finalChoice) {
+ if (selectedValue == EDIT_MAPPINGS || selectedValue == ADD_MAPPING) {
+ return doFinalStep(() -> runSchemaEditorForCurrentFile());
+ }
+ else if (selectedValue == LOAD_REMOTE) {
+ return doFinalStep(() -> myService.triggerUpdateRemote());
+ }
+ else {
+ setMapping(selectedValue, myVirtualFile, myProject);
+ return doFinalStep(() -> myService.reset());
+ }
+ }
+ return PopupStep.FINAL_CHOICE;
+ }
+
+ private void runSchemaEditorForCurrentFile() {
+ JsonSchemaMappingsConfigurable configurable = new JsonSchemaMappingsConfigurable(myProject);
+ JsonSchemaMappingsProjectConfiguration mappingsConf = JsonSchemaMappingsProjectConfiguration.getInstance(myProject);
+
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable, () -> {
+ UserDefinedJsonSchemaConfiguration mappingForFile = mappingsConf.findMappingForFile(myVirtualFile);
+ if (mappingForFile == null) {
+ UserDefinedJsonSchemaConfiguration configuration = configurable.addProjectSchema();
+ String relativePath = VfsUtilCore.getRelativePath(myVirtualFile, myProject.getBaseDir());
+ configuration.patterns.add(new UserDefinedJsonSchemaConfiguration.Item(
+ relativePath == null ? myVirtualFile.getUrl() : relativePath, false, false));
+ mappingForFile = configuration;
+ }
+
+ configurable.selectInTree(mappingForFile);
+ });
+ }
+
+ @Override
+ public boolean isSpeedSearchEnabled() {
+ return true;
+ }
+
+ private static void setMapping(@Nullable JsonSchemaInfo selectedValue, @NotNull VirtualFile virtualFile, @NotNull Project project) {
+ JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+
+ VirtualFile projectBaseDir = project.getBaseDir();
+
+ UserDefinedJsonSchemaConfiguration mappingForFile = configuration.findMappingForFile(virtualFile);
+ if (mappingForFile != null) {
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : mappingForFile.patterns) {
+ if (Objects.equals(VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts()), virtualFile)
+ || virtualFile.getUrl().equals(pattern.getPath())) {
+ mappingForFile.patterns.remove(pattern);
+ if (mappingForFile.patterns.size() == 0 && mappingForFile.isApplicationDefined()) {
+ configuration.removeConfiguration(mappingForFile);
+ }
+ else {
+ mappingForFile.refreshPatterns();
+ }
+ break;
+ }
+ }
+ }
+
+ if (selectedValue == null) return;
+
+ String path = VfsUtilCore.getRelativePath(virtualFile, projectBaseDir);
+ if (path == null) {
+ path = virtualFile.getUrl();
+ }
+
+ UserDefinedJsonSchemaConfiguration existing = configuration.findMappingBySchemaInfo(selectedValue);
+ UserDefinedJsonSchemaConfiguration.Item item = new UserDefinedJsonSchemaConfiguration.Item(path, false, false);
+ if (existing != null) {
+ if (!existing.patterns.contains(item)) {
+ existing.patterns.add(item);
+ existing.refreshPatterns();
+ }
+ }
+ else {
+ configuration.addConfiguration(new UserDefinedJsonSchemaConfiguration(selectedValue.getDescription(),
+ selectedValue.getSchemaVersion(),
+ selectedValue.getUrl(project),
+ true,
+ Collections.singletonList(item)));
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java
new file mode 100644
index 00000000..06eff237
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java
@@ -0,0 +1,82 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.ListPopup;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class JsonSchemaStatusPopup {
+ static final JsonSchemaInfo ADD_MAPPING = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "New Schema Mapping…";
+ }
+ };
+
+ static final JsonSchemaInfo EDIT_MAPPINGS = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "Edit Schema Mappings…";
+ }
+ };
+
+ static final JsonSchemaInfo LOAD_REMOTE = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "Load SchemaStore Mappings";
+ }
+ };
+
+ static ListPopup createPopup(@NotNull JsonSchemaService service,
+ @NotNull Project project,
+ @NotNull VirtualFile virtualFile,
+ boolean showOnlyEdit) {
+ JsonSchemaInfoPopupStep step = createPopupStep(service, project, virtualFile, showOnlyEdit);
+ return JBPopupFactory.getInstance().createListPopup(step);
+ }
+
+ @NotNull
+ static JsonSchemaInfoPopupStep createPopupStep(@NotNull JsonSchemaService service,
+ @NotNull Project project,
+ @NotNull VirtualFile virtualFile,
+ boolean showOnlyEdit) {
+ List<JsonSchemaInfo> allSchemas;
+ JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+ UserDefinedJsonSchemaConfiguration mapping = configuration.findMappingForFile(virtualFile);
+ if (!showOnlyEdit || mapping == null) {
+ List<JsonSchemaInfo> infos = service.getAllUserVisibleSchemas();
+ Comparator<JsonSchemaInfo> comparator = Comparator.comparing(JsonSchemaInfo::getDescription, String::compareTo);
+ Stream<JsonSchemaInfo> registered = infos.stream().filter(i -> i.getProvider() != null).sorted(comparator);
+ List<JsonSchemaInfo> otherList = ContainerUtil.emptyList();
+
+ if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled()) {
+ otherList = infos.stream().filter(i -> i.getProvider() == null).sorted(comparator).collect(Collectors.toList());
+ if (otherList.size() == 0) {
+ otherList = ContainerUtil.createMaybeSingletonList(LOAD_REMOTE);
+ }
+ }
+ allSchemas = Stream.concat(registered, otherList.stream()).collect(Collectors.toList());
+ allSchemas.add(0, mapping == null ? ADD_MAPPING : EDIT_MAPPINGS);
+ }
+ else {
+ allSchemas = ContainerUtil.createMaybeSingletonList(EDIT_MAPPINGS);
+ }
+ return new JsonSchemaInfoPopupStep(allSchemas, project, virtualFile, service);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java
new file mode 100644
index 00000000..5ff023e9
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java
@@ -0,0 +1,310 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.ListPopup;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.extension.*;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaConflictNotificationProvider;
+import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class JsonSchemaStatusWidget extends EditorBasedStatusBarPopup {
+ private static final String JSON_SCHEMA_BAR = "JSON: ";
+ private static final String JSON_SCHEMA_BAR_OTHER_FILES = "Schema: ";
+ private static final String JSON_SCHEMA_TOOLTIP = "JSON Schema: ";
+ private static final String JSON_SCHEMA_TOOLTIP_OTHER_FILES = "Validated by JSON Schema: ";
+ private final JsonSchemaService myService;
+ private static final String ID = "JSONSchemaSelector";
+
+ JsonSchemaStatusWidget(Project project) {
+ super(project);
+ myService = JsonSchemaService.Impl.get(project);
+ myService.registerRemoteUpdateCallback(myUpdateCallback);
+ myService.registerResetAction(myUpdateCallback);
+ }
+
+ private final Runnable myUpdateCallback = this::update;
+
+ private static class MyWidgetState extends WidgetState {
+ boolean warning = false;
+ MyWidgetState(String toolTip, String text, boolean actionEnabled) {
+ super(toolTip, text, actionEnabled);
+ }
+
+ public boolean isWarning() {
+ return warning;
+ }
+
+ public void setWarning(boolean warning) {
+ this.warning = warning;
+ this.setIcon(warning ? AllIcons.General.Warning : null);
+ }
+ }
+
+ private boolean hasAccessToSymbols() {
+ return !DumbService.getInstance(myProject).isDumb();
+ }
+
+ @NotNull
+ @Override
+ protected WidgetState getWidgetState(@Nullable VirtualFile file) {
+ if (file == null) {
+ return WidgetState.HIDDEN;
+ }
+
+ JsonSchemaEnabler[] enablers = JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensions();
+ if (Arrays.stream(enablers).noneMatch(e -> e.isEnabledForFile(file) && e.shouldShowSwitcherWidget(file))) {
+ return WidgetState.HIDDEN;
+ }
+
+ FileType fileType = file.getFileType();
+ Language language = fileType instanceof LanguageFileType ? ((LanguageFileType)fileType).getLanguage() : null;
+ boolean isJsonFile = language instanceof JsonLanguage;
+
+ if (!hasAccessToSymbols()) {
+ return WidgetState.getDumbModeState("JSON schema service", isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES);
+ }
+
+ JsonWidgetSuppressor[] suppressors = JsonWidgetSuppressor.EXTENSION_POINT_NAME.getExtensions();
+ if (Arrays.stream(suppressors).anyMatch(s -> s.suppressSwitcherWidget(file, myProject))) {
+ return WidgetState.HIDDEN;
+ }
+
+ Collection<VirtualFile> schemaFiles = myService.getSchemaFilesForFile(file);
+ if (schemaFiles.size() == 0) {
+ return getNoSchemaState();
+ }
+
+ if (schemaFiles.size() != 1) {
+ List<VirtualFile> onlyUserSchemas = schemaFiles.stream().filter(s -> {
+ JsonSchemaFileProvider provider = myService.getSchemaProvider(s);
+ return provider != null && provider.getSchemaType() == SchemaType.userSchema;
+ }).collect(Collectors.toList());
+ if (onlyUserSchemas.size() > 1) {
+ MyWidgetState state = new MyWidgetState(JsonSchemaConflictNotificationProvider.createMessage(schemaFiles, myService,
+ "<br/>", "Conflicting schemas:<br/>",
+ ""),
+ schemaFiles.size() + " schemas (!)", true);
+ state.setWarning(true);
+ return state;
+ }
+ schemaFiles = onlyUserSchemas;
+ if (schemaFiles.size() == 0) {
+ return getNoSchemaState();
+ }
+ }
+
+ VirtualFile schemaFile = schemaFiles.iterator().next();
+ schemaFile = ((JsonSchemaServiceImpl)myService).replaceHttpFileWithBuiltinIfNeeded(schemaFile);
+
+ String tooltip = isJsonFile ? JSON_SCHEMA_TOOLTIP : JSON_SCHEMA_TOOLTIP_OTHER_FILES;
+ String bar = isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES;
+
+ if (schemaFile instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)schemaFile).getFileInfo();
+ if (info == null) return getDownloadErrorState(null);
+
+ //noinspection EnumSwitchStatementWhichMissesCases
+ switch (info.getState()) {
+ case DOWNLOADING_NOT_STARTED:
+ addDownloadingUpdateListener(info);
+ return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile),
+ true);
+ case DOWNLOADING_IN_PROGRESS:
+ addDownloadingUpdateListener(info);
+ return new MyWidgetState("Download is scheduled or in progress", "Downloading JSON schema", false);
+ case ERROR_OCCURRED:
+ return getDownloadErrorState(info.getErrorMessage());
+ }
+ }
+
+ if (!isValidSchemaFile(schemaFile)) {
+ MyWidgetState state = new MyWidgetState("File is not a schema", "JSON schema error", true);
+ state.setWarning(true);
+ return state;
+ }
+
+ JsonSchemaFileProvider provider = myService.getSchemaProvider(schemaFile);
+ if (provider != null) {
+ final boolean preferRemoteSchemas = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas();
+ final String remoteSource = provider.getRemoteSource();
+ String providerName = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? remoteSource : provider.getPresentableName();
+ String shortName = StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema");
+ String name = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? bar + new JsonSchemaInfo(remoteSource).getDescription()
+ : (shortName.startsWith("JSON schema") ? shortName : (bar + shortName));
+ String kind = !preferRemoteSchemas && (provider.getSchemaType() == SchemaType.embeddedSchema || provider.getSchemaType() == SchemaType.schema)
+ ? " (bundled)"
+ : "";
+ return new MyWidgetState(tooltip + providerName + kind, name, true);
+ }
+
+ return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile),
+ true);
+ }
+
+ private void addDownloadingUpdateListener(@NotNull RemoteFileInfo info) {
+ info.addDownloadingListener(new FileDownloadingAdapter() {
+ @Override
+ public void fileDownloaded(@NotNull VirtualFile localFile) {
+ update();
+ }
+
+ @Override
+ public void errorOccurred(@NotNull String errorMessage) {
+ update();
+ }
+
+ @Override
+ public void downloadingCancelled() {
+ update();
+ }
+ });
+ }
+
+ private boolean isValidSchemaFile(@Nullable VirtualFile schemaFile) {
+ return schemaFile != null && myService.isSchemaFile(schemaFile) && myService.isApplicableToFile(schemaFile);
+ }
+
+ @Nullable
+ private static String extractNpmPackageName(@Nullable String path) {
+ if (path == null) return null;
+ int idx = path.indexOf("node_modules");
+ if (idx != -1) {
+ int trimIndex = idx + "node_modules".length() + 1;
+ if (trimIndex < path.length()) {
+ path = path.substring(trimIndex);
+ idx = StringUtil.indexOfAny(path, "\\/");
+ if (idx != -1) {
+ if (path.startsWith("@")) {
+ idx = StringUtil.indexOfAny(path, "\\/", idx + 1, path.length());
+ }
+ }
+
+ if (idx != -1) {
+ return path.substring(0, idx);
+ }
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static String getPresentableNameForFile(@NotNull VirtualFile schemaFile) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ return new JsonSchemaInfo(schemaFile.getUrl()).getDescription();
+ }
+
+ String nameWithoutExtension = schemaFile.getNameWithoutExtension();
+ if (!JsonSchemaInfo.isVeryDumbName(nameWithoutExtension)) return nameWithoutExtension;
+
+ String path = schemaFile.getPath();
+
+ String npmPackageName = extractNpmPackageName(path);
+ return npmPackageName != null ? npmPackageName : schemaFile.getName();
+ }
+
+ @NotNull
+ private static WidgetState getDownloadErrorState(@Nullable String message) {
+ MyWidgetState state = new MyWidgetState("Error downloading schema" + (message == null ? "" : (": <br/>" + message)),
+ "JSON schema error", true);
+ state.setWarning(true);
+ return state;
+ }
+
+ @NotNull
+ private static WidgetState getNoSchemaState() {
+ return new MyWidgetState("No JSON Schema defined", "No JSON schema", true);
+ }
+
+ @NotNull
+ private static String getSchemaFileDesc(@NotNull VirtualFile schemaFile) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ return schemaFile.getPresentableUrl();
+ }
+
+ String npmPackageName = extractNpmPackageName(schemaFile.getPath());
+ return schemaFile.getName() + (npmPackageName == null ? "" : (" (Package: " + npmPackageName + ")"));
+ }
+
+ @Nullable
+ @Override
+ protected ListPopup createPopup(DataContext context) {
+ final VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(context);
+ if (virtualFile == null) return null;
+
+ Project project = getProject();
+ if (project == null) return null;
+ WidgetState state = getWidgetState(virtualFile);
+ if (!(state instanceof MyWidgetState)) return null;
+ return doCreatePopup(virtualFile, project, ((MyWidgetState)state).isWarning());
+ }
+
+ @NotNull
+ private ListPopup doCreatePopup(@NotNull VirtualFile virtualFile, @NotNull Project project, boolean showOnlyEdit) {
+ return JsonSchemaStatusPopup.createPopup(myService, project, virtualFile, showOnlyEdit);
+ }
+
+ @Override
+ protected void registerCustomListeners() {
+ class Listener implements DumbService.DumbModeListener {
+ volatile boolean isDumbMode;
+
+ @Override
+ public void enteredDumbMode() {
+ isDumbMode = true;
+ update();
+ }
+
+ @Override
+ public void exitDumbMode() {
+ isDumbMode = false;
+ update();
+ }
+ }
+
+ Listener listener = new Listener();
+ myConnection.subscribe(DumbService.DUMB_MODE, listener);
+ }
+
+ @NotNull
+ @Override
+ protected StatusBarWidget createInstance(Project project) {
+ return new JsonSchemaStatusWidget(project);
+ }
+
+ @NotNull
+ @Override
+ public String ID() {
+ return ID;
+ }
+
+ @Override
+ public void dispose() {
+ myService.unregisterRemoteUpdateCallback(myUpdateCallback);
+ myService.unregisterResetAction(myUpdateCallback);
+ super.dispose();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java
new file mode 100644
index 00000000..f688cbe9
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java
@@ -0,0 +1,16 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.openapi.wm.StatusBarWidgetProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonSchemaStatusWidgetProvider implements StatusBarWidgetProvider {
+
+ @NotNull
+ @Override
+ public StatusBarWidget getWidget(@NotNull Project project) {
+ return new JsonSchemaStatusWidget(project);
+ }
+}
diff --git a/json/src/jsonSchema/build.xml b/json/src/jsonSchema/build.xml
new file mode 100644
index 00000000..dbf3e4af
--- /dev/null
+++ b/json/src/jsonSchema/build.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="JSON Schemas Update" default="ALL">
+ <property name="idea.project.home" value="${basedir}/../../../../"/>
+
+ <macrodef name="get_schema">
+ <attribute name="fromUrl" />
+ <attribute name="toFile" />
+
+ <sequential>
+ <get src="@{fromUrl}"
+ dest="@{toFile}" />
+ <local name="baseUrl"/>
+ <fixcrlf file="@{toFile}"/>
+ <property name="baseUrl" value="@{fromUrl}" />
+ <script language="javascript"><![CDATA[
+ var url = new java.net.URL(project.getProperty("baseUrl"));
+ var connection = url.openConnection();
+ var etag = connection.getHeaderField("ETag");
+ project.setProperty("etag", etag);
+ ]]></script>
+ <echo message="${etag}" file="@{toFile}.ETAG" />
+ </sequential>
+ </macrodef>
+
+ <target name="prettier">
+ <get_schema fromUrl="http://json.schemastore.org/prettierrc"
+ toFile="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json" />
+ <!-- Add schema ID manually -->
+ <replaceregexp file="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/prettierrc&quot;,${line.separator} &quot;definitions&quot;" />
+ </target>
+
+ <target name="tslint">
+ <get_schema fromUrl="http://json.schemastore.org/tslint"
+ toFile="${idea.project.home}/contrib/tslint/resources/tslintJsonSchema/tslint-schema.json" />
+ </target>
+
+ <target name="webpack4">
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/WebpackOptions.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpack-schema4.json" />
+ </target>
+
+ <target name="webpack4plugins">
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/BannerPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/BannerPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllReferencePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllReferencePlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/HashedModuleIdsPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/HashedModuleIdsPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/LoaderOptionsPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/LoaderOptionsPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/SourceMapDevToolPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/SourceMapDevToolPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/WatchIgnorePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/WatchIgnorePlugin.json" />
+
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/debug/ProfilingPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/debug/ProfilingPlugin.json" />
+
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/AggressiveSplittingPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/AggressiveSplittingPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/LimitChunkCountPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/LimitChunkCountPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/MinChunkSizePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/MinChunkSizePlugin.json" />
+
+ </target>
+
+ <target name="nodejs">
+ <get_schema fromUrl="http://json.schemastore.org/package"
+ toFile="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" />
+
+ <!-- add custom properties for eslint, prettier, styleling, jest, jshint, hscs -->
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;eslintConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/eslintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;prettier&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/prettierrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;stylelint&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/stylelintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jest&quot; : {&quot;type&quot;: &quot;object&quot;, &quot;$ref&quot;: &quot;https://facebook.github.io/jest/docs/configuration.html!#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jshintConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/jshintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jscsConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/jscsrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ </target>
+
+ <target name="js_schemas">
+ <get_schema fromUrl="http://json.schemastore.org/babelrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.babelrc-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/stylelintrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json" />
+ <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/stylelintrc&quot;,${line.separator}&#9;&quot;definitions&quot;" />
+ <get_schema fromUrl="http://json.schemastore.org/jsconfig"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/jsconfig-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/tsconfig"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsconfig-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/tsd"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsd-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/typings"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/typings-schema.json" />
+
+ <get_schema fromUrl="http://json.schemastore.org/eslintrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json" />
+ <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/eslintrc&quot;,${line.separator} &quot;definitions&quot;" />
+ </target>
+
+ <target name="ALL">
+ <antcall target="js_schemas" />
+ <antcall target="prettier" />
+ <antcall target="tslint" />
+ <antcall target="nodejs" />
+ <antcall target="webpack4" />
+ <antcall target="webpack4plugins" />
+ </target>
+</project> \ No newline at end of file
diff --git a/json/src/jsonSchema/schema.json b/json/src/jsonSchema/schema.json
new file mode 100644
index 00000000..048c444f
--- /dev/null
+++ b/json/src/jsonSchema/schema.json
@@ -0,0 +1,154 @@
+{
+ "id": "http://json-schema.org/draft-04/schema#",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
+ },
+ "simpleTypes": {
+ "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": {},
+ "multipleOf": {
+ "type": "number",
+ "minimum": 0,
+ "exclusiveMinimum": true
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "boolean",
+ "default": false
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxLength": { "$ref": "#/definitions/positiveInteger" },
+ "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": {
+ "anyOf": [
+ { "type": "boolean" },
+ { "$ref": "#" }
+ ],
+ "default": {}
+ },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": {}
+ },
+ "maxItems": { "$ref": "#/definitions/positiveInteger" },
+ "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxProperties": { "$ref": "#/definitions/positiveInteger" },
+ "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": {
+ "anyOf": [
+ { "type": "boolean" },
+ { "$ref": "#" }
+ ],
+ "default": {}
+ },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "enum": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "dependencies": {
+ "exclusiveMaximum": [ "maximum" ],
+ "exclusiveMinimum": [ "minimum" ]
+ },
+ "default": {}
+} \ No newline at end of file
diff --git a/json/src/jsonSchema/schema06.json b/json/src/jsonSchema/schema06.json
new file mode 100644
index 00000000..5877ae97
--- /dev/null
+++ b/json/src/jsonSchema/schema06.json
@@ -0,0 +1,158 @@
+{
+ "$schema": "http://json-schema.org/draft-06/schema#",
+ "$id": "http://json-schema.org/draft-06/schema#",
+ "title": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonNegativeIntegerDefault0": {
+ "allOf": [
+ { "$ref": "#/definitions/nonNegativeInteger" },
+ { "default": 0 }
+ ]
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "uniqueItems": true,
+ "default": []
+ }
+ },
+ "type": ["object", "boolean"],
+ "properties": {
+ "$id": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": {},
+ "examples": {
+ "type": "array",
+ "items": {}
+ },
+ "multipleOf": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "number"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "number"
+ },
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": { "$ref": "#" },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": {}
+ },
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "contains": { "$ref": "#" },
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": { "$ref": "#" },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "propertyNames": { "$ref": "#" },
+ "const": {},
+ "enum": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "format": { "type": "string" },
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "default": {}
+}
diff --git a/json/src/jsonSchema/schema07.json b/json/src/jsonSchema/schema07.json
new file mode 100644
index 00000000..539c6069
--- /dev/null
+++ b/json/src/jsonSchema/schema07.json
@@ -0,0 +1,172 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://json-schema.org/draft-07/schema#",
+ "title": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonNegativeIntegerDefault0": {
+ "allOf": [
+ { "$ref": "#/definitions/nonNegativeInteger" },
+ { "default": 0 }
+ ]
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "uniqueItems": true,
+ "default": []
+ }
+ },
+ "type": ["object", "boolean"],
+ "properties": {
+ "$id": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$comment": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": true,
+ "readOnly": {
+ "type": "boolean",
+ "default": false
+ },
+ "examples": {
+ "type": "array",
+ "items": true
+ },
+ "multipleOf": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "number"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "number"
+ },
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": { "$ref": "#" },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": true
+ },
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "contains": { "$ref": "#" },
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": { "$ref": "#" },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "propertyNames": { "format": "regex" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "propertyNames": { "$ref": "#" },
+ "const": true,
+ "enum": {
+ "type": "array",
+ "items": true,
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "format": { "type": "string" },
+ "contentMediaType": { "type": "string" },
+ "contentEncoding": { "type": "string" },
+ "if": {"$ref": "#"},
+ "then": {"$ref": "#"},
+ "else": {"$ref": "#"},
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "default": true
+}
diff --git a/json/tests/intellij.json.tests.iml b/json/tests/intellij.json.tests.iml
new file mode 100644
index 00000000..0337bee4
--- /dev/null
+++ b/json/tests/intellij.json.tests.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
+ <orderEntry type="module" module-name="intellij.json" scope="TEST" />
+ <orderEntry type="module" module-name="intellij.java.testFramework" scope="TEST" />
+ <orderEntry type="module" module-name="intellij.spellchecker" scope="TEST" />
+ <orderEntry type="module" module-name="intellij.platform.testExtensions" scope="TEST" />
+ </component>
+</module> \ No newline at end of file
diff --git a/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java b/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java
new file mode 100644
index 00000000..9416a649
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.ui.components.breadcrumbs.Crumb;
+
+import java.util.List;
+
+public class JsonBreadcrumbsTest extends JsonTestCase {
+
+ private void doTest(String... components) {
+ myFixture.configureByFile("breadcrumbs/" + getTestName(false) + ".json");
+ List<Crumb> caret = myFixture.getBreadcrumbsAtCaret();
+ assertOrderedEquals(caret.stream().map(Crumb::getText).toArray(String[]::new), components);
+ }
+
+ public void testComplexItems() {
+ doTest("foo", "bar", "0", "0", "baz");
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonCommenterTest.java b/json/tests/test/com/intellij/json/JsonCommenterTest.java
new file mode 100644
index 00000000..f26d96a7
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonCommenterTest.java
@@ -0,0 +1,32 @@
+package com.intellij.json;
+
+import com.intellij.openapi.actionSystem.IdeActions;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCommenterTest extends JsonTestCase {
+
+ private void doTest(@NotNull String actionId) {
+ myFixture.configureByFile("commenter/" + getTestName(false) + ".json");
+ myFixture.performEditorAction(actionId);
+ myFixture.checkResultByFile("commenter/" + getTestName(false) + "_after.json", true);
+ }
+
+ public void testLineComment() {
+ doTest(IdeActions.ACTION_COMMENT_LINE);
+ }
+
+ public void testLineComment2() {
+ doTest(IdeActions.ACTION_COMMENT_LINE);
+ }
+
+ public void testLineComment3() {
+ doTest(IdeActions.ACTION_COMMENT_LINE);
+ }
+
+ public void testBlockComment() {
+ doTest(IdeActions.ACTION_COMMENT_BLOCK);
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonCompletionTest.java b/json/tests/test/com/intellij/json/JsonCompletionTest.java
new file mode 100644
index 00000000..661fe34f
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonCompletionTest.java
@@ -0,0 +1,61 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.util.ArrayUtil;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCompletionTest extends JsonTestCase {
+ private static final String[] ALL_KEYWORDS = new String[]{"true", "false", "null"};
+ private static final String[] NOTHING = ArrayUtil.EMPTY_STRING_ARRAY;
+
+ private void doTest(String... variants) {
+ myFixture.testCompletionVariants("completion/" + getTestName(false) + ".json", variants);
+ }
+
+ private void doTestSingleVariant() {
+ myFixture.configureByFile("completion/" + getTestName(false) + ".json");
+ final LookupElement[] variants = myFixture.completeBasic();
+ assertNull(variants);
+ myFixture.checkResultByFile("completion/" + getTestName(false) + "_after.json" );
+ }
+
+ public void testInsideArrayElement1() {
+ doTest(ALL_KEYWORDS);
+ }
+
+ public void testInsideArrayElement2() {
+ doTest(ALL_KEYWORDS);
+ }
+
+ public void testInsidePropertyKey1() {
+ doTest(NOTHING);
+ }
+
+ public void testInsidePropertyKey2() {
+ doTest(NOTHING);
+ }
+
+ public void testInsideStringLiteral1() {
+ doTest(NOTHING);
+ }
+
+ public void testInsideStringLiteral2() {
+ doTest(NOTHING);
+ }
+
+ public void testInsidePropertyValue() {
+ doTest(ALL_KEYWORDS);
+ }
+
+ // Moved from JavaScript
+
+ public void testKeywords() {
+ doTestSingleVariant();
+ }
+
+ public void testKeywords_2() {
+ doTestSingleVariant();
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonCopyPasteTest.java b/json/tests/test/com/intellij/json/JsonCopyPasteTest.java
new file mode 100644
index 00000000..7997274c
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonCopyPasteTest.java
@@ -0,0 +1,119 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase;
+
+public class JsonCopyPasteTest extends CodeInsightFixtureTestCase {
+
+ private void doCopyPasteTest(String source, String dest, String expected, String filename1, String filename2) {
+ myFixture.configureByText(filename1, source);
+ myFixture.performEditorAction(IdeActions.ACTION_EDITOR_COPY);
+ myFixture.configureByText(filename2, dest);
+ myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE);
+ myFixture.checkResult(expected);
+ }
+
+ private void doTestFromTextToJson(String source, String dest, String expected) {
+ doCopyPasteTest(source, dest, expected, "dummy.txt", "dummy.json");
+ }
+
+ private void doTestFromJsonToText(String source, String dest, String expected) {
+ doCopyPasteTest(source, dest, expected, "dummy.json", "dummy.txt");
+ }
+
+ public void testUnescapeQuotes() {
+ doTestFromJsonToText("{\"p\": \"<selection>\\\"quoted\\\"</selection>\"}", "<caret>", "\"quoted\"");
+ }
+
+ public void testUnescapeWhitespaces() {
+ doTestFromJsonToText("{\"p\": \"<selection>lorem ipsum\\tdolor sit amet</selection>\"}", "<caret>", "lorem ipsum\tdolor sit amet");
+ }
+
+ public void testUnescapeFromPropNames() {
+ doTestFromJsonToText("{\"<selection>lorem ipsum\\tdolor sit amet</selection>\": \"foo\"}", "<caret>", "lorem ipsum\tdolor sit amet");
+ }
+
+ public void testEscapeQuotes() {
+ doTestFromTextToJson("<selection>\"quoted\"</selection>", "{\"p\": \"<caret>\"}", "{\"p\": \"\\\"quoted\\\"\"}");
+ }
+
+ public void testEscapeWhitespaces() {
+ doTestFromTextToJson("<selection>lorem ipsum\tdolor sit amet</selection>", "{\"p\": \"<caret>\"}", "{\"p\": \"lorem ipsum\\tdolor sit amet\"}");
+ }
+
+ public void testEscapeInPropNames() {
+ doTestFromTextToJson("<selection>lorem ipsum\tdolor sit amet</selection>", "{\"<caret>\": \"foo\"}", "{\"lorem ipsum\\tdolor sit amet\": \"foo\"}");
+ }
+
+ public void testAddTrailingComma() {
+ doTestFromTextToJson("<selection>\"x\": 5</selection>", "{\"q\": \"foo\"<caret>}", "{\"q\": \"foo\",\"x\": 5}");
+ }
+
+ public void testAddRemoveTrailingComma() {
+ doTestFromTextToJson("<selection>\"x\": 5,</selection>", "{\"q\": \"foo\"<caret>}", "{\"q\": \"foo\",\"x\": 5}");
+ }
+
+ public void testAddLeadingComma() {
+ doTestFromTextToJson("<selection>\"x\": 5</selection>", "{<caret>\"q\": \"foo\"}", "{\"x\": 5,\"q\": \"foo\"}");
+ }
+
+ public void testDoNothingLeadingComma() {
+ doTestFromTextToJson("<selection>\"x\": 5,</selection>", "{<caret>\"q\": \"foo\"}", "{\"x\": 5,\"q\": \"foo\"}");
+ }
+
+ public void testCommasMidPropList() {
+ doTestFromTextToJson("<selection>\"x\": 5</selection>", "{\"s\": \"foo\",<caret>\"q\": \"foo\"}", "{\"s\": \"foo\",\"x\": 5,\"q\": \"foo\"}");
+ }
+
+ public void testAddTrailingCommaArray() {
+ doTestFromTextToJson("<selection>\"a\"</selection>", "[4<caret>]", "[4,\"a\"]");
+ }
+
+ public void testAddRemoveTrailingCommaArray() {
+ doTestFromTextToJson("<selection>\"a\",</selection>", "[4<caret>]", "[4,\"a\"]");
+ }
+
+ public void testAddLeadingCommaArray() {
+ doTestFromTextToJson("<selection>\"a\"</selection>", "[<caret>4]", "[\"a\",4]");
+ }
+
+ public void testCommasMidPropListArray() {
+ doTestFromTextToJson("<selection>\"a\"</selection>", "[3,<caret>4]", "[3,\"a\",4]");
+ }
+
+ public void testWithLeadingAndTrailingWhitespaces() {
+ doTestFromTextToJson("<selection> \t \"a\": true \t </selection>", "{\"q\": 5<caret>}", "{\"q\": 5, \t \"a\": true \t }");
+ }
+
+ public void testWithLeadingAndTrailingWhitespacesArray() {
+ doTestFromTextToJson("<selection> \t \"a\" \t </selection>", "[\"q\"<caret>]", "[\"q\", \t \"a\" \t ]");
+ }
+
+ public void testWithLeadingAndTrailingWhitespacesBefore() {
+ doTestFromTextToJson("<selection> \t \"a\": true \t </selection>", "{<caret>\"q\": 5}", "{ \t \"a\": true, \t \"q\": 5}");
+ }
+
+ public void testWithLeadingAndTrailingWhitespacesArrayBefore() {
+ doTestFromTextToJson("<selection> \t \"a\" \t </selection>", "[<caret>\"q\"]", "[ \t \"a\", \t \"q\"]");
+ }
+
+ public void testTrailingNewline() {
+ doTestFromTextToJson(" \"react-dom\": \"^16.5.2\"\n", "{\n" +
+ " \"name\": \"untitled\",\n" +
+ " \"version\": \"1.0.0\",\n" +
+ " \"dependencies\": {<caret>\n" +
+ " \"react\": \"^16.5.2\",\n" +
+ " \"react-dom\": \"^16.5.2\"\n" +
+ " }\n" +
+ "}", "{\n" +
+ " \"name\": \"untitled\",\n" +
+ " \"version\": \"1.0.0\",\n" +
+ " \"dependencies\": { \"react-dom\": \"^16.5.2\",\n" +
+ "\n" +
+ " \"react\": \"^16.5.2\",\n" +
+ " \"react-dom\": \"^16.5.2\"\n" +
+ " }\n" +
+ "}");
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonEditingTest.java b/json/tests/test/com/intellij/json/JsonEditingTest.java
new file mode 100644
index 00000000..97d33adf
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonEditingTest.java
@@ -0,0 +1,76 @@
+package com.intellij.json;
+
+import com.intellij.json.formatter.JsonCodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonEditingTest extends JsonTestCase {
+
+ private void doTest(@NotNull final String characters) {
+ final String testName = "editing/" + getTestName(false);
+ myFixture.configureByFile(testName + ".json");
+ myFixture.type(characters);
+ myFixture.checkResultByFile(testName + ".after.json");
+ }
+
+ public void testContinuationIndentAfterPropertyKey() {
+ doTest("\n");
+ }
+
+ public void testContinuationIndentAfterColon() {
+ doTest("\n");
+ }
+
+ // IDEA-130594
+ public void testNormalIndentAfterPropertyWithoutComma() {
+ doTest("\n");
+ }
+
+ // WEB-13675
+ public void testIndentWithTabsWhenSmartTabEnabled() {
+ final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions();
+ final CommonCodeStyleSettings.IndentOptions oldSettings = (CommonCodeStyleSettings.IndentOptions)indentOptions.clone();
+ indentOptions.TAB_SIZE = 4;
+ indentOptions.INDENT_SIZE = 4;
+ indentOptions.USE_TAB_CHARACTER = true;
+ indentOptions.SMART_TABS = true;
+ try {
+ doTest("\n\"baz\"");
+ }
+ finally {
+ indentOptions.copyFrom(oldSettings);
+ }
+ }
+
+ // Moved from JavaScript
+
+ // WEB-11600
+ public void testEnterWhenPropertiesAlignedOnValue() {
+ doEnterTestForWeb11600();
+ }
+
+ // WEB-11600
+ public void testEnterWhenPropertiesAlignedOnValue1() {
+ doEnterTestForWeb11600();
+ }
+
+ private void doEnterTestForWeb11600() {
+ final JsonCodeStyleSettings settings = getCustomCodeStyleSettings();
+ final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions();
+
+ final int oldPropertyAlignment = settings.PROPERTY_ALIGNMENT;
+ final int oldIndentSize = indentOptions.INDENT_SIZE;
+ settings.PROPERTY_ALIGNMENT = JsonCodeStyleSettings.ALIGN_PROPERTY_ON_VALUE;
+ indentOptions.INDENT_SIZE = 4;
+ try {
+ doTest("\n");
+ }
+ finally {
+ indentOptions.INDENT_SIZE = oldIndentSize;
+ settings.PROPERTY_ALIGNMENT = oldPropertyAlignment;
+ }
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonFoldingTest.java b/json/tests/test/com/intellij/json/JsonFoldingTest.java
new file mode 100644
index 00000000..2f21918a
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonFoldingTest.java
@@ -0,0 +1,38 @@
+package com.intellij.json;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFoldingTest extends JsonTestCase {
+ private void doTest() {
+ myFixture.testFolding(getTestDataPath() + "/folding/" + getTestName(false) + ".json");
+ }
+
+
+ public void testArrayFolding() {
+ doTest();
+ }
+
+ public void testObjectFolding() {
+ doTest();
+ }
+
+ public void testCommentaries() {
+ doTest();
+ }
+
+ // Moved from JavaScript
+
+ public void testObjectLiteral2() {
+ doTest();
+ }
+
+ public void testObjectLiteral3() {
+ doTest();
+ }
+
+ public void testObjectLiteral4() {
+ doTest();
+ }
+
+}
diff --git a/json/tests/test/com/intellij/json/JsonFormattingTest.java b/json/tests/test/com/intellij/json/JsonFormattingTest.java
new file mode 100644
index 00000000..98f2a005
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonFormattingTest.java
@@ -0,0 +1,112 @@
+package com.intellij.json;
+
+import com.intellij.json.formatter.JsonCodeStyleSettings.PropertyAlignment;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.testFramework.PlatformTestUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFormattingTest extends JsonTestCase {
+
+ @Override
+ protected String getTestDataPath() {
+ return super.getTestDataPath() + "/formatting";
+ }
+
+ public void testContainerElementsAlignment() {
+ doTest();
+ }
+
+ public void testBlankLinesStripping() {
+ doTest();
+ }
+
+ public void testSpacesInsertion() {
+ doTest();
+ }
+
+ public void testDoNotThrowFailedToAlignException() {
+ getCustomCodeStyleSettings().PROPERTY_ALIGNMENT = PropertyAlignment.ALIGN_ON_VALUE.getId();
+ doTest();
+ }
+
+ public void testWrapping() {
+ getCodeStyleSettings().setRightMargin(JsonLanguage.INSTANCE, 20);
+ doTest();
+ }
+
+ // WEB-13587
+ public void testAlignPropertiesOnColon() {
+ checkPropertyAlignment(PropertyAlignment.ALIGN_ON_COLON);
+ }
+
+ // WEB-13587
+ public void testAlignPropertiesOnValue() {
+ checkPropertyAlignment(PropertyAlignment.ALIGN_ON_VALUE);
+ }
+
+ private void checkPropertyAlignment(@NotNull final PropertyAlignment alignmentType) {
+ getCustomCodeStyleSettings().PROPERTY_ALIGNMENT = alignmentType.getId();
+ doTest();
+ }
+
+ public void testChopDownArrays() {
+ getCustomCodeStyleSettings().ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM;
+ getCodeStyleSettings().setRightMargin(JsonLanguage.INSTANCE, 40);
+ doTest();
+ }
+
+ // IDEA-138902
+ public void testObjectsWithSingleProperty() {
+ doTest();
+ }
+
+ // Moved from JavaScript
+
+ public void testWeb3830() {
+ CommonCodeStyleSettings.IndentOptions options = getIndentOptions();
+ options.INDENT_SIZE = 8;
+ options.USE_TAB_CHARACTER = true;
+ options.TAB_SIZE = 8;
+ doTest();
+ }
+
+ public void testReformatJSon() {
+ getIndentOptions().INDENT_SIZE = 4;
+ doTest();
+ }
+
+ public void testReformatJSon2() {
+ getIndentOptions().INDENT_SIZE = 4;
+ doTest();
+ }
+
+ public void testRemoveTrailingCommas() {
+ doTest();
+ }
+
+ public void testReformatIncompleteJson1() { doTest();}
+
+ public void testReformatIncompleteJson2() { doTest();}
+
+ public void testIndentForElements() { doTest();}
+ public void testNoExtraNewLineByWrap() { doTest();}
+
+ public void testHugeJsonFile() {
+ // IDEA-195340 bad JSON kills IntelliJ
+ PlatformTestUtil.startPerformanceTest(getTestName(false), 20000, this::doTest).attempts(1).usesAllCPUCores().assertTiming();
+ }
+
+ private void doTest() {
+ myFixture.configureByFile(getTestName(false) + ".json");
+ WriteCommandAction.runWriteCommandAction(null, () -> {
+ CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(myFixture.getProject());
+ codeStyleManager.reformat(myFixture.getFile());
+ });
+ myFixture.checkResultByFile(getTestName(false) + "_after.json");
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonHighlightingTest.java b/json/tests/test/com/intellij/json/JsonHighlightingTest.java
new file mode 100644
index 00000000..ab7d42fa
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonHighlightingTest.java
@@ -0,0 +1,74 @@
+package com.intellij.json;
+
+import com.intellij.json.codeinsight.JsonDuplicatePropertyKeysInspection;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonHighlightingTest extends JsonHighlightingTestBase {
+
+ @Override
+ protected String getExtension() {
+ return "json";
+ }
+
+ private void enableStandardComplianceInspection(boolean checkComments, boolean checkTopLevelValues) {
+ final JsonStandardComplianceInspection inspection = new JsonStandardComplianceInspection();
+ inspection.myWarnAboutComments = checkComments;
+ inspection.myWarnAboutMultipleTopLevelValues = checkTopLevelValues;
+ myFixture.enableInspections(inspection);
+ }
+
+ public void testStringLiterals() {
+ doTest();
+ }
+
+ // IDEA-134372
+ public void testComplianceProblemsLiteralTopLevelValueIsAllowed() {
+ enableStandardComplianceInspection(true, true);
+ doTest();
+ }
+
+ // WEB-16009
+ public void testComplianceProblemsMultipleTopLevelValuesAllowed() {
+ enableStandardComplianceInspection(true, false);
+ }
+
+ public void testComplianceProblems() {
+ enableStandardComplianceInspection(true, true);
+ doTestHighlighting(false, true, true);
+ }
+
+ public void testDuplicatePropertyKeys() {
+ myFixture.enableInspections(JsonDuplicatePropertyKeysInspection.class);
+ doTestHighlighting(false, true, true);
+ }
+
+ // WEB-13600
+ public void testIncompleteFloatingPointLiteralsWithExponent() {
+ doTestHighlighting(false, false, false);
+ }
+
+ // Moved from JavaScript
+
+ public void testJSON_with_comment() {
+ enableStandardComplianceInspection(false, true);
+ doTestHighlighting(false, true, true);
+ }
+
+ public void testJSON() {
+ enableStandardComplianceInspection(true, true);
+ doTestHighlighting(false, true, true);
+ }
+
+ public void testTabInString() {
+ enableStandardComplianceInspection(true, true);
+ doTestHighlighting(false, true, true);
+ }
+
+ public void testSemanticHighlighting() {
+ // WEB-11239
+ doTest();
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java b/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java
new file mode 100644
index 00000000..623f314f
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java
@@ -0,0 +1,14 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+public abstract class JsonHighlightingTestBase extends JsonTestCase {
+ protected void doTest() {
+ doTestHighlighting(true, true, true);
+ }
+
+ protected abstract String getExtension();
+
+ protected void doTestHighlighting(boolean checkInfo, boolean checkWeakWarning, boolean checkWarning) {
+ myFixture.testHighlighting(checkWarning, checkInfo, checkWeakWarning, "/highlighting/" + getTestName(false) + "." + getExtension());
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonLexerTest.java b/json/tests/test/com/intellij/json/JsonLexerTest.java
new file mode 100644
index 00000000..b531af43
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonLexerTest.java
@@ -0,0 +1,34 @@
+package com.intellij.json;
+
+import com.intellij.lexer.Lexer;
+import com.intellij.testFramework.LexerTestCase;
+
+/**
+ * @author Konstantin.Ulitin
+ */
+public class JsonLexerTest extends LexerTestCase {
+ @Override
+ protected Lexer createLexer() {
+ return new JsonLexer();
+ }
+
+ @Override
+ protected String getDirPath() {
+ return null;
+ }
+
+ public void testEscapeSlash() {
+ // WEB-2803
+ doTest("[\"\\/\",-1,\"\\n\", 1]",
+ "[ ('[')\n" +
+ "DOUBLE_QUOTED_STRING ('\"\\/\"')\n" +
+ ", (',')\n" +
+ "NUMBER ('-1')\n" +
+ ", (',')\n" +
+ "DOUBLE_QUOTED_STRING ('\"\\n\"')\n" +
+ ", (',')\n" +
+ "WHITE_SPACE (' ')\n" +
+ "NUMBER ('1')\n" +
+ "] (']')");
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonLineMoverTest.java b/json/tests/test/com/intellij/json/JsonLineMoverTest.java
new file mode 100644
index 00000000..2deefaee
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonLineMoverTest.java
@@ -0,0 +1,92 @@
+package com.intellij.json;
+
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLineMoverTest extends JsonTestCase {
+ private void doTest(boolean down) {
+ final String testName = getTestName(false);
+
+ if (down) {
+ myFixture.configureByFile("mover/" + testName + ".json");
+ myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION);
+ myFixture.checkResultByFile("mover/" + testName + "_afterDown.json", true);
+ }
+ else {
+ myFixture.configureByFile("mover/" + testName + ".json");
+ myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION);
+ myFixture.checkResultByFile("mover/" + testName + "_afterUp.json", true);
+ }
+ }
+
+ private void doTest() {
+ doTest(false);
+ FileDocumentManager.getInstance().reloadFromDisk(myFixture.getDocument(myFixture.getFile()));
+ doTest(true);
+ }
+
+ public void testLastArrayElementMovedUp() {
+ doTest(false);
+ }
+
+ public void testLastObjectPropertyMovedUp() {
+ doTest(false);
+ }
+
+ public void testArraySelectionMovedDown() {
+ doTest(true);
+ }
+
+ public void testOutOfScopeHasProp() {
+ doTest(true);
+ }
+
+ public void testOutOfScopeNoFollowing() {
+ doTest(true);
+ }
+
+ public void testStatementSetMovedSameLevelDown() {
+ doTest(true);
+ }
+
+ public void testStatementSetMovedSameLevelUp() {
+ doTest(false);
+ }
+
+ public void testIntoScope() {
+ doTest(false);
+ }
+
+ public void testFromUpperIntoScope() {
+ doTest(true);
+ }
+
+ public void testOutsideArray() {
+ doTest(true);
+ }
+
+ public void testInsideArray() {
+ doTest(false);
+ }
+
+ public void testUpToUpper() {
+ doTest(false);
+ }
+
+ public void testObjectSelectionMovedDown() {
+ doTest(true);
+ }
+
+ public void testLineCommentariesMovedTogether() {
+ doTest(true);
+ }
+
+ // Moved from JavaScript
+
+ public void testWeb_10585() {
+ doTest();
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonLiteralApiTest.java b/json/tests/test/com/intellij/json/JsonLiteralApiTest.java
new file mode 100644
index 00000000..1f4725c5
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonLiteralApiTest.java
@@ -0,0 +1,111 @@
+package com.intellij.json;
+
+import com.intellij.json.psi.*;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLiteralApiTest extends JsonTestCase {
+
+ public static final String ALL_CHARACTER_ESCAPES = "\"\\/\b\f\n\r\t";
+
+ @NotNull
+ private JsonStringLiteral createStringLiteralFromText(@NotNull String rawText) {
+ final JsonArray jsonArray = new JsonElementGenerator(getProject()).createValue("[\n" + rawText + "\n]");
+ assertEquals(1, jsonArray.getValueList().size());
+ final JsonValue firstElement = jsonArray.getValueList().get(0);
+ assertInstanceOf(firstElement, JsonStringLiteral.class);
+ return ((JsonStringLiteral)firstElement);
+ }
+
+ @NotNull
+ private JsonStringLiteral createStringLiteralFromContent(@NotNull String unescapedContent) {
+ return new JsonElementGenerator(getProject()).createStringLiteral(unescapedContent);
+ }
+
+ private void checkFragments(@NotNull String rawStringLiteral, @NotNull Pair<TextRange, String>... fragments) {
+ final List<Pair<TextRange, String>> actual = createStringLiteralFromText(rawStringLiteral).getTextFragments();
+ final List<Pair<TextRange, String>> expected = Arrays.asList(fragments);
+ assertEquals(expected, actual);
+ for (Pair<TextRange, String> fragment : fragments) {
+ final TextRange range = fragment.getFirst();
+ final String unescaped = range.substring(rawStringLiteral);
+ if (!unescaped.contains("\\")) {
+ final String escaped = fragment.getSecond();
+ assertEquals(String.format("Bad range %s: fragment without escaping differs after decoding", range), escaped, unescaped);
+ }
+ }
+ }
+
+ @NotNull
+ private static Pair<TextRange, String> fragment(int start, int end, @NotNull String chunk) {
+ return Pair.create(new TextRange(start, end), chunk);
+ }
+
+ public void testFragmentSplittingSimpleEscapes() {
+ checkFragments("\"\\b\\f\\n\\r\\t\\\\\\/\\\"\"",
+
+ fragment(1, 3, "\b"),
+ fragment(3, 5, "\f"),
+ fragment(5, 7, "\n"),
+ fragment(7, 9, "\r"),
+ fragment(9, 11, "\t"),
+ fragment(11, 13, "\\"),
+ fragment(13, 15, "/"),
+ fragment(15, 17, "\""));
+ }
+
+ public void testFragmentSplittingIllegalEscapes() {
+ checkFragments("\"\\q\\zz\\uBEE\\u\\ ",
+
+ fragment(1, 3, "\\q"),
+ fragment(3, 5, "\\z"),
+ fragment(5, 6, "z"),
+ fragment(6, 11, "\\uBEE"),
+ fragment(11, 13, "\\u"),
+ fragment(13, 15, "\\ "));
+ }
+
+ public void testFragmentSplittingUnicodeEscapes() {
+ checkFragments("'foo\\uCAFEBABE\\u0027baz'",
+
+ fragment(1, 4, "foo"),
+ fragment(4, 10, "\\uCAFE"),
+ fragment(10, 14, "BABE"),
+ fragment(14, 20, "\\u0027"),
+ fragment(20, 23, "baz"));
+ }
+
+ public void testStringLiteralValue() {
+ checkStringContent("simple");
+ checkStringContent(ALL_CHARACTER_ESCAPES);
+ checkStringContent("\u043c\u0435\u0434\u0432\u0435\u0434\u044c");
+ }
+
+ private void checkStringContent(@NotNull String content) {
+ assertEquals(content, createStringLiteralFromContent(content).getValue());
+ }
+
+ public void testBooleanLiteralValue() {
+ final JsonElementGenerator generator = new JsonElementGenerator(getProject());
+ assertTrue(generator.<JsonBooleanLiteral>createValue("true").getValue());
+ assertFalse(generator.<JsonBooleanLiteral>createValue("false").getValue());
+ }
+
+ public void testNumberLiteralValue() {
+ final JsonElementGenerator generator = new JsonElementGenerator(getProject());
+ assertEquals(123.0, generator.<JsonNumberLiteral>createValue("123.0").getValue(), 1e-5);
+ assertEquals(0.1, generator.<JsonNumberLiteral>createValue("0.1").getValue(), 1e-5);
+ assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1e3").getValue(), 1e-5);
+ assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1e+3").getValue(), 1e-5);
+ assertEquals(1e-3, generator.<JsonNumberLiteral>createValue("1e-3").getValue(), 1e-5);
+ assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1.00e3").getValue(), 1e-5);
+ assertEquals(1.23e-3, generator.<JsonNumberLiteral>createValue("1.23e-3").getValue(), 1e-5);
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java b/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java
new file mode 100644
index 00000000..b9e4dedb
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json;
+
+import com.intellij.codeInsight.lookup.Lookup;
+import com.intellij.codeInsight.lookup.LookupManager;
+import com.intellij.codeInsight.lookup.impl.LookupImpl;
+import com.intellij.codeInsight.template.Template;
+import com.intellij.codeInsight.template.TemplateContextType;
+import com.intellij.codeInsight.template.TemplateManager;
+import com.intellij.codeInsight.template.impl.TemplateImpl;
+import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
+import com.intellij.codeInsight.template.impl.actions.ListTemplatesAction;
+import com.intellij.json.liveTemplates.JsonContextType;
+import com.intellij.json.liveTemplates.JsonInLiteralsContextType;
+import com.intellij.json.liveTemplates.JsonInPropertyKeysContextType;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.testFramework.fixtures.CodeInsightTestUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLiveTemplateTest extends JsonTestCase {
+
+ private boolean isApplicableContextUnderCaret(@NotNull String text, Class<? extends TemplateContextType> ...contextsToDisable) {
+ myFixture.configureByText(JsonFileType.INSTANCE, text);
+ final Template template = createJsonTemplate("foo", "foo", "[42]", contextsToDisable);
+ return TemplateManagerImpl.isApplicable(myFixture.getFile(), myFixture.getCaretOffset(), (TemplateImpl)template);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @NotNull
+ private Template createJsonTemplate(@NotNull String name, @NotNull String group, @NotNull String text, Class<? extends TemplateContextType> ...contextsToDisable) {
+ final TemplateManager templateManager = TemplateManager.getInstance(getProject());
+ final Template template = templateManager.createTemplate(name, group, text);
+
+ TemplateContextType context = ContainerUtil.findInstance(TemplateContextType.EP_NAME.getExtensions(), JsonContextType.class);
+ assertNotNull(context);
+ ((TemplateImpl)template).getTemplateContext().setEnabled(context, true);
+ for (Class<? extends TemplateContextType> ctx: contextsToDisable) {
+ context = ContainerUtil.findInstance(TemplateContextType.EP_NAME.getExtensions(), ctx);
+ assertNotNull(context);
+ ((TemplateImpl)template).getTemplateContext().setEnabled(context, false);
+ }
+
+ CodeInsightTestUtil.addTemplate(template, myFixture.getTestRootDisposable());
+ return template;
+ }
+
+ public void testNotExpandableInsideStringLiteral() {
+ assertFalse(isApplicableContextUnderCaret("{\"bar\": \"fo<caret>o\"}",
+ JsonInLiteralsContextType.class));
+ }
+
+ public void testNotExpandableInsidePropertyKey() {
+ assertFalse(isApplicableContextUnderCaret("{fo<caret>o: \"bar\"}",
+ JsonInPropertyKeysContextType.class));
+ }
+
+ public void testNotExpandableInsidePropertyKeyWithWhitespace() {
+ assertFalse(isApplicableContextUnderCaret("{fo<caret>o : \"bar\"}",
+ JsonInPropertyKeysContextType.class));
+ }
+
+ public void testExpandableAtTopLevel() {
+ assertTrue(isApplicableContextUnderCaret("fo<caret>o"));
+ }
+
+ public void testExpandableInObjectLiteral() {
+ assertTrue(isApplicableContextUnderCaret("{fo<caret>o}"));
+ }
+
+ public void testCustomTemplateExpansion() {
+ final String templateContent = "{\n" +
+ " \"foo\": \"$1$\"\n" +
+ "}";
+ createJsonTemplate("foo", "foo", templateContent);
+ myFixture.configureByText(JsonFileType.INSTANCE, "foo<caret>");
+ final Editor editor = myFixture.getEditor();
+ new ListTemplatesAction().actionPerformedImpl(getProject(), editor);
+ final LookupImpl lookup = (LookupImpl)LookupManager.getActiveLookup(editor);
+ assertNotNull(lookup);
+ lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR);
+ myFixture.checkResult(templateContent.replaceAll("\\$.*?\\$", ""));
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonNavigationTest.java b/json/tests/test/com/intellij/json/JsonNavigationTest.java
new file mode 100644
index 00000000..da610a6b
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonNavigationTest.java
@@ -0,0 +1,20 @@
+package com.intellij.json;
+
+import com.intellij.ide.actions.CopyReferenceAction;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.psi.PsiElement;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonNavigationTest extends JsonTestCase {
+
+ // WEB-14048
+ public void testCopyReference() {
+ myFixture.configureByFile("navigation/" + getTestName(false) + ".json");
+ final PsiElement element = myFixture.getElementAtCaret();
+ assertInstanceOf(element, JsonProperty.class);
+ final String qualifiedName = CopyReferenceAction.elementToFqn(element);
+ assertEquals("foo.bar[0][0].baz", qualifiedName);
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonParsingTest.java b/json/tests/test/com/intellij/json/JsonParsingTest.java
new file mode 100644
index 00000000..14f44e2d
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonParsingTest.java
@@ -0,0 +1,105 @@
+package com.intellij.json;
+
+import com.intellij.testFramework.ParsingTestCase;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.testFramework.TestDataPath;
+
+/**
+ * @author Mikhail Golubev
+ */
+@TestDataPath("$CONTENT_ROOT/testData/psi/")
+public class JsonParsingTest extends ParsingTestCase {
+ public JsonParsingTest() {
+ super("psi", "json", new JsonParserDefinition());
+ }
+
+ @Override
+ protected String getTestDataPath() {
+ return PlatformTestUtil.getCommunityPath() + "/json/tests/testData";
+ }
+
+ private void doTest() {
+ doTest(true);
+ }
+
+ public void testKeywords() {
+ doTest();
+ }
+
+ public void testNestedArrayLiterals() {
+ doTest();
+ }
+
+ public void testNestedObjectLiterals() {
+ doTest();
+ }
+
+ public void testTopLevelStringLiteral() {
+ doTest();
+ }
+
+ public void testStringLiterals() {
+ doTest();
+ }
+
+ public void testComments() {
+ doTest();
+ }
+
+ public void testIncompleteObjectProperties() {
+ doTest();
+ }
+
+ public void testMissingCommaBetweenArrayElements() {
+ doTest();
+ }
+
+ public void testMissingCommaBetweenObjectProperties() {
+ doTest();
+ }
+
+ public void testNonStandardPropertyKeys() {
+ doTest();
+ }
+
+ public void testTrailingCommas() {
+ doTest();
+ }
+
+ // WEB-13600
+ public void testNumberLiterals() {
+ doTest();
+ }
+
+ public void testExtendedIdentifierToken() {
+ doTest();
+ }
+
+ // Moved from JavaScript
+
+ public void testSimple1() {
+ doTest();
+ }
+
+ public void testSimple2() {
+ doTest();
+ }
+
+ public void testSimple4() {
+ doTest();
+ }
+
+ // TODO: ask about these tests
+ //public void testSimple3() {
+ // doTest();
+ //}
+ //
+ //public void testReal1() {
+ // doTest();
+ //}
+ //
+ //public void testReal2() {
+ // doTest();
+
+ //}
+}
diff --git a/json/tests/test/com/intellij/json/JsonPsiUtilTest.java b/json/tests/test/com/intellij/json/JsonPsiUtilTest.java
new file mode 100644
index 00000000..0d68f4e3
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonPsiUtilTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.json;
+
+import com.intellij.json.psi.JsonElementGenerator;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonPsiUtilTest extends JsonTestCase {
+ public void testAddPropertyInEmptyLiteral() {
+ checkAddProperty("{}", "{\"foo\": null}", true);
+ }
+
+ public void testAddPropertyInEmptyUnclosedLiteral() {
+ checkAddProperty("{", "{\"foo\": null", true);
+ }
+
+ public void testAddPropertyFirst() {
+ checkAddProperty("{\"bar\": 42}", "{\"foo\": null, \"bar\": 42}", true);
+ }
+
+ public void testAddPropertyLast() {
+ checkAddProperty("{\"bar\": 42}", "{\"bar\": 42, \"foo\": null}", false);
+ }
+
+ private void checkAddProperty(@NotNull String before, @NotNull String after, final boolean first) {
+ getCustomCodeStyleSettings().OBJECT_WRAPPING = CommonCodeStyleSettings.DO_NOT_WRAP;
+ myFixture.configureByText(JsonFileType.INSTANCE, before);
+ final PsiElement atCaret = myFixture.getFile().findElementAt(myFixture.getCaretOffset());
+ final JsonObject jsonObject = PsiTreeUtil.getParentOfType(atCaret, JsonObject.class);
+ assertNotNull(jsonObject);
+ WriteCommandAction.runWriteCommandAction(getProject(), () -> {
+ JsonPsiUtil.addProperty(jsonObject, new JsonElementGenerator(getProject()).createProperty("foo", "null"), first);
+ });
+ myFixture.checkResult(after);
+ }
+
+ public void testGetOtherSiblingPropertyNames() {
+ myFixture.configureByText(JsonFileType.INSTANCE, "{\"firs<caret>t\" : 1, \"second\" : 2}");
+ PsiElement atCaret = myFixture.getFile().findElementAt(myFixture.getCaretOffset());
+ JsonProperty property = PsiTreeUtil.getParentOfType(atCaret, JsonProperty.class);
+ assertNotNull(property);
+ assertEquals(Collections.singleton("second"), JsonPsiUtil.getOtherSiblingPropertyNames(property));
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonQuickFixTest.java b/json/tests/test/com/intellij/json/JsonQuickFixTest.java
new file mode 100644
index 00000000..54c4eda8
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonQuickFixTest.java
@@ -0,0 +1,54 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.intention.IntentionAction;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonQuickFixTest extends JsonTestCase {
+
+ protected void doTest(@NotNull Class<? extends LocalInspectionTool> inspectionClass, @NotNull String hint) {
+ final String testFileName = "quickfix/" + getTestName(false);
+ myFixture.enableInspections(inspectionClass);
+ myFixture.configureByFile(testFileName + ".json");
+ myFixture.checkHighlighting(true, false, false);
+ final IntentionAction intentionAction = myFixture.getAvailableIntention(hint);
+ assertNotNull(intentionAction);
+ myFixture.launchAction(intentionAction);
+ myFixture.checkResultByFile(testFileName + "_after.json", true);
+ }
+
+ public void testWrapInDoubleQuotes() {
+ checkWrapInDoubleQuotes("{n<caret>ull: false}", "{\"null\": false}");
+ checkWrapInDoubleQuotes("{t<caret>rue: false}", "{\"true\": false}");
+ checkWrapInDoubleQuotes("{4<caret>2: false}", "{\"42\": false}");
+ checkWrapInDoubleQuotes("{fo<caret>o: false}", "{\"foo\": false}");
+ checkWrapInDoubleQuotes("{'fo<caret>o': false}", "{\"foo\": false}");
+ checkWrapInDoubleQuotes("'foo\\\"", "\"foo\\\"\"");
+ checkWrapInDoubleQuotes("{\"foo\": b<caret>ar}", "{\"foo\": \"bar\"}");
+ checkWrapInDoubleQuotes("{\"foo\": 'b<caret>ar'}", "{\"foo\": \"bar\"}");
+ checkWrapInDoubleQuotes("'foo\\n\\'\"\\\\\\\"bar", "\"foo\\n'\\\"\\\\\\\"bar\"");
+ }
+
+ private void checkWrapInDoubleQuotes(@NotNull String before, @NotNull String after) {
+ myFixture.configureByText(JsonFileType.INSTANCE, before);
+ myFixture.enableInspections(JsonStandardComplianceInspection.class);
+ final IntentionAction intentionAction = myFixture.getAvailableIntention(JsonBundle.message("quickfix.add.double.quotes.desc"));
+ assertNotNull(intentionAction);
+ myFixture.launchAction(intentionAction);
+ myFixture.checkResult(after);
+ }
+
+ // Moved from JavaScript
+
+ public void testJSON2() {
+ doTest(JsonStandardComplianceInspection.class, JsonBundle.message("quickfix.add.double.quotes.desc"));
+ }
+
+ public void testJSON3() {
+ doTest(JsonStandardComplianceInspection.class, JsonBundle.message("quickfix.add.double.quotes.desc"));
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonRenameTest.java b/json/tests/test/com/intellij/json/JsonRenameTest.java
new file mode 100644
index 00000000..76b5eea0
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonRenameTest.java
@@ -0,0 +1,28 @@
+package com.intellij.json;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonRenameTest extends JsonTestCase {
+
+ private void doTest(final String newName) {
+ myFixture.configureByFile("rename/" + getTestName(false) + ".json");
+ myFixture.renameElementAtCaret(newName);
+ myFixture.checkResultByFile("rename/" + getTestName(false) + "_after.json");
+ }
+
+ public void testPropertyNameRequiresEscaping() {
+ doTest("\"/\\\b\f\n\r\t\0\u001B'");
+ }
+
+ // Moved from JavaScript
+
+ public void testDuplicateProperties() {
+ doTest("aaa2");
+ }
+
+ public void testDuplicatePropertiesQuotedName() {
+ doTest("\"aaa2\"");
+ }
+
+}
diff --git a/json/tests/test/com/intellij/json/JsonSmartEnterTest.java b/json/tests/test/com/intellij/json/JsonSmartEnterTest.java
new file mode 100644
index 00000000..ba44915b
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonSmartEnterTest.java
@@ -0,0 +1,43 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
+import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessors;
+import com.intellij.openapi.application.Result;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.Editor;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSmartEnterTest extends JsonTestCase {
+ public void doTest() {
+ myFixture.configureByFile("smartEnter/" + getTestName(false) + ".json");
+ final List<SmartEnterProcessor> processors = SmartEnterProcessors.INSTANCE.forKey(JsonLanguage.INSTANCE);
+ WriteCommandAction.runWriteCommandAction(myFixture.getProject(), () -> {
+ final Editor editor = myFixture.getEditor();
+ for (SmartEnterProcessor processor : processors) {
+ processor.process(myFixture.getProject(), editor, myFixture.getFile());
+ }
+ });
+ myFixture.checkResultByFile("smartEnter/" + getTestName(false) + "_after.json", true);
+ }
+
+ public void testCommaInsertedAfterArrayElement() {
+ doTest();
+ }
+
+ public void testCommaInsertedAfterProperty() {
+ doTest();
+ }
+
+ public void testCommaInsertedAfterPropertyWithMultilineValue() {
+ doTest();
+ }
+
+ public void testColonInsertedAfterPropertyKey() {
+ doTest();
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java b/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java
new file mode 100644
index 00000000..eed6d8eb
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java
@@ -0,0 +1,73 @@
+package com.intellij.json;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.extensions.AreaPicoContainer;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.spellchecker.inspections.SpellCheckingInspection;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.JsonSchemaTestProvider;
+import com.jetbrains.jsonSchema.JsonSchemaTestServiceImpl;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSpellcheckerTest extends JsonTestCase {
+
+ private void doTest() {
+ myFixture.enableInspections(SpellCheckingInspection.class);
+ myFixture.configureByFile(getTestName(false) + ".json");
+ myFixture.checkHighlighting(true, false, true);
+ }
+
+ public void testEscapeAwareness() {
+ doTest();
+ }
+
+ public void testSimple() {
+ doTest();
+ }
+
+ protected Predicate<VirtualFile> getAvailabilityPredicate() {
+ return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf(
+ JsonLanguage.INSTANCE);
+ }
+
+ public void testWithSchema() {
+ PsiFile[] files = myFixture.configureByFiles(getTestName(false) + ".json", "Schema.json");
+ JsonSchemaTestServiceImpl.setProvider(new JsonSchemaTestProvider(files[1].getVirtualFile(),
+ getAvailabilityPredicate()));
+ AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer();
+ String key = JsonSchemaService.class.getName();
+ container.unregisterComponent(key);
+ container.registerComponentImplementation(key, JsonSchemaTestServiceImpl.class);
+ Disposer.register(getTestRootDisposable(), new Disposable() {
+ @Override
+ public void dispose() {
+ JsonSchemaTestServiceImpl.setProvider(null);
+ }
+ });
+ myFixture.enableInspections(SpellCheckingInspection.class);
+ myFixture.checkHighlighting(true, false, true);
+ }
+
+ // WEB-31894 EA-117068
+ public void testAfterModificationOfStringLiteralWithEscaping() {
+ myFixture.configureByFile(getTestName(false) + ".json");
+ myFixture.enableInspections(SpellCheckingInspection.class);
+ myFixture.checkHighlighting();
+ myFixture.performEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE);
+ myFixture.performEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE);
+ myFixture.doHighlighting();
+ }
+
+ @Override
+ protected String getTestDataPath() {
+ return super.getTestDataPath() + "/spellchecker";
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonStructureViewTest.java b/json/tests/test/com/intellij/json/JsonStructureViewTest.java
new file mode 100644
index 00000000..4c70bafc
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonStructureViewTest.java
@@ -0,0 +1,165 @@
+package com.intellij.json;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.newStructureView.StructureViewComponent;
+import com.intellij.ide.util.treeView.smartTree.TreeElement;
+import com.intellij.lang.LanguageStructureViewBuilder;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.util.Consumer;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.testFramework.PlatformTestUtil.assertTreeEqual;
+import static com.intellij.testFramework.PlatformTestUtil.expandAll;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewTest extends JsonTestCase {
+
+ private void doTest(final String expected) {
+ myFixture.configureByFile(getTestName(false) + ".json");
+ myFixture.testStructureView(svc -> {
+ expandAll(svc.getTree());
+ assertTreeEqual(svc.getTree(), expected);
+ });
+ }
+
+ private void doTestTreeStructure(@NotNull Consumer<StructureViewModel> consumer) {
+ myFixture.configureByFile(getTestName(false) + ".json");
+ final StructureViewBuilder builder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(myFixture.getFile());
+ assertNotNull(builder);
+ StructureViewComponent component = null;
+ try {
+ final FileEditor editor = FileEditorManager.getInstance(getProject()).getSelectedEditor(myFixture.getFile().getVirtualFile());
+ component = (StructureViewComponent)builder.createStructureView(editor, myFixture.getProject());
+ final StructureViewModel model = component.getTreeModel();
+ consumer.consume(model);
+ }
+ finally {
+ if (component != null) {
+ Disposer.dispose(component);
+ }
+ }
+ }
+
+ public void testPropertyOrderPreserved() {
+ doTest("-PropertyOrderPreserved.json\n" +
+ " ccc\n" +
+ " bbb\n" +
+ " -aaa\n" +
+ " eee\n" +
+ " ddd\n");
+ }
+
+ // IDEA-127119
+ public void testObjectsInsideArraysAreShown() {
+ doTest("-ObjectsInsideArraysAreShown.json\n" +
+ " aProp\n" +
+ " -node1\n" +
+ " anotherProp\n" +
+ " subNode1\n" +
+ " subNode2\n" +
+ " -node2\n" +
+ " -object\n" +
+ " -subNode2\n" +
+ " -object\n" +
+ " someNode\n" +
+ " -node3\n" +
+ " -object\n" +
+ " prop1\n" +
+ " prop2\n" +
+ " someFlag\n" +
+ " -array\n" +
+ " -object\n" +
+ " arrProp1\n" +
+ " -array2\n" +
+ " -object\n" +
+ " arr2Prop1\n" +
+ " arr2Prop2\n" +
+ " -array3\n" +
+ " -object\n" +
+ " prop1\n" +
+ " prop2\n");
+ }
+
+ // IDEA-131502
+ public void testArrayNodesAreShownIfNecessary() {
+ doTest("-ArrayNodesAreShownIfNecessary.json\n" +
+ " -array\n" +
+ " -object\n" +
+ " nestedObject\n" +
+ " -array\n" +
+ " -array\n" +
+ " -object\n" +
+ " deepNestedObject\n" +
+ " -object\n" +
+ " siblingObject\n");
+ }
+
+ // IDEA-167017
+ public void testValuesOfScalarPropertiesAreShown() {
+ doTestTreeStructure(model -> {
+ final TreeElement[] children = model.getRoot().getChildren();
+ assertSize(6, children);
+ final ItemPresentation booleanNode = children[0].getPresentation();
+ assertEquals("boolean", booleanNode.getPresentableText());
+ assertEquals("true", booleanNode.getLocationString());
+
+ final ItemPresentation nullNode = children[1].getPresentation();
+ assertEquals("nullable", nullNode.getPresentableText());
+ assertEquals("null", nullNode.getLocationString());
+
+ final ItemPresentation numNode = children[2].getPresentation();
+ assertEquals("number", numNode.getPresentableText());
+ assertEquals("42", numNode.getLocationString());
+
+ final ItemPresentation stringNode = children[3].getPresentation();
+ assertEquals("string", stringNode.getPresentableText());
+ assertEquals("\"foo\"", stringNode.getLocationString());
+
+ final ItemPresentation arrayNode = children[4].getPresentation();
+ assertEquals("array", arrayNode.getPresentableText());
+ assertNull(arrayNode.getLocationString());
+
+ final ItemPresentation objectNode = children[5].getPresentation();
+ assertEquals("object", objectNode.getPresentableText());
+ assertNull(objectNode.getLocationString());
+
+ final TreeElement[] nestedChildren = children[5].getChildren();
+ assertSize(1, nestedChildren);
+ final ItemPresentation subStringNode = nestedChildren[0].getPresentation();
+ assertEquals("foo", subStringNode.getPresentableText());
+ assertEquals("\"bar\"", subStringNode.getLocationString());
+ });
+ }
+
+ // Moved from JavaScript
+
+ public void testSimpleStructure() {
+ doTestTreeStructure(model -> {
+ TreeElement[] children = model.getRoot().getChildren();
+ assertEquals(2, children.length);
+ assertEquals("aaa", children[0].getPresentation().getPresentableText());
+ assertEquals(PlatformIcons.PROPERTY_ICON, children[0].getPresentation().getIcon(false));
+ assertEquals("bbb", children[1].getPresentation().getPresentableText());
+ assertEquals(AllIcons.Json.Property_braces, children[1].getPresentation().getIcon(false));
+
+ children = children[1].getChildren();
+ assertEquals(1, children.length);
+ assertEquals("ccc", children[0].getPresentation().getPresentableText());
+ assertEquals(PlatformIcons.PROPERTY_ICON, children[0].getPresentation().getIcon(false));
+ });
+ }
+
+ @NotNull
+ @Override
+ public String getBasePath() {
+ return super.getBasePath() + "/structureView";
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonSurroundWithTest.java b/json/tests/test/com/intellij/json/JsonSurroundWithTest.java
new file mode 100644
index 00000000..061d2cf0
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonSurroundWithTest.java
@@ -0,0 +1,56 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler;
+import com.intellij.json.surroundWith.JsonWithArrayLiteralSurrounder;
+import com.intellij.json.surroundWith.JsonWithObjectLiteralSurrounder;
+import com.intellij.json.surroundWith.JsonWithQuotesSurrounder;
+import com.intellij.lang.surroundWith.Surrounder;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSurroundWithTest extends JsonTestCase {
+ private void doTest(Surrounder surrounder) {
+ myFixture.configureByFile("/surround/" + getTestName(false) + ".json");
+ SurroundWithHandler.invoke(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile(), surrounder);
+ myFixture.checkResultByFile("/surround/" + getTestName(false) + "_after.json", true);
+ }
+
+ public void testSingleValue() {
+ doTest(new JsonWithObjectLiteralSurrounder());
+ }
+
+ public void testSingleProperty() {
+ doTest(new JsonWithObjectLiteralSurrounder());
+ }
+
+ public void testMultipleProperties() {
+ doTest(new JsonWithObjectLiteralSurrounder());
+ }
+
+ public void testCannotSurroundPropertyKey() {
+ doTest(new JsonWithObjectLiteralSurrounder());
+ }
+
+ public void testArrayLiteral() {
+ doTest(new JsonWithArrayLiteralSurrounder());
+ }
+
+ public void testMultipleValuesIntoArray() {
+ doTest(new JsonWithArrayLiteralSurrounder());
+ }
+
+ public void testQuotes() {
+ doTest(new JsonWithQuotesSurrounder());
+ }
+
+ public void testMultipleValuesIntoString() {
+ doTest(new JsonWithQuotesSurrounder());
+ }
+
+ // Moved from JavaScript
+
+ public void testObjectLiteral() {
+ doTest(new JsonWithObjectLiteralSurrounder());
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonTestCase.java b/json/tests/test/com/intellij/json/JsonTestCase.java
new file mode 100644
index 00000000..b231ba81
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonTestCase.java
@@ -0,0 +1,50 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.json.formatter.JsonCodeStyleSettings;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.testFramework.TestDataPath;
+import com.intellij.testFramework.TestLoggerFactory;
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+@TestDataPath("$CONTENT_ROOT/testData")
+public abstract class JsonTestCase extends LightCodeInsightFixtureTestCase {
+ static {
+ Logger.setFactory(TestLoggerFactory.class);
+ }
+
+ @NotNull
+ protected CodeStyleSettings getCodeStyleSettings() {
+ return CodeStyleSettingsManager.getSettings(getProject());
+ }
+
+ @NotNull
+ protected CommonCodeStyleSettings getCommonCodeStyleSettings() {
+ return getCodeStyleSettings().getCommonSettings(JsonLanguage.INSTANCE);
+ }
+
+ @NotNull
+ protected JsonCodeStyleSettings getCustomCodeStyleSettings() {
+ return getCodeStyleSettings().getCustomSettings(JsonCodeStyleSettings.class);
+ }
+
+ @NotNull
+ protected CommonCodeStyleSettings.IndentOptions getIndentOptions() {
+ final CommonCodeStyleSettings.IndentOptions options = getCommonCodeStyleSettings().getIndentOptions();
+ assertNotNull(options);
+ return options;
+ }
+
+ @Override
+ @NotNull
+ public String getBasePath() {
+ return "/json/tests/testData";
+ }
+} \ No newline at end of file
diff --git a/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java b/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java
new file mode 100644
index 00000000..6fffef05
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java
@@ -0,0 +1,118 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTypingHandlingTest extends JsonTestCase {
+ private void doTestEnter(@NotNull final String before, @NotNull final String expected) {
+ doTypingTest('\n', before, expected, "json");
+ }
+ private void doTestLBrace(@NotNull final String before, @NotNull final String expected) {
+ doTypingTest('{', before, expected, "json");
+ }
+ private void doTestLBracket(@NotNull final String before, @NotNull final String expected) {
+ doTypingTest('[', before, expected, "json");
+ }
+ private void doTestQuote(@NotNull final String before, @NotNull final String expected) {
+ doTypingTest('"', before, expected, "json");
+ }
+ private void doTestColon(@NotNull final String before, @NotNull final String expected) {
+ doTypingTest(':', before, expected, "json");
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void doTypingTest(char c,
+ @NotNull String before,
+ @NotNull String expected,
+ @NotNull String extension) {
+ myFixture.configureByText("test." + extension, before);
+ myFixture.type(c);
+ myFixture.checkResult(expected);
+ }
+
+ // JsonEnterHandler
+ public void testEnterAfterProperty() {
+ doTestEnter("{\"a\": true<caret>}", "{\"a\": true,\n}");
+ }
+ public void testEnterMidProperty() {
+ doTestEnter("{\"a\": tr<caret>ue}", "{\"a\": true,\n}");
+ }
+ public void testEnterMidObjectNoFollowing() {
+ doTestEnter("{\"a\": {<caret>}}", "{\"a\": {\n \n}}");
+ }
+ public void testEnterMidObjectWithFollowing() {
+ doTestEnter("{\"a\": {<caret>} \"b\": 5}", "{\"a\": {\n \n}, \"b\": 5}");
+ }
+ public void testEnterAfterObject() {
+ doTestEnter("{\"a\": {}<caret>}", "{\"a\": {},\n}");
+ }
+
+ // JsonTypedHandler
+ public void testAutoCommaAfterLBraceInArray() {
+ doTestLBrace("[ <caret> {\"a\": 5} ]", "[ {}, {\"a\": 5} ]");
+ }
+ public void testAutoCommaAfterLBracketInArray() {
+ doTestLBracket("[ <caret> {\"a\": 5} ]", "[ [], {\"a\": 5} ]");
+ }
+ public void testAutoCommaAfterQuoteInArray() {
+ doTestQuote("[ <caret> {\"a\": 5} ]", "[ \"\", {\"a\": 5} ]");
+ }
+ public void testAutoCommaAfterLBraceInObject() {
+ doTestLBrace("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": {}, \"y\": {\"a\": 5} }");
+ }
+ public void testAutoCommaAfterLBracketInObject() {
+ doTestLBracket("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": [], \"y\": {\"a\": 5} }");
+ }
+ public void testAutoCommaAfterQuoteInObject() {
+ doTestQuote("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": \"\", \"y\": {\"a\": 5} }");
+ }
+ public void testAutoQuotesForPropName() {
+ doTestColon( "{ x<caret>}", "{\n" +
+ " \"x\": <caret>\n" +
+ "}");
+ }
+ public void testAutoQuotesForPropNameFalse1() {
+ doTestColon( "{ \"x\"<caret>}", "{ \"x\": <caret>}");
+ }
+ public void testAutoQuotesForPropNameFalse2() {
+ doTestColon( "{ \"x<caret>\"}", "{ \"x:<caret>\"}");
+ }
+ public void testAutoQuotesAndWhitespaceFollowingNewline() {
+ doTestColon("{\n" +
+ " \"a\": 5,\n" +
+ " x<caret>\n" +
+ " \"q\": 8\n" +
+ "}",
+ "{\n" +
+ " \"a\": 5,\n" +
+ " \"x\": <caret>\n" +
+ " \"q\": 8\n" +
+ "}");
+ }
+
+ public void testAutoWhitespaceErasure() {
+ myFixture.configureByText("test.json", "{a<caret>}");
+ myFixture.type(":");
+ myFixture.type(" ");
+ myFixture.checkResult("{\n" +
+ " \"a\": <caret>\n" +
+ "}");
+ }
+
+ public void testPairedSingleQuote() {
+ doTypingTest('\'', "{<caret>}", "{'<caret>'}", "json");
+ }
+ public void testPairedSingleQuote2() {
+ doTypingTest('\'', "{\n" +
+ " \"rules\": {\n" +
+ " \"at-rule-no-vendor-prefix\": null,\n" +
+ " <caret>\n" +
+ " }\n" +
+ "}", "{\n" +
+ " \"rules\": {\n" +
+ " \"at-rule-no-vendor-prefix\": null,\n" +
+ " '<caret>'\n" +
+ " }\n" +
+ "}", "json");
+ }
+}
diff --git a/json/tests/test/com/intellij/json/JsonWordSelectionTest.java b/json/tests/test/com/intellij/json/JsonWordSelectionTest.java
new file mode 100644
index 00000000..5378cf1a
--- /dev/null
+++ b/json/tests/test/com/intellij/json/JsonWordSelectionTest.java
@@ -0,0 +1,17 @@
+package com.intellij.json;
+
+import com.intellij.testFramework.fixtures.CodeInsightTestUtil;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonWordSelectionTest extends JsonTestCase {
+
+ private void doTest() {
+ CodeInsightTestUtil.doWordSelectionTestOnDirectory(myFixture, "selectWord/" + getTestName(false), "json");
+ }
+
+ public void testEscapeAwareness() {
+ doTest();
+ }
+}
diff --git a/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java b/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java
new file mode 100644
index 00000000..d4b7ffcb
--- /dev/null
+++ b/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java
@@ -0,0 +1,18 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonHighlightingTestBase;
+import com.intellij.json.json5.codeinsight.Json5StandardComplianceInspection;
+
+public class Json5HighlightingTest extends JsonHighlightingTestBase {
+
+ @Override
+ protected String getExtension() {
+ return "json5";
+ }
+
+ public void testJSON5() {
+ myFixture.enableInspections(new Json5StandardComplianceInspection());
+ doTestHighlighting(false, true, true);
+ }
+}
diff --git a/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java b/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java
new file mode 100644
index 00000000..a3fe4b95
--- /dev/null
+++ b/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java
@@ -0,0 +1,27 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.testFramework.ParsingTestCase;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.testFramework.TestDataPath;
+
+@TestDataPath("$CONTENT_ROOT/testData/psi/")
+public class Json5ParsingTest extends ParsingTestCase {
+ public Json5ParsingTest() {
+ super("psi", "json5", new Json5ParserDefinition(), new JsonParserDefinition());
+ }
+
+ @Override
+ protected String getTestDataPath() {
+ return PlatformTestUtil.getCommunityPath() + "/json/tests/testData";
+ }
+
+ private void doTest() {
+ doTest(true);
+ }
+
+ public void testJson5Syntax() {
+ doTest();
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java
new file mode 100644
index 00000000..f97a0f5f
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.documentation.DocumentationManager;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.LanguageDocumentation;
+import com.intellij.lang.documentation.DocumentationProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilBase;
+import com.jetbrains.jsonSchema.impl.JsonSchemaDocumentationProvider;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+import java.util.ArrayList;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public abstract class JsonBySchemaDocumentationBaseTest extends JsonSchemaHeavyAbstractTest {
+ protected void doTest(boolean hasDoc, String extension) throws Exception {
+ final JsonSchemaDocumentationProvider provider = new JsonSchemaDocumentationProvider();
+ LanguageDocumentation.INSTANCE.addExplicitExtension(JsonLanguage.INSTANCE, provider);
+
+ try {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final ArrayList<UserDefinedJsonSchemaConfiguration.Item> patterns = new ArrayList<>();
+ patterns.add(new UserDefinedJsonSchemaConfiguration.Item(getTestName(true) + "*", true, false));
+ addSchema(
+ new UserDefinedJsonSchemaConfiguration("testDoc", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/" + getTestName(true) + "Schema.json", false,
+ patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/" + getTestName(true) + "." + extension, "/" + getTestName(true) + "Schema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final PsiElement psiElement = PsiUtilBase.getElementAtCaret(myEditor);
+ Assert.assertNotNull(psiElement);
+ assertDocumentation(psiElement, psiElement, hasDoc);
+ }
+ });
+ } finally {
+ LanguageDocumentation.INSTANCE.removeExplicitExtension(JsonLanguage.INSTANCE, provider);
+ JsonSchemaTestServiceImpl.setProvider(null);
+ }
+ }
+
+ protected void assertDocumentation(@NotNull PsiElement docElement, @NotNull PsiElement context, boolean shouldHaveDoc) {
+ DocumentationProvider documentationProvider = DocumentationManager.getProviderFromElement(context);
+ String inlineDoc = documentationProvider.generateDoc(docElement, context);
+ String quickNavigate = documentationProvider.getQuickNavigateInfo(docElement, context);
+ checkExpectedDoc(shouldHaveDoc, inlineDoc, false);
+ checkExpectedDoc(shouldHaveDoc, quickNavigate, true);
+ }
+
+ private void checkExpectedDoc(boolean shouldHaveDoc, String inlineDoc, boolean preferShort) {
+ if (shouldHaveDoc) {
+ assertNotNull("inline help is null", inlineDoc);
+ }
+ else {
+ assertNull("inline help is not null", inlineDoc);
+ }
+ if (shouldHaveDoc) {
+ assertSameLinesWithFile(getTestDataPath() + "/" + getTestName(true) + (preferShort ? "_short.html" : ".html"), inlineDoc);
+ }
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java
new file mode 100644
index 00000000..ec3e0bd2
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java
@@ -0,0 +1,892 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.impl.LookupImpl;
+import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.extensions.AreaPicoContainer;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.Trinity;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+import com.jetbrains.jsonSchema.schemaFile.TestJsonSchemaMappingsProjectConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 3/28/2016.
+ */
+public class JsonSchemaCrossReferencesTest extends JsonSchemaHeavyAbstractTest {
+ private final static String BASE_PATH = "/tests/testData/jsonSchema/crossReferences";
+
+ @Override
+ protected String getBasePath() {
+ return BASE_PATH;
+ }
+
+ public void testJsonSchemaCrossReferenceCompletion() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void doCheck() {
+ checkCompletion("\"one\"", "\"two\"");
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/completion.json", "/baseSchema.json", "/inheritedSchema.json");
+ }
+
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration base =
+ new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList());
+ addSchema(base);
+
+ final UserDefinedJsonSchemaConfiguration inherited
+ = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/inheritedSchema.json", false,
+ Collections
+ .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false))
+ );
+
+ addSchema(inherited);
+ }
+ });
+ }
+
+ private void checkCompletion(String... strings) {
+ assertStringItems(strings);
+
+ LookupImpl lookup = getActiveLookup();
+ if (lookup != null) lookup.hide();
+ JsonSchemaService.Impl.get(getProject()).reset();
+ doHighlighting();
+ complete();
+ assertStringItems(strings);
+ }
+
+ public void testRefreshSchemaCompletionSimpleVariant() throws Exception {
+ skeleton(new Callback() {
+ private String myModuleDir;
+
+ @Override
+ public void registerSchemes() {
+ myModuleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration base =
+ new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/basePropertiesSchema.json", false,
+ Collections
+ .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false))
+ );
+ addSchema(base);
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/baseCompletion.json", "/basePropertiesSchema.json");
+ }
+
+ @Override
+ public void doCheck() throws Exception {
+ final VirtualFile moduleFile = getProject().getBaseDir().findChild(myModuleDir);
+ assertNotNull(moduleFile);
+ checkSchemaCompletion(moduleFile, "basePropertiesSchema.json", false);
+ }
+ });
+ }
+
+ public void testJsonSchemaCrossReferenceCompletionWithSchemaEditing() throws Exception {
+ skeleton(new Callback() {
+ private String myModuleDir;
+
+ @Override
+ public void registerSchemes() {
+ myModuleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration base =
+ new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/baseSchema.json", false, Collections.emptyList());
+ addSchema(base);
+
+ final UserDefinedJsonSchemaConfiguration inherited
+ = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/inheritedSchema.json", false,
+ Collections
+ .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false))
+ );
+
+ addSchema(inherited);
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/completion.json", "/baseSchema.json", "/inheritedSchema.json");
+ }
+
+ @Override
+ public void doCheck() throws Exception {
+ final VirtualFile moduleFile = getProject().getBaseDir().findChild(myModuleDir);
+ assertNotNull(moduleFile);
+ checkSchemaCompletion(moduleFile, "baseSchema.json", true);
+ }
+ });
+ }
+
+ private void checkSchemaCompletion(VirtualFile moduleFile, final String fileName, boolean delayAfterUpdate) throws InterruptedException {
+ doHighlighting();
+ complete();
+ assertStringItems("\"one\"", "\"two\"");
+
+ final VirtualFile baseFile = moduleFile.findChild(fileName);
+ Assert.assertNotNull(baseFile);
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ Document document = fileDocumentManager.getDocument(baseFile);
+ Assert.assertNotNull(document);
+ String str = "\"enum\": [\"one\", \"two\"]";
+ int start = document.getText().indexOf(str);
+ Assert.assertTrue(start > 0);
+
+ ApplicationManager.getApplication().runWriteAction(() -> {
+ document.replaceString(start, start + str.length(), "\"enum\": [\"one1\", \"two1\"]");
+ fileDocumentManager.saveAllDocuments();
+ });
+ LookupImpl lookup = getActiveLookup();
+ if (lookup != null) lookup.hide();
+ JsonSchemaService.Impl.get(getProject()).reset();
+
+ if (delayAfterUpdate) {
+ // give time for vfs callbacks to finish
+ Thread.sleep(400);
+ }
+
+ doHighlighting();
+ complete();
+ assertStringItems("\"one1\"", "\"two1\"");
+
+ lookup = getActiveLookup();
+ if (lookup != null) lookup.hide();
+ JsonSchemaService.Impl.get(getProject()).reset();
+ doHighlighting();
+ complete();
+ assertStringItems("\"one1\"", "\"two1\"");
+ }
+
+ public void testJsonSchemaRefsCrossResolve() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiReference referenceAt = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(referenceAt);
+ final PsiElement resolve = referenceAt.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("{\n" +
+ " \"type\": \"string\",\n" +
+ " \"enum\": [\"one\", \"two\"]\n" +
+ " }", resolve.getText());
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/referencingSchema.json", "/localRefSchema.json");
+ }
+
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration base =
+ new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/localRefSchema.json", false, Collections.emptyList());
+ addSchema(base);
+
+ final UserDefinedJsonSchemaConfiguration inherited
+ = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referencingSchema.json", false,
+ Collections.emptyList()
+ );
+
+ addSchema(inherited);
+ }
+ });
+ }
+
+ public void testJsonSchemaGlobalRefsCrossResolve() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+
+ AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer();
+ final String key = JsonSchemaMappingsProjectConfiguration.class.getName();
+ container.unregisterComponent(key);
+ container.registerComponentImplementation(key, TestJsonSchemaMappingsProjectConfiguration.class);
+
+ final UserDefinedJsonSchemaConfiguration inherited
+ = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referencingGlobalSchema.json", false,
+ Collections.emptyList()
+ );
+
+ addSchema(inherited);
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/referencingGlobalSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiReference referenceAt = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(referenceAt);
+ final PsiElement resolve = referenceAt.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertTrue(StringUtil.equalsIgnoreWhitespaces("{\n" +
+ " \"type\": \"array\",\n" +
+ " \"minItems\": 1,\n" +
+ " \"uniqueItems\": true\n" +
+ " }", resolve.getText()));
+ }
+ });
+ }
+
+ public void testJson2SchemaPropertyResolve() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final UserDefinedJsonSchemaConfiguration inherited
+ = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/basePropertiesSchema.json", false,
+ Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false))
+ );
+
+ addSchema(inherited);
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/testFileForBaseProperties.json", "/basePropertiesSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("basePropertiesSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("\"baseEnum\"", resolve.getText());
+ }
+ });
+ }
+
+ public void testFindRefInOtherFile() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/refToDefinitionInFileSchema.json", false,
+ Collections.emptyList()));
+ addSchema(
+ new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/definitionsSchema.json", false, Collections.emptyList()));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/refToDefinitionInFileSchema.json", "/definitionsSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiReference referenceAt = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(referenceAt);
+ final PsiElement resolve = referenceAt.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("definitionsSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("{\"type\": \"object\"}", resolve.getText());
+ }
+ });
+ }
+
+ public void testFindRefToOtherFile() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(
+ new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/refToOtherFileSchema.json", false, Collections.emptyList()
+ ));
+ addSchema(
+ new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/definitionsSchema.json", false, Collections.emptyList()
+ ));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/refToOtherFileSchema.json", "/definitionsSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiReference referenceAt = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(referenceAt);
+ final PsiElement resolve = referenceAt.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("definitionsSchema.json", resolve.getContainingFile().getName());
+ }
+ });
+ }
+
+ public void testNavigateToPropertyDefinitionInPackageJsonSchema() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("package.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/packageJsonSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/package.json", "/packageJsonSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final String text = myFile.getText();
+ final int indexOf = text.indexOf("dependencies");
+ assertTrue(indexOf > 0);
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, indexOf);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("packageJsonSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("\"dependencies\"", resolve.getText());
+ }
+ });
+ }
+
+ public void testNavigateToPropertyDefinitionNestedDefinitions() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("testNestedDefinitionsNavigation.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/nestedDefinitionsSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/testNestedDefinitionsNavigation.json", "/nestedDefinitionsSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("nestedDefinitionsSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("\"definitions\"", resolve.getText());
+ }
+ });
+ }
+
+ public void testNavigateToAllOfOneOfDefinitions() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("testNestedAllOfOneOfDefinitions.json", true, false));
+ addSchema(
+ new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/nestedAllOfOneOfDefinitionsSchema.json", false, patterns
+ ));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/testNestedAllOfOneOfDefinitions.json", "/nestedAllOfOneOfDefinitionsSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("nestedAllOfOneOfDefinitionsSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("\"begriff\"", resolve.getText());
+ }
+ });
+ }
+
+ public void testNestedAllOneAnyWithInheritanceNavigation() throws Exception {
+ final String prefix = "nestedAllOneAnyWithInheritance/";
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList()));
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("testNavigation.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, prefix + "testNavigation.json", prefix + "baseSchema.json", prefix + "referentSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("baseSchema.json", resolve.getContainingFile().getName());
+ Assert.assertEquals("\"findMe\"", resolve.getText());
+ }
+ });
+ }
+
+ public void testNestedAllOneAnyWithInheritanceCompletion() throws Exception {
+ final String prefix = "nestedAllOneAnyWithInheritance/";
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList()));
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("testCompletion.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, prefix + "testCompletion.json", prefix + "baseSchema.json", prefix + "referentSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checkCompletion("1", "2");
+ }
+ });
+ }
+
+ public void testNestedAllOneAnyWithInheritanceHighlighting() throws Exception {
+ final String prefix = "nestedAllOneAnyWithInheritance/";
+ enableInspectionTool(new JsonSchemaComplianceInspection());
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList()));
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("testHighlighting.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, prefix + "testHighlighting.json", prefix + "baseSchema.json", prefix + "referentSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ doDoTest(true, false);
+ }
+ });
+ }
+
+ public void testNavigateToDefinitionByRef() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/withReferenceToDefinitionSchema.json", false,
+ Collections.emptyList()
+ ));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "withReferenceToDefinitionSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiReference referenceAt = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(referenceAt);
+ final PsiElement resolve = referenceAt.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("{\n" +
+ " \"enum\": [1,4,8]\n" +
+ " }", resolve.getText());
+ final PsiElement parent = resolve.getParent();
+ Assert.assertTrue(parent instanceof JsonProperty);
+ final JsonValue value = ((JsonProperty)parent).getValue();
+ Assert.assertTrue(value instanceof JsonObject);
+ final JsonProperty anEnum = ((JsonObject)value).findProperty("enum");
+ Assert.assertNotNull(anEnum);
+ Assert.assertEquals("[1,4,8]", anEnum.getValue().getText());
+ }
+ });
+ }
+
+ public void testCompletionInsideSchemaDefinition() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one",
+ JsonSchemaVersion.SCHEMA_4, moduleDir + "/completionInsideSchemaDefinition.json", false,
+ Collections.emptyList()));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "completionInsideSchemaDefinition.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final Set<String> strings = Arrays.stream(myItems).map(LookupElement::getLookupString).collect(Collectors.toSet());
+ Assert.assertTrue(strings.contains("\"enum\""));
+ Assert.assertTrue(strings.contains("\"exclusiveMinimum\""));
+ Assert.assertTrue(strings.contains("\"description\""));
+ }
+ });
+ }
+
+ public void testNavigateFromSchemaDefinitionToMainSchema() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one",
+ JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/navigateFromSchemaDefinitionToMainSchema.json", false,
+ Collections.emptyList()));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "navigateFromSchemaDefinitionToMainSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ int offset = getCaretOffset();
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("\"properties\"", resolve.getText());
+ final PsiElement parent = resolve.getParent();
+ Assert.assertTrue(parent instanceof JsonProperty);
+ Assert.assertEquals("schema.json", resolve.getContainingFile().getName());
+ }
+ });
+ }
+
+ public void testNavigateToRefInsideMainSchema() {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(myProject);
+ final List<JsonSchemaFileProvider> providers = new JsonSchemaProjectSelfProviderFactory().getProviders(myProject);
+ Assert.assertEquals(JsonSchemaProjectSelfProviderFactory.TOTAL_PROVIDERS, providers.size());
+ for (JsonSchemaFileProvider provider: providers) {
+ final VirtualFile mainSchema = provider.getSchemaFile();
+ assertNotNull(mainSchema);
+ assertTrue(service.isSchemaFile(mainSchema));
+
+ final PsiFile psi = PsiManager.getInstance(myProject).findFile(mainSchema);
+ Assert.assertNotNull(psi);
+ Assert.assertTrue(psi instanceof JsonFile);
+ final JsonValue top = ((JsonFile)psi).getTopLevelValue();
+ final JsonObject obj = ObjectUtils.tryCast(top, JsonObject.class);
+ Assert.assertNotNull(obj);
+ final JsonProperty properties = obj.findProperty("properties");
+ final JsonObject propObj = ObjectUtils.tryCast(properties.getValue(), JsonObject.class);
+ final JsonProperty maxLength = propObj.findProperty("maxLength");
+ final JsonObject value = ObjectUtils.tryCast(maxLength.getValue(), JsonObject.class);
+ Assert.assertNotNull(value);
+ final JsonProperty ref = value.findProperty("$ref");
+ Assert.assertNotNull(ref);
+ final JsonStringLiteral literal = ObjectUtils.tryCast(ref.getValue(), JsonStringLiteral.class);
+ Assert.assertNotNull(literal);
+
+ final PsiReference reference = psi.findReferenceAt(literal.getTextRange().getEndOffset() - 1);
+ Assert.assertNotNull(reference);
+ String positiveOrNonNegative = ((JsonSchemaProjectSelfProviderFactory.MyJsonSchemaFileProvider)provider).isSchemaV4()
+ ? "positiveInteger"
+ : "nonNegativeInteger";
+ Assert.assertEquals("#/definitions/" + positiveOrNonNegative, reference.getCanonicalText());
+ final PsiElement resolve = reference.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertTrue(StringUtil.equalsIgnoreWhitespaces("{\n" +
+ " \"type\": \"integer\",\n" +
+ " \"minimum\": 0\n" +
+ " }", resolve.getText()));
+ Assert.assertTrue(resolve.getParent() instanceof JsonProperty);
+ Assert.assertEquals(positiveOrNonNegative, ((JsonProperty)resolve.getParent()).getName());
+ }
+ }
+
+ public void testNavigateToDefinitionByRefInFileWithIncorrectReference() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/withIncorrectReferenceSchema.json", false,
+ Collections.emptyList()
+ ));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "withIncorrectReferenceSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final String midia = "{\n" +
+ " \"properties\": {\n" +
+ " \"mittel\" : {\n" +
+ " \"type\": [\"integer\", \"boolean\"],\n" +
+ " \"description\": \"this is found!\",\n" +
+ " \"enum\": [1,2, false]\n" +
+ " }\n" +
+ " }\n" +
+ " }";
+ checkNavigationTo(midia, "midia", getCaretOffset(), JsonSchemaObject.DEFINITIONS, true);
+ }
+ });
+ }
+
+ private int getCaretOffset() {
+ return myEditor.getCaretModel().getPrimaryCaret().getOffset();
+ }
+
+ private void checkNavigationTo(@NotNull String resolvedText, @NotNull String name, int offset, @NotNull String base, boolean isReference) {
+ final PsiElement resolve = isReference
+ ? myFile.findReferenceAt(offset).resolve()
+ : GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals(resolvedText, resolve.getText());
+ final PsiElement parent = resolve.getParent();
+ Assert.assertTrue(parent instanceof JsonProperty);
+ Assert.assertEquals(name, ((JsonProperty)parent).getName());
+ Assert.assertTrue(parent.getParent().getParent() instanceof JsonProperty);
+ Assert.assertEquals(base, ((JsonProperty)parent.getParent().getParent()).getName());
+ }
+
+ public void testInsideCycledSchemaNavigation() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/insideCycledSchemaNavigationSchema.json",
+ false, Collections.emptyList()));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "insideCycledSchemaNavigationSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checkNavigationTo("{\n" +
+ " \"$ref\": \"#/definitions/one\"\n" +
+ " }", "all", getCaretOffset(), JsonSchemaObject.DEFINITIONS, true);
+ }
+ });
+ }
+
+ public void testNavigationIntoCycledSchema() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/cycledSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "testNavigationIntoCycled.json", "cycledSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checkNavigationTo("\"bbb\"", "bbb", getCaretOffset(), JsonSchemaObject.PROPERTIES, false);
+ }
+ });
+ }
+
+ public void testNavigationWithCompositeDefinitionsObject() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/navigationWithCompositeDefinitionsObjectSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "navigationWithCompositeDefinitionsObjectSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final Collection<JsonStringLiteral> strings = PsiTreeUtil.findChildrenOfType(myFile, JsonStringLiteral.class);
+ final List<JsonStringLiteral> list = strings.stream()
+ .filter(expression -> expression.getText().contains("#/definitions")).collect(Collectors.toList());
+ Assert.assertEquals(3, list.size());
+ list.forEach(literal -> checkNavigationTo("{\n" +
+ " \"type\": \"object\",\n" +
+ " \"properties\": {\n" +
+ " \"id\": {\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"range\": {\n" +
+ " \"type\": \"string\"\n" +
+ " }\n" +
+ " }\n" +
+ " }", "cycle.schema", literal.getTextRange().getEndOffset() - 1,
+ JsonSchemaObject.DEFINITIONS, true));
+ }
+ });
+ }
+
+ public void testNavigationIntoWithCompositeDefinitionsObject() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/navigationWithCompositeDefinitionsObjectSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "navigationIntoWithCompositeDefinitionsObjectSchema.json",
+ "navigationWithCompositeDefinitionsObjectSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checkNavigationTo("\"id\"", "id", getCaretOffset(), JsonSchemaObject.PROPERTIES, false);
+ }
+ });
+ }
+
+ public void testCompletionWithRootRef() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/cycledWithRootRefSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "completionWithRootRef.json", "cycledWithRootRefSchema.json");
+ complete();
+ }
+
+ @Override
+ public void doCheck() {
+ checkCompletion("\"id\"", "\"testProp\"");
+ }
+ });
+ }
+
+ public void testResolveByValuesCombinations() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList(
+ new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false));
+ addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/ResolveByValuesCombinationsSchema.json", false, patterns));
+ }
+
+ @Override
+ public void configureFiles() throws Exception {
+ configureByFile("ResolveByValuesCombinationsSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ final List<Trinity<String, String, String>> variants = ContainerUtil.list(
+ Trinity.create("yes", "barkling", "dog"),
+ Trinity.create("yes", "meowing", "cat"),
+ Trinity.create("yes", "crowling", "mouse"),
+ Trinity.create("not", "apparel", "schrank"),
+ Trinity.create("not", "dinner", "tisch"),
+ Trinity.create("not", "rest", "sessel")
+ );
+ variants.forEach(
+ t -> {
+ final PsiFile file = configureByText(JsonFileType.INSTANCE, String.format("{\"alive\":\"%s\",\n" +
+ "\"feature\":\"%s\"}", t.getFirst(), t.getSecond()), "json");
+ final JsonFile jsonFile = ObjectUtils.tryCast(file, JsonFile.class);
+ Assert.assertNotNull(jsonFile);
+ final JsonObject top = ObjectUtils.tryCast(jsonFile.getTopLevelValue(), JsonObject.class);
+ Assert.assertNotNull(top);
+
+ TextRange range = top.findProperty("alive").getNameElement().getTextRange();
+ checkNavigationToSchemaVariant("alive", range.getStartOffset() + 1, t.getThird());
+
+ range = top.findProperty("feature").getNameElement().getTextRange();
+ checkNavigationToSchemaVariant("feature", range.getStartOffset() + 1, t.getThird());
+ }
+ );
+ }
+ });
+ }
+
+ private void checkNavigationToSchemaVariant(@NotNull String name, int offset, @NotNull String parentPropertyName) {
+ final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset);
+ Assert.assertEquals("\"" + name + "\"", resolve.getText());
+ final PsiElement parent = resolve.getParent();
+ Assert.assertTrue(parent instanceof JsonProperty);
+ Assert.assertEquals(name, ((JsonProperty)parent).getName());
+ Assert.assertTrue(parent.getParent().getParent() instanceof JsonProperty);
+ final JsonProperty props = (JsonProperty)parent.getParent().getParent();
+ Assert.assertEquals("properties", props.getName());
+ final JsonProperty parentProperty = ObjectUtils.tryCast(props.getParent().getParent(), JsonProperty.class);
+ Assert.assertNotNull(parentProperty);
+ Assert.assertEquals(parentPropertyName, parentProperty.getName());
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java
new file mode 100644
index 00000000..227d8e66
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java
@@ -0,0 +1,33 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+public class JsonSchemaDocumentationTest extends JsonBySchemaDocumentationBaseTest {
+ @Override
+ protected String getBasePath() {
+ return "/tests/testData/jsonSchema/documentation";
+ }
+
+ public void testSimple() throws Exception {
+ doTest(true, "json");
+ }
+
+ public void testSecondLevel() throws Exception {
+ doTest(true, "json");
+ }
+
+ public void testCheckEscaping() throws Exception {
+ doTest(true, "json");
+ }
+
+ public void testWithDefinition() throws Exception {
+ doTest(true, "json");
+ }
+
+ public void testWithTitleInDefinition() throws Exception {
+ doTest(true, "json");
+ }
+
+ public void testHtmlDescription() throws Exception {
+ doTest(true, "json");
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java
new file mode 100644
index 00000000..7780f856
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java
@@ -0,0 +1,87 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.completion.CompletionTestCase;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.application.ex.PathManagerEx;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Irina.Chernushina on 12/5/2016.
+ */
+public abstract class JsonSchemaHeavyAbstractTest extends CompletionTestCase {
+ private Map<String, UserDefinedJsonSchemaConfiguration> mySchemas;
+ protected boolean myDoCompletion = true;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ //WriteCommandAction.runWriteCommandAction(getProject(), () -> myFileTypeManager.associatePattern(JsonSchemaFileType.INSTANCE, "*Schema.json"));
+ mySchemas = new HashMap<>();
+ myDoCompletion = true;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ try {
+ //WriteCommandAction.runWriteCommandAction(getProject(), () -> myFileTypeManager.removeAssociatedExtension(JsonSchemaFileType.INSTANCE, "*Schema.json"));
+ final JsonSchemaMappingsProjectConfiguration instance = JsonSchemaMappingsProjectConfiguration.getInstance(getProject());
+ instance.setState(Collections.emptyMap());
+ } finally {
+ super.tearDown();
+ }
+ }
+
+ @Override
+ public String getTestDataPath() {
+ PathManagerEx.TestDataLookupStrategy strategy = PathManagerEx.guessTestDataLookupStrategy();
+ if (strategy.equals(PathManagerEx.TestDataLookupStrategy.COMMUNITY)) {
+ return PathManager.getHomePath() + "/json" + getBasePath() + "/";
+ }
+ return PathManager.getHomePath() + "/community/json" + getBasePath() + "/";
+ }
+
+ protected abstract String getBasePath();
+
+ protected void skeleton(@NotNull final Callback callback) throws Exception {
+ callback.configureFiles();
+ callback.registerSchemes();
+ JsonSchemaMappingsProjectConfiguration.getInstance(getProject()).setState(mySchemas);
+ JsonSchemaService.Impl.get(getProject()).reset();
+ doHighlighting();
+ if (myDoCompletion) complete();
+ callback.doCheck();
+ }
+
+ @NotNull
+ protected static String getModuleDir(@NotNull final Project project) {
+ String moduleDir = null;
+ VirtualFile[] children = project.getBaseDir().getChildren();
+ for (VirtualFile child : children) {
+ if (child.isDirectory()) {
+ moduleDir = child.getName();
+ break;
+ }
+ }
+ Assert.assertNotNull(moduleDir);
+ return moduleDir;
+ }
+
+ protected interface Callback {
+ void registerSchemes();
+ void configureFiles() throws Exception;
+ void doCheck() throws Exception;
+ }
+
+ protected void addSchema(@NotNull final UserDefinedJsonSchemaConfiguration schema) {
+ mySchemas.put(schema.getName(), schema);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java
new file mode 100644
index 00000000..42cdf7ed
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java
@@ -0,0 +1,1051 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInspection.InspectionProfileEntry;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+import org.intellij.lang.annotations.Language;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 9/21/2015.
+ */
+public class JsonSchemaHighlightingTest extends JsonSchemaHighlightingTestBase {
+ @Override
+ protected String getTestDataPath() {
+ return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema/highlighting";
+ }
+
+ @Override
+ protected String getTestFileName() {
+ return "config.json";
+ }
+
+ @Override
+ protected InspectionProfileEntry getInspectionProfile() {
+ return new JsonSchemaComplianceInspection();
+ }
+
+ @Override
+ protected Predicate<VirtualFile> getAvailabilityPredicate() {
+ return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf(
+ JsonLanguage.INSTANCE);
+ }
+
+ public void testNumberMultipleWrong() throws Exception {
+ doTest("{ \"properties\": { \"prop\": {\"type\": \"number\", \"multipleOf\": 2}}}",
+ "{ \"prop\": <warning descr=\"Is not multiple of 2\">3</warning>}");
+ }
+
+ public void testNumberMultipleCorrect() throws Exception {
+ doTest("{ \"properties\": { \"prop\": {\"type\": \"number\", \"multipleOf\": 2}}}", "{ \"prop\": 4}");
+ }
+
+ public void testNumberMinMax() throws Exception {
+ doTest("{ \"properties\": { \"prop\": {\n" +
+ " \"type\": \"number\",\n" +
+ " \"minimum\": 0,\n" +
+ " \"maximum\": 100,\n" +
+ " \"exclusiveMaximum\": true\n" +
+ "}}}", "{ \"prop\": 14}");
+ }
+
+ public void testEnum() throws Exception {
+ @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"enum\": [1,2,3,\"18\"]}}}";
+ doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3, \\\"18\\\"\">18</warning>}");
+ doTest(schema, "{\"prop\": 2}");
+ doTest(schema, "{\"prop\": \"18\"}");
+ doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3, \\\"18\\\"\">\"2\"</warning>}");
+ }
+
+ public void testSimpleString() throws Exception {
+ @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"type\": \"string\", \"minLength\": 2, \"maxLength\": 3}}}";
+ doTest(schema, "{\"prop\": <warning descr=\"String is shorter than 2\">\"s\"</warning>}");
+ doTest(schema, "{\"prop\": \"sh\"}");
+ doTest(schema, "{\"prop\": \"sho\"}");
+ doTest(schema, "{\"prop\": <warning descr=\"String is longer than 3\">\"shor\"</warning>}");
+ }
+
+ public void testArray() throws Exception {
+ @Language("JSON") final String schema = schema("{\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": {\n" +
+ " \"type\": \"number\", \"minimum\": 18" +
+ " }\n" +
+ "}");
+ doTest(schema, "{\"prop\": [101, 102]}");
+ doTest(schema, "{\"prop\": [<warning descr=\"Less than a minimum 18\">16</warning>]}");
+ doTest(schema, "{\"prop\": [<warning descr=\"Type is not allowed. Expected: number.\">\"test\"</warning>]}");
+ }
+
+ public void testTopLevelArray() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": {\n" +
+ " \"type\": \"number\", \"minimum\": 18" +
+ " }\n" +
+ "}";
+ doTest(schema, "[101, 102]");
+ }
+
+ public void testTopLevelObjectArray() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": {\n" +
+ " \"type\": \"object\", \"properties\": {\"a\": {\"type\": \"number\"}}" +
+ " }\n" +
+ "}";
+ doTest(schema, "[{\"a\": <warning descr=\"Type is not allowed. Expected: number.\">true</warning>}]");
+ doTest(schema, "[{\"a\": 18}]");
+ }
+
+ public void testArrayTuples1() throws Exception {
+ @Language("JSON") final String schema = schema("{\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": [{\n" +
+ " \"type\": \"number\", \"minimum\": 18" +
+ " }, {\"type\" : \"string\"}]\n" +
+ "}");
+ doTest(schema, "{\"prop\": [101, <warning descr=\"Type is not allowed. Expected: string.\">102</warning>]}");
+ doTest(schema, "{\"prop\": [101, \"102\"]}");
+ doTest(schema, "{\"prop\": [101, \"102\", \"additional\"]}");
+
+ @Language("JSON") final String schema2 = schema("{\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": [{\n" +
+ " \"type\": \"number\", \"minimum\": 18" +
+ " }, {\"type\" : \"string\"}],\n" +
+ "\"additionalItems\": false}");
+ doTest(schema2, "{\"prop\": [101, \"102\", <warning descr=\"Additional items are not allowed\">\"additional\"</warning>]}");
+ }
+
+ public void testArrayLength() throws Exception {
+ @Language("JSON") final String schema = schema("{\"type\": \"array\", \"minItems\": 2, \"maxItems\": 3}");
+ doTest(schema, "{\"prop\": <warning descr=\"Array is shorter than 2\">[]</warning>}");
+ doTest(schema, "{\"prop\": [1,2]}");
+ doTest(schema, "{\"prop\": <warning descr=\"Array is longer than 3\">[1,2,3,4]</warning>}");
+ }
+
+ public void testArrayUnique() throws Exception {
+ @Language("JSON") final String schema = schema("{\"type\": \"array\", \"uniqueItems\": true}");
+ doTest(schema, "{\"prop\": [1,2]}");
+ doTest(schema, "{\"prop\": [<warning descr=\"Item is not unique\">1</warning>,2, \"test\", <warning descr=\"Item is not unique\">1</warning>]}");
+ }
+
+ public void testMetadataIsOk() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"title\" : \"Match anything\",\n" +
+ " \"description\" : \"This is a schema that matches anything.\",\n" +
+ " \"default\" : \"Default value\"\n" +
+ "}";
+ doTest(schema, "{\"anything\": 1}");
+ }
+
+ public void testRequiredField() throws Exception {
+ @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"a\": {}, \"b\": {}}, \"required\": [\"a\"]}";
+ doTest(schema, "{\"a\": 11}");
+ doTest(schema, "{\"a\": 1, \"b\": true}");
+ doTest(schema, "<warning descr=\"Missing required property 'a'\">{\"b\": \"alarm\"}</warning>");
+ }
+
+ public void testInnerRequired() throws Exception {
+ @Language("JSON") final String schema = schema("{\"type\": \"object\", \"properties\": {\"a\": {}, \"b\": {}}, \"required\": [\"a\"]}");
+ doTest(schema, "{\"prop\": {\"a\": 11}}");
+ doTest(schema, "{\"prop\": {\"a\": 1, \"b\": true}}");
+ doTest(schema, "{\"prop\": <warning descr=\"Missing required property 'a'\">{\"b\": \"alarm\"}</warning>}");
+ }
+
+ public void testUseDefinition() throws Exception {
+ @Language("JSON") final String schema = "{\"definitions\": {\"address\": {\"type\": \"object\", \"properties\": {\"street\": {\"type\": \"string\"}," +
+ "\"house\": {\"type\": \"integer\"}}}}," +
+ "\"type\": \"object\", \"properties\": {" +
+ "\"home\": {\"$ref\": \"#/definitions/address\"}, " +
+ "\"office\": {\"$ref\": \"#/definitions/address\"}" +
+ "}}";
+ doTest(schema, "{\"home\": {\"street\": \"Broadway\", \"house\": 11}}");
+ doTest(schema, "{\"home\": {\"street\": \"Broadway\", \"house\": <warning descr=\"Type is not allowed. Expected: integer.\">\"unknown\"</warning>}," +
+ "\"office\": {\"street\": <warning descr=\"Type is not allowed. Expected: string.\">5</warning>}}");
+ }
+
+ public void testAdditionalPropertiesAllowed() throws Exception {
+ @Language("JSON") final String schema = schema("{}");
+ doTest(schema, "{\"prop\": {}, \"someStuff\": 20}");
+ }
+
+ public void testAdditionalPropertiesDisabled() throws Exception {
+ @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"prop\": {}}, \"additionalProperties\": false}";
+ // not sure abt inner object
+ doTest(schema, "{\"prop\": {}, <warning descr=\"Property 'someStuff' is not allowed\">\"someStuff\": 20</warning>}");
+ }
+
+ public void testAdditionalPropertiesSchema() throws Exception {
+ @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"a\": {}}," +
+ "\"additionalProperties\": {\"type\": \"string\"}}";
+ doTest(schema, "{\"a\" : 18, \"b\": \"wall\", \"c\": <warning descr=\"Type is not allowed. Expected: string.\">11</warning>}");
+ }
+
+ public void testMinMaxProperties() throws Exception {
+ @Language("JSON") final String schema = "{\"type\": \"object\", \"minProperties\": 1, \"maxProperties\": 2}";
+ doTest(schema, "<warning descr=\"Number of properties is less than 1\">{}</warning>");
+ doTest(schema, "{\"a\": 1}");
+ doTest(schema, "<warning descr=\"Number of properties is greater than 2\">{\"a\": 1, \"b\": 22, \"c\": 33}</warning>");
+ }
+
+ public void testOneOf() throws Exception {
+ final List<String> subSchemas = new ArrayList<>();
+ subSchemas.add("{\"type\": \"string\"}");
+ subSchemas.add("{\"type\": \"boolean\"}");
+ @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}");
+ doTest(schema, "{\"prop\": \"abc\"}");
+ doTest(schema, "{\"prop\": true}");
+ doTest(schema, "{\"prop\": <warning descr=\"Type is not allowed. Expected one of: boolean, string.\">11</warning>}");
+ }
+
+ public void testOneOfForTwoMatches() throws Exception {
+ final List<String> subSchemas = new ArrayList<>();
+ subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"b\"]}");
+ subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"c\"]}");
+ @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}");
+ doTest(schema, "{\"prop\": \"b\"}");
+ doTest(schema, "{\"prop\": \"c\"}");
+ doTest(schema, "{\"prop\": <warning descr=\"Validates to more than one variant\">\"a\"</warning>}");
+ }
+
+ public void testOneOfSelectError() throws Exception {
+ final List<String> subSchemas = new ArrayList<>();
+ subSchemas.add("{\"type\": \"string\",\n" +
+ " \"enum\": [\n" +
+ " \"off\", \"warn\", \"error\"\n" +
+ " ]}");
+ subSchemas.add("{\"type\": \"integer\"}");
+ @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}");
+ doTest(schema, "{\"prop\": \"off\"}");
+ doTest(schema, "{\"prop\": 12}");
+ doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: \\\"off\\\", \\\"warn\\\", \\\"error\\\"\">\"wrong\"</warning>}");
+ }
+
+ public void testAnyOf() throws Exception {
+ final List<String> subSchemas = new ArrayList<>();
+ subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"b\"]}");
+ subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"c\"]}");
+ @Language("JSON") final String schema = schema("{\"anyOf\": [" + StringUtil.join(subSchemas, ", ") + "]}");
+ doTest(schema, "{\"prop\": \"b\"}");
+ doTest(schema, "{\"prop\": \"c\"}");
+ doTest(schema, "{\"prop\": \"a\"}");
+ doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: \\\"a\\\", \\\"b\\\", \\\"c\\\"\">\"d\"</warning>}");
+ }
+
+ public void testAllOf() throws Exception {
+ final List<String> subSchemas = new ArrayList<>();
+ subSchemas.add("{\"type\": \"integer\", \"multipleOf\": 2}");
+ subSchemas.add("{\"enum\": [1,2,3]}");
+ @Language("JSON") final String schema = schema("{\"allOf\": [" + StringUtil.join(subSchemas, ", ") + "]}");
+ doTest(schema, "{\"prop\": <warning descr=\"Is not multiple of 2\">1</warning>}");
+ doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3\">4</warning>}");
+ doTest(schema, "{\"prop\": 2}");
+ }
+
+ public void testObjectInArray() throws Exception {
+ @Language("JSON") final String schema = schema("{\"type\": \"array\", \"items\": {\"type\": \"object\"," +
+ "\"properties\": {" +
+ "\"innerType\":{}, \"innerValue\":{}" +
+ "}, \"additionalProperties\": false" +
+ "}}");
+ doTest(schema, "{\"prop\": [{\"innerType\":{}, <warning descr=\"Property 'alien' is not allowed\">\"alien\":{}</warning>}]}");
+ }
+
+ public void testObjectDeeperInArray() throws Exception {
+ final String innerTypeSchema = "{\"properties\": {\"only\": {}}, \"additionalProperties\": false}";
+ @Language("JSON") final String schema = schema("{\"type\": \"array\", \"items\": {\"type\": \"object\"," +
+ "\"properties\": {" +
+ "\"innerType\":" + innerTypeSchema +
+ "}, \"additionalProperties\": false" +
+ "}}");
+ doTest(schema,
+ "{\"prop\": [{\"innerType\":{\"only\": true, <warning descr=\"Property 'hidden' is not allowed\">\"hidden\": false</warning>}}]}");
+ }
+
+ public void testInnerObjectPropValueInArray() throws Exception {
+ @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"type\": \"array\", \"items\": {\"enum\": [1,2,3]}}}}";
+ doTest(schema, "{\"prop\": [1,3]}");
+ doTest(schema, "{\"prop\": [<warning descr=\"Value should be one of: 1, 2, 3\">\"out\"</warning>]}");
+ }
+
+ public void testAllOfProperties() throws Exception {
+ @Language("JSON") final String schema = "{\"allOf\": [{\"type\": \"object\", \"properties\": {\"first\": {}}}," +
+ " {\"properties\": {\"second\": {\"enum\": [33,44]}}}], \"additionalProperties\": false}";
+ doTest(schema, "{\"first\": {}, \"second\": <warning descr=\"Value should be one of: 33, 44\">null</warning>}");
+ doTest(schema, "{\"first\": {}, \"second\": 44, <warning descr=\"Property 'other' is not allowed\">\"other\": 15</warning>}");
+ doTest(schema, "{\"first\": {}, \"second\": <warning descr=\"Value should be one of: 33, 44\">12</warning>}");
+ }
+
+ public void testWithWaySelection() throws Exception {
+ final String subSchema1 = "{\"enum\": [1,2,3,4,5]}";
+ final String subSchema2 = "{\"type\": \"array\", \"items\": {\"properties\": {\"kilo\": {}}, \"additionalProperties\": false}}";
+ @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"oneOf\": [" + subSchema1 + ", " + subSchema2 + "]}}}";
+ //doTest(schema, "{\"prop\": [{\"kilo\": 20}]}");
+ //doTest(schema, "{\"prop\": 5}");
+ doTest(schema, "{\"prop\": [{<warning descr=\"Property 'foxtrot' is not allowed\">\"foxtrot\": 15</warning>, \"kilo\": 20}]}");
+ }
+
+ public void testIntegerTypeWithMinMax() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/integerTypeWithMinMax_schema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/integerTypeWithMinMax.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testOneOf1() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOf1.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testOneOf2() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOf2.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testAnyOnePropertySelection() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOnePropertySelectionSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOnePropertySelection.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testAnyOneTypeSelection() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOneTypeSelectionSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOneTypeSelection.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testOneOfWithEmptyPropertyValue() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfWithEmptyPropertyValue.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testCycledSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledSchema.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testWithRootRefCycledSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledWithRootRefSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledWithRootRefSchema.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testCycledWithRootRefInNotSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledWithRootRefInNotSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledWithRootRefInNotSchema.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testPatternPropertiesHighlighting() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"patternProperties\": {\n" +
+ " \"^A\" : {\n" +
+ " \"type\": \"number\"\n" +
+ " },\n" +
+ " \"B\": {\n" +
+ " \"type\": \"boolean\"\n" +
+ " },\n" +
+ " \"C\": {\n" +
+ " \"enum\": [\"test\", \"em\"]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\n" +
+ " \"Abezjana\": 2,\n" +
+ " \"Auto\": <warning descr=\"Type is not allowed. Expected: number.\">\"no\"</warning>,\n" +
+ " \"BAe\": <warning descr=\"Type is not allowed. Expected: boolean.\">22</warning>,\n" +
+ " \"Boloto\": <warning descr=\"Type is not allowed. Expected: boolean.\">2</warning>,\n" +
+ " \"Cyan\": <warning descr=\"Value should be one of: \\\"test\\\", \\\"em\\\"\">\"me\"</warning>\n" +
+ "}");
+ }
+
+ public void testPatternPropertiesFromIssue() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"type\": \"object\",\n" +
+ " \"additionalProperties\": false,\n" +
+ " \"patternProperties\": {\n" +
+ " \"p[0-9]\": {\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"a[0-9]\": {\n" +
+ " \"enum\": [\"auto!\"]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\n" +
+ " \"p1\": <warning descr=\"Type is not allowed. Expected: string.\">1</warning>,\n" +
+ " \"p2\": \"3\",\n" +
+ " \"a2\": \"auto!\",\n" +
+ " \"a1\": <warning descr=\"Value should be one of: \\\"auto!\\\"\">\"moto!\"</warning>\n" +
+ "}");
+ }
+
+ public void testPatternForPropertyValue() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"withPattern\": {\n" +
+ " \"pattern\": \"p[0-9]\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ final String correctText = "{\n" +
+ " \"withPattern\": \"p1\"\n" +
+ "}";
+ final String wrongText = "{\n" +
+ " \"withPattern\": <warning descr=\"String is violating the pattern: 'p[0-9]'\">\"wrong\"</warning>\n" +
+ "}";
+ doTest(schema, correctText);
+ doTest(schema, wrongText);
+ }
+
+ public void testPatternWithSpecialEscapedSymbols() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"withPattern\": {\n" +
+ " \"pattern\": \"^\\\\d{4}\\\\-(0?[1-9]|1[012])\\\\-(0?[1-9]|[12][0-9]|3[01])$\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ @Language("JSON") final String correctText = "{\n" +
+ " \"withPattern\": \"1234-11-11\"\n" +
+ "}";
+ final String wrongText = "{\n" +
+ " \"withPattern\": <warning descr=\"String is violating the pattern: '^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$'\">\"wrong\"</warning>\n" +
+ "}";
+ doTest(schema, correctText);
+ doTest(schema, wrongText);
+ }
+
+ public void testRootObjectRedefinedAdditionalPropertiesForbidden() throws Exception {
+ doTest(rootObjectRedefinedSchema(), "{<warning descr=\"Property 'a' is not allowed\">\"a\": true</warning>," +
+ "\"r1\": \"allowed!\"}");
+ }
+
+ public void testNumberOfSameNamedPropertiesCorrectlyChecked() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"size\": {\n" +
+ " \"type\": \"object\",\n" +
+ " \"minProperties\": 2,\n" +
+ " \"maxProperties\": 3,\n" +
+ " \"properties\": {\n" +
+ " \"a\": {\n" +
+ " \"type\": \"boolean\"\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\n" +
+ " \"size\": {\n" +
+ " \"a\": <warning descr=\"Type is not allowed. Expected: boolean.\">1</warning>," +
+ " \"b\":3, \"c\": 4, " +
+ "\"a\": <warning descr=\"Type is not allowed. Expected: boolean.\">5</warning>\n" +
+ " }\n" +
+ "}");
+ doTest(schema, "{\n" +
+ " \"size\": <warning descr=\"Number of properties is greater than 3\">{\n" +
+ " \"a\": true," +
+ " \"b\":3, \"c\": 4, " +
+ "\"a\": false\n" +
+ " }</warning>\n" +
+ "}");
+ }
+
+ public void testManyDuplicatesInArray() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"array\":{\n" +
+ " \"type\": \"array\",\n" +
+ " \"uniqueItems\": true\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\"array\": [<warning descr=\"Item is not unique\">1</warning>," +
+ "<warning descr=\"Item is not unique\">1</warning>," +
+ "<warning descr=\"Item is not unique\">1</warning>," +
+ "<warning descr=\"Item is not unique\">2</warning>," +
+ "<warning descr=\"Item is not unique\">2</warning>," +
+ "5," +
+ "<warning descr=\"Item is not unique\">3</warning>," +
+ "<warning descr=\"Item is not unique\">3</warning>]}");
+ }
+
+ public void testPropertyValueAlsoHighlightedIfPatternIsInvalid() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"withPattern\": {\n" +
+ " \"pattern\": \"^[]$\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ final String text = "{\"withPattern\":" +
+ " <warning descr=\"Can not check string by pattern because of error: Unclosed character class near index 3\n^[]$\n ^\">\"(124)555-4216\"</warning>}";
+ doTest(schema, text);
+ }
+
+ public void testNotSchema() throws Exception {
+ @Language("JSON") final String schema = "{\"properties\": {\n" +
+ " \"not_type\": { \"not\": { \"type\": \"string\" } }\n" +
+ " }}";
+ doTest(schema, "{\"not_type\": <warning descr=\"Validates against 'not' schema\">\"wrong\"</warning>}");
+ }
+
+ public void testNotSchemaCombinedWithNormal() throws Exception {
+ @Language("JSON") final String schema = "{\"properties\": {\n" +
+ " \"not_type\": {\n" +
+ " \"pattern\": \"^[a-z]*[0-5]*$\",\n" +
+ " \"not\": { \"pattern\": \"^[a-z]{1}[0-5]$\" }\n" +
+ " }\n" +
+ " }}";
+ doTest(schema, "{\"not_type\": \"va4\"}");
+ doTest(schema, "{\"not_type\": <warning descr=\"Validates against 'not' schema\">\"a4\"</warning>}");
+ doTest(schema, "{\"not_type\": <warning descr=\"String is violating the pattern: '^[a-z]*[0-5]*$'\">\"4a4\"</warning>}");
+ }
+
+ public void testDoNotMarkOneOfThatDiffersWithFormat() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ "\n" +
+ " \"properties\": {\n" +
+ " \"withFormat\": {\n" +
+ " \"type\": \"string\"," +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"format\":\"hostname\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"format\": \"ip4\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\"withFormat\": \"localhost\"}");
+ }
+
+ public void testAcceptSchemaWithoutType() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ "\n" +
+ " \"properties\": {\n" +
+ " \"withFormat\": {\n" +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"format\":\"hostname\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"format\": \"ip4\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\"withFormat\": \"localhost\"}");
+ }
+
+ public void testArrayItemReference() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"type\": \"integer\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"$ref\": \"#/items/0\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ doTest(schema, "[1, 2]");
+ doTest(schema, "[1, <warning>\"foo\"</warning>]");
+ }
+
+ public void testArrayReference() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"definitions\": {\n" +
+ " \"options\": {\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": {\n" +
+ " \"type\": \"number\"\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"items\":{\n" +
+ " \"$ref\": \"#/definitions/options/items\"\n" +
+ " }\n" +
+ " \n" +
+ "}";
+ doTest(schema, "[2, 3 ,4]");
+ doTest(schema, "[2, <warning>\"3\"</warning>]");
+ }
+
+ public void testSelfArrayReferenceDoesNotThrowSOE() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"$ref\": \"#/items/0\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ doTest(schema, "[]");
+ }
+
+ public void testValidateAdditionalItems() throws Exception {
+ @Language("JSON") final String schema = "{\n" +
+ " \"definitions\": {\n" +
+ " \"options\": {\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": {\n" +
+ " \"type\": \"number\"\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"type\": \"boolean\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"boolean\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"additionalItems\": {\n" +
+ " \"$ref\": \"#/definitions/options/items\"\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "[true, true]");
+ doTest(schema, "[true, true, 1, 2, 3]");
+ doTest(schema, "[true, true, 1, <warning>\"2\"</warning>]");
+ }
+
+ public static String rootObjectRedefinedSchema() {
+ return "{\n" +
+ " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" +
+ " \"type\": \"object\",\n" +
+ " \"$ref\" : \"#/definitions/root\",\n" +
+ " \"definitions\": {\n" +
+ " \"root\" : {\n" +
+ " \"type\": \"object\",\n" +
+ " \"additionalProperties\": false,\n" +
+ " \"properties\": {\n" +
+ " \"r1\": {\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"r2\": {\n" +
+ " \"type\": \"string\"\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ "}\n";
+ }
+
+ static String schema(final String s) {
+ return "{\"type\": \"object\", \"properties\": {\"prop\": " + s + "}}";
+ }
+
+ public void testExclusiveMinMaxV6() throws Exception {
+ @Language("JSON") String exclusiveMinSchema = "{\"properties\": {\"prop\": {\"exclusiveMinimum\": 3}}}";
+ doTest(exclusiveMinSchema, "{\"prop\": <warning>2</warning>}");
+ doTest(exclusiveMinSchema, "{\"prop\": <warning>3</warning>}");
+ doTest(exclusiveMinSchema, "{\"prop\": 4}");
+
+ @Language("JSON") String exclusiveMaxSchema = "{\"properties\": {\"prop\": {\"exclusiveMaximum\": 3}}}";
+ doTest(exclusiveMaxSchema, "{\"prop\": 2}");
+ doTest(exclusiveMaxSchema, "{\"prop\": <warning>3</warning>}");
+ doTest(exclusiveMaxSchema, "{\"prop\": <warning>4</warning>}");
+ }
+
+ public void testPropertyNamesV6() throws Exception {
+ doTest("{\"propertyNames\": {\"minLength\": 7}}", "{<warning>\"prop\"</warning>: 2}");
+ doTest("{\"properties\": {\"prop\": {\"propertyNames\": {\"minLength\": 7}}}}", "{\"prop\": {<warning>\"qq\"</warning>: 7}}");
+ }
+
+ public void testContainsV6() throws Exception {
+ @Language("JSON") String schema = "{\"properties\": {\"prop\": {\"type\": \"array\", \"contains\": {\"type\": \"number\"}}}}";
+ doTest(schema, "{\"prop\": <warning>[{}, \"a\", true]</warning>}");
+ doTest(schema, "{\"prop\": [{}, \"a\", 1, true]}");
+ }
+
+ public void testConstV6() throws Exception {
+ @Language("JSON") String schema = "{\"properties\": {\"prop\": {\"type\": \"string\", \"const\": \"foo\"}}}";
+ doTest(schema, "{\"prop\": <warning>\"a\"</warning>}");
+ doTest(schema, "{\"prop\": <warning>5</warning>}");
+ doTest(schema, "{\"prop\": \"foo\"}");
+ }
+
+ public void testIfThenElseV7() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"if\": {\n" +
+ " \"properties\": {\n" +
+ " \"a\": {\n" +
+ " \"type\": \"string\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"a\"]\n" +
+ " },\n" +
+ " \"then\": {\n" +
+ " \"properties\": {\n" +
+ " \"b\": {\n" +
+ " \"type\": \"number\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"b\"]\n" +
+ " },\n" +
+ " \"else\": {\n" +
+ " \"properties\": {\n" +
+ " \"c\": {\n" +
+ " \"type\": \"boolean\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"c\"]\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "<warning>{}</warning>");
+ doTest(schema, "{\"c\": <warning>5</warning>}");
+ doTest(schema, "{\"c\": true}");
+ doTest(schema, "<warning>{\"a\": 5, \"b\": 5}</warning>");
+ doTest(schema, "{\"a\": 5, \"c\": <warning>5</warning>}");
+ doTest(schema, "{\"a\": 5, \"c\": true}");
+ doTest(schema, "<warning>{\"a\": \"a\", \"c\": true}</warning>");
+ doTest(schema, "{\"a\": \"a\", \"b\": <warning>true</warning>}");
+ doTest(schema, "{\"a\": \"a\", \"b\": 5}");
+ }
+
+ public void testNestedOneOf() throws Exception {
+ @Language("JSON") String schema = "{\"type\":\"object\",\n" +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"properties\": {\n" +
+ " \"type\": {\n" +
+ " \"type\": \"string\",\n" +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"pattern\": \"(good)\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"pattern\": \"(ok)\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"properties\": {\n" +
+ " \"type\": {\n" +
+ " \"type\": \"string\",\n" +
+ " \"pattern\": \"^(fine)\"\n" +
+ " },\n" +
+ " \"extra\": {\n" +
+ " \"type\": \"string\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"type\", \"extra\"]\n" +
+ " }\n" +
+ " ]}";
+
+ doTest(schema, "{\"type\": \"good\"}");
+ doTest(schema, "{\"type\": \"ok\"}");
+ doTest(schema, "{\"type\": <warning>\"doog\"</warning>}");
+ doTest(schema, "{\"type\": <warning>\"ko\"</warning>}");
+ }
+
+ public void testArrayRefs() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"myDefs\": {\n" +
+ " \"myArray\": [\n" +
+ " {\n" +
+ " \"type\": \"number\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"type\": \"array\",\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"$ref\": \"#/myDefs/myArray/0\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"$ref\": \"#/myDefs/myArray/1\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ doTest(schema, "[1, <warning>2</warning>]");
+ doTest(schema, "[<warning>\"1\"</warning>, <warning>2</warning>]");
+ doTest(schema, "[<warning>\"1\"</warning>, \"2\"]");
+ doTest(schema, "[1, \"2\"]");
+ }
+
+ public void testOneOfInsideAllOf() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"foo\": {\n" +
+ " \"allOf\": [\n" +
+ " {\n" +
+ " \"type\": \"object\"\n" +
+ " }, {\n" +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"type\": \"object\",\n" +
+ " \"properties\": {\n" +
+ " \"provider\": {\n" +
+ " \"enum\": [\"script\"]\n" +
+ " },\n" +
+ " \"foo21\": {}\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"object\",\n" +
+ " \"properties\": {\n" +
+ " \"provider\": {\n" +
+ " \"enum\": [\"npm\"]\n" +
+ " },\n" +
+ " \"foo11\": {}\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+
+ doTest(schema, "{\n" +
+ " \"foo\": {\n" +
+ " \"provider\": \"npm\"\n" +
+ " }\n" +
+ "}");
+
+ doTest(schema, "{\n" +
+ " \"foo\": {\n" +
+ " \"provider\": \"script\"\n" +
+ " }\n" +
+ "}");
+
+ doTest(schema, "{\n" +
+ " \"foo\": {\n" +
+ " \"provider\": <warning>\"etwasanderes\"</warning>\n" +
+ " }\n" +
+ "}");
+ }
+
+ public void testOneOfBestChoiceSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfBestChoiceSchema.json"));
+ doTest(schemaText, "{\n" +
+ " \"results\": [\n" +
+ " <warning descr=\"Missing required properties 'dateOfBirth', 'name'\">{\n" +
+ " \"type\": \"person\"\n" +
+ " }</warning>\n" +
+ " ]\n" +
+ "}");
+ }
+
+ public void testAnyOfBestChoiceSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOfBestChoiceSchema.json"));
+ doTest(schemaText, "[\n" +
+ " {\n" +
+ " \"directory\": \"/test\",\n" +
+ " \"arguments\": [\n" +
+ " \"a\"\n" +
+ " ],\n" +
+ " \"file\": <warning>\"\"</warning>\n" +
+ " }\n" +
+ "] ");
+ }
+
+ public void testComplexOneOfSchema() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/complexOneOfSchema.json"));
+ doTest(schemaText, "{\n" +
+ " \"indentation\": \"tab\"\n" +
+ " }");
+ doTest(schemaText, "{\n" +
+ " \"indentation\": <warning>\"ttab\"</warning>\n" +
+ " }");
+ }
+
+ public void testEnumCasing() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"type\": \"object\",\n" +
+ "\n" +
+ " \"properties\": {\n" +
+ " \"name\": { \"type\": \"string\", \"enum\": [\"aa\", \"bb\"] }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\n" +
+ " \"name\": \"aa\"\n" +
+ "}");
+ doTest(schema, "{\n" +
+ " \"name\": <warning>\"aA\"</warning>\n" +
+ "}");
+ }
+
+ public void testEnumArrayValue() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"foo\": {\n" +
+ " \"enum\": [ [{\"x\": 5}, [true], \"q\"] ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\"foo\": <warning>5</warning>}");
+ doTest(schema, "{\"foo\": <warning>[ ]</warning>}");
+ doTest(schema, "{\"foo\": <warning>[{\"x\": 5}]</warning>}");
+ doTest(schema, "{\"foo\": <warning>[{\"x\": 5}, true]</warning>}");
+ doTest(schema, "{\"foo\": <warning>[{\"x\": 5}, [true]]</warning>}");
+ doTest(schema, "{\"foo\": [ { \"x\" : 5 } , [ true ] , \"q\" ]}");
+ }
+
+ public void testEnumObjectValue() throws Exception {
+ @Language("JSON") String schema = "{\n" +
+ " \"properties\": {\n" +
+ " \"foo\": {\n" +
+ " \"enum\": [ {\"x\": 5} ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ doTest(schema, "{\"foo\": <warning>{}</warning>}");
+ doTest(schema, "{\"foo\": <warning>{\"x\": 4}</warning>}");
+ doTest(schema, "{\"foo\": <warning>{\"x\": true}</warning>}");
+ doTest(schema, "{\"foo\": { \r \"x\" : \t 5 \n }}");
+ }
+
+ public void testIntersectingHighlightingRanges() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/avroSchema.json"));
+ doTest(schemaText, "<warning descr=\"Missing required property 'items'\">{\n" +
+ " \"type\": \"array\"\n" +
+ "}</warning>");
+ doTest(schemaText, "{\n" +
+ " \"type\": <warning descr=\"Value should be one of: \\\"record\\\", \\\"enum\\\", \\\"array\\\", \\\"map\\\", \\\"fixed\\\"\">\"array2\"</warning>\n" +
+ "}");
+ }
+
+ public void testMissingMultipleAltPropertySets() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/avroSchema.json"));
+ doTest(schemaText, "<warning descr=\"One of the following property sets is required: properties 'type' = record, 'fields', 'name', or properties 'type' = enum, 'name', 'symbols', or properties 'type' = array, 'items', or properties 'type' = map, 'values', or properties 'type' = fixed, 'name', 'size'\">{\n" +
+ " \n" +
+ "}</warning>");
+ }
+
+ public void testValidateEnumVsPattern() throws Exception {
+ doTest("{\n" +
+ " \"oneOf\": [\n" +
+ " {\n" +
+ " \"properties\": {\n" +
+ " \"type\": {\n" +
+ " \"enum\": [\"library\"],\n" +
+ " \"pattern\": \".*\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"type\", \"name\", \"description\"]\n" +
+ " },\n" +
+ " {\n" +
+ " \"properties\": {\n" +
+ " \"type\": {\n" +
+ " \"not\": {\n" +
+ " \"enum\": [\"library\"]\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}", "{\n" +
+ " \"type\": \"project\",\n" +
+ " \"name\": \"asd\",\n" +
+ " \"description\": \"asdasdqwdqw\"\n" +
+ "}");
+ }
+
+ public void testJsonPointerEscapes() throws Exception {
+ doTest("{\n" +
+ " \"properties\": {\n" +
+ " \"q~q/q\": {\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"a\": {\n" +
+ " \"$ref\": \"#/properties/q~0q~1q\"\n" +
+ " }\n" +
+ " }\n" +
+ "}", "{\n" +
+ " \"a\": <warning>1</warning>\n" +
+ "}");
+ }
+
+ public void testOneOfMultipleBranches() throws Exception {
+ doTest("{\n" +
+ "\t\"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" +
+ "\n" +
+ "\t\"type\": \"object\",\n" +
+ "\t\"oneOf\": [\n" +
+ "\t\t{\n" +
+ "\t\t\t\"properties\": {\n" +
+ "\t\t\t\t\"startTime\": {\n" +
+ "\t\t\t\t\t\"type\": \"string\"\n" +
+ "\t\t\t\t}\n" +
+ "\t\t\t}\n" +
+ "\t\t},\n" +
+ "\t\t{\n" +
+ "\t\t\t\"properties\": {\n" +
+ "\t\t\t\t\"startTime\": {\n" +
+ "\t\t\t\t\t\"type\": \"number\"\n" +
+ "\t\t\t\t}\n" +
+ "\t\t\t}\n" +
+ "\t\t}\n" +
+ "\t]\n" +
+ "}", "{\n" +
+ " \"startTime\": <warning descr=\"Type is not allowed. Expected one of: number, string.\">null</warning>\n" +
+ "}");
+ }
+
+ public void testReferenceById() throws Exception {
+ doTest("{\n" +
+ " \"type\": \"object\",\n" +
+ "\n" +
+ " \"properties\": {\n" +
+ " \"a\": {\n" +
+ " \"$id\": \"#aa\",\n" +
+ " \"type\": \"object\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"patternProperties\": {\n" +
+ " \"aa\": {\n" +
+ " \"type\": \"object\"\n" +
+ " },\n" +
+ " \"bb\": {\n" +
+ " \"$ref\": \"#aa\"\n" +
+ " }\n" +
+ " }\n" +
+ "}", "{\n" +
+ " \"aa\": {\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"bb\": <warning>578</warning>\n" +
+ "}\n" +
+ "\n");
+ }
+
+ public void testComplicatedConditions() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/complicatedConditions_schema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/complicatedConditions.json"));
+ doTest(schemaText, inputText);
+ }
+
+ public void testExoticProps() throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/exoticPropsSchema.json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/exoticProps.json"));
+ doTest(schemaText, inputText);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java
new file mode 100644
index 00000000..89db6b4c
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java
@@ -0,0 +1,69 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase;
+import com.intellij.codeInspection.InspectionProfileEntry;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.extensions.AreaPicoContainer;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+
+public abstract class JsonSchemaHighlightingTestBase extends DaemonAnalyzerTestCase {
+
+ protected abstract String getTestFileName();
+ protected abstract InspectionProfileEntry getInspectionProfile();
+ protected abstract Predicate<VirtualFile> getAvailabilityPredicate();
+
+ protected void doTest(@Language("JSON") @NotNull final String schema, @NotNull final String text) throws Exception {
+ final PsiFile file = configureInitially(schema, text);
+ doTest(file.getVirtualFile(), true, false);
+ }
+
+ @NotNull
+ protected PsiFile configureInitially(@NotNull @Language("JSON") String schema,
+ @NotNull String text) throws Exception {
+ enableInspectionTool(getInspectionProfile());
+
+ final PsiFile file = doCreateFile(text);
+
+ registerProvider(getProject(), schema);
+ Disposer.register(getTestRootDisposable(), new Disposable() {
+ @Override
+ public void dispose() {
+ JsonSchemaTestServiceImpl.setProvider(null);
+ }
+ });
+ configureByFile(file.getVirtualFile());
+ return file;
+ }
+
+ @NotNull
+ protected PsiFile doCreateFile(@NotNull String text) throws Exception {
+ return createFile(myModule, getTestFileName(), text);
+ }
+
+ private void registerProvider(Project project, @NotNull String schema) throws IOException {
+ File dir = createTempDir("json_schema_test", true);
+ File child = new File(dir, "schema.json");
+ //noinspection ResultOfMethodCallIgnored
+ child.createNewFile();
+ FileUtil.writeToFile(child, schema);
+ VirtualFile schemaFile = getVirtualFile(child);
+ JsonSchemaTestServiceImpl.setProvider(new JsonSchemaTestProvider(schemaFile, getAvailabilityPredicate()));
+ AreaPicoContainer container = Extensions.getArea(project).getPicoContainer();
+ String key = JsonSchemaService.class.getName();
+ container.unregisterComponent(key);
+ container.registerComponentImplementation(key, JsonSchemaTestServiceImpl.class);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java
new file mode 100644
index 00000000..8380e235
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaPatternComparator;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+/**
+ * @author Irina.Chernushina on 2/17/2016.
+ */
+public class JsonSchemaPatternComparatorTest extends LightPlatformCodeInsightFixtureTestCase {
+ public void testPatterns() {
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject());
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("tes*")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("tes*"), p("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("*est")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("testwords"), p("test*words")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("testwords"), p("*test*words")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(p("*.abc"), p("*.cde")));
+ Assert.assertEquals(ThreeState.UNSURE, comparator.isSimilar(p("*.abc"), p("start.*")));
+ Assert.assertEquals(ThreeState.UNSURE, comparator.isSimilar(p("two*words"), p("circus")));
+ }
+
+ public void test2Files() {
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject());
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("test"), f("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("./test"), f("test")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(f("../test"), f("test")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(f("other"), f("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("one/../one/two"), f("one/two")));
+ }
+
+ public void test2Dirs() {
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject());
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), d("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("./test"), d("test")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("../test"), d("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d(".."), d("test")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("another"), d("test")));
+
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test/child"), d("test")));
+ }
+
+ public void testDirAndFile() {
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject());
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("test"), f("test")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), f("test/lower")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), f("./test/lower")));
+ Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d(".."), f("test/lower")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("one"), f("test/lower")));
+ Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("one"), f("test")));
+ }
+
+ private static UserDefinedJsonSchemaConfiguration.Item p(@NotNull final String p) {
+ return new UserDefinedJsonSchemaConfiguration.Item(p, true, false);
+ }
+
+ private static UserDefinedJsonSchemaConfiguration.Item d(@NotNull final String d) {
+ return new UserDefinedJsonSchemaConfiguration.Item(d, false, true);
+ }
+
+ private static UserDefinedJsonSchemaConfiguration.Item f(@NotNull final String f) {
+ return new UserDefinedJsonSchemaConfiguration.Item(f, false, false);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java
new file mode 100644
index 00000000..9aac7710
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java
@@ -0,0 +1,51 @@
+// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.ThrowableRunnable;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+
+import java.util.Collections;
+
+/**
+ * @author Irina.Chernushina on 10/9/2017.
+ */
+public class JsonSchemaPerformanceTest extends JsonSchemaHeavyAbstractTest {
+ public static final String BASE_PATH = "/tests/testData/jsonSchema/performance/";
+
+ @Override
+ protected String getBasePath() {
+ return BASE_PATH;
+ }
+
+ public void testSwaggerHighlighting() {
+ doPerformanceTest(8000, "swagger");
+ }
+
+ public void testTsLintSchema() {
+ doPerformanceTest(7000, "tslint-schema");
+ }
+
+ private void doPerformanceTest(int expectedMs, String jsonFileNameWithoutExtension) {
+ final ThrowableRunnable<Exception> test = () -> skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+ addSchema(new UserDefinedJsonSchemaConfiguration(jsonFileNameWithoutExtension, JsonSchemaVersion.SCHEMA_4,
+ moduleDir + "/" + jsonFileNameWithoutExtension + ".json", false, Collections.emptyList()));
+ myDoCompletion = false;
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/" + jsonFileNameWithoutExtension + ".json");
+ }
+
+ @Override
+ public void doCheck() {
+ doHighlighting();
+ }
+ });
+ PlatformTestUtil.startPerformanceTest(getTestName(false), expectedMs, test).attempts(1).usesAllCPUCores().assertTiming();
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java
new file mode 100644
index 00000000..b9013872
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java
@@ -0,0 +1,187 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.daemon.impl.HighlightInfo;
+import com.intellij.codeInspection.InspectionProfileEntry;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.ExpectedHighlightingData;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+
+public class JsonSchemaReSharperHighlightingTest extends JsonSchemaHighlightingTestBase {
+ @Override
+ protected String getTestDataPath() {
+ return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema/highlighting/resharper";
+ }
+
+ @Override
+ protected String getTestFileName() {
+ return "config.json";
+ }
+
+ @Override
+ protected InspectionProfileEntry getInspectionProfile() {
+ return new JsonSchemaComplianceInspection();
+ }
+
+ @Override
+ protected Predicate<VirtualFile> getAvailabilityPredicate() {
+ return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf(
+ JsonLanguage.INSTANCE);
+ }
+
+ private void doTestFiles(String file, String schema) throws Exception {
+ @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/" + schema + ".json"));
+ String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/" + file + ".json"));
+ doTest(schemaText, inputText);
+ }
+
+ @Override
+ protected void doCheckResult(@NotNull ExpectedHighlightingData data, Collection<HighlightInfo> infos, String text) {
+ data.checkResult(infos, text, getTestDataPath() + "/" + getName() + ".json");
+ }
+
+ // generated code below
+ public void test001() throws Exception {
+ doTestFiles("test001", "schema001");
+ }
+ public void test002() throws Exception {
+ doTestFiles("test002", "schema002");
+ }
+ public void test003() throws Exception {
+ doTestFiles("test003", "schema003");
+ }
+ public void test004() throws Exception {
+ doTestFiles("test004", "schema004");
+ }
+ public void test004_2() throws Exception {
+ doTestFiles("test004_2", "schema004");
+ }
+ public void test005() throws Exception {
+ doTestFiles("test005", "schema005");
+ }
+ public void test005_2() throws Exception {
+ doTestFiles("test005_2", "schema005");
+ }
+ public void test006() throws Exception {
+ doTestFiles("test006", "schema006");
+ }
+ public void test007() throws Exception {
+ doTestFiles("test007", "schema007");
+ }
+ public void test008() throws Exception {
+ doTestFiles("test008", "schema008");
+ }
+ public void test008_2() throws Exception {
+ doTestFiles("test008_2", "schema008");
+ }
+ public void test008_3() throws Exception {
+ doTestFiles("test008_3", "schema008");
+ }
+ public void test009() throws Exception {
+ doTestFiles("test009", "schema009");
+ }
+ public void test010() throws Exception {
+ doTestFiles("test010", "schema010");
+ }
+ public void test011() throws Exception {
+ doTestFiles("test011", "schema011");
+ }
+ public void test012() throws Exception {
+ doTestFiles("test012", "schema012");
+ }
+ public void test012_2() throws Exception {
+ doTestFiles("test012_2", "schema012");
+ }
+ public void test012_3() throws Exception {
+ doTestFiles("test012_3", "schema012");
+ }
+ public void test013() throws Exception {
+ doTestFiles("test013", "schema013");
+ }
+ public void test014() throws Exception {
+ doTestFiles("test014", "schema014");
+ }
+ public void test015() throws Exception {
+ doTestFiles("test015", "schema015");
+ }
+ public void test016() throws Exception {
+ doTestFiles("test016", "schema016");
+ }
+ public void test016_2() throws Exception {
+ doTestFiles("test016_2", "schema016");
+ }
+ public void test016_3() throws Exception {
+ doTestFiles("test016_3", "schema016");
+ }
+ public void test016_4() throws Exception {
+ doTestFiles("test016_4", "schema016");
+ }
+ public void test016_5() throws Exception {
+ doTestFiles("test016_5", "schema016");
+ }
+ public void test017() throws Exception {
+ doTestFiles("test017", "schema017");
+ }
+ public void test017_2() throws Exception {
+ doTestFiles("test017_2", "schema017");
+ }
+ public void test017_3() throws Exception {
+ doTestFiles("test017_3", "schema017");
+ }
+ public void test018() throws Exception {
+ doTestFiles("test018", "schema018");
+ }
+ public void test019() throws Exception {
+ doTestFiles("test019", "schema019");
+ }
+ public void test019_2() throws Exception {
+ doTestFiles("test019_2", "schema019");
+ }
+ public void test020() throws Exception {
+ doTestFiles("test020", "schema020");
+ }
+ public void test020_2() throws Exception {
+ doTestFiles("test020_2", "schema020");
+ }
+ public void test021() throws Exception {
+ doTestFiles("test021", "schema021");
+ }
+ public void test022() throws Exception {
+ doTestFiles("test022", "schema022");
+ }
+ public void test023() throws Exception {
+ doTestFiles("test023", "schema023");
+ }
+ public void test024() throws Exception {
+ doTestFiles("test024", "schema024");
+ }
+ public void test025() throws Exception {
+ doTestFiles("test025", "schema025");
+ }
+ public void test026() throws Exception {
+ doTestFiles("test026", "schema026");
+ }
+ public void _test027() throws Exception { // todo file refs cannot be resolved in tests for now
+ doTestFiles("test027", "schema027");
+ }
+ public void test028() throws Exception {
+ doTestFiles("test028", "schema028");
+ }
+ public void _test029() throws Exception { // TODO bug
+ doTestFiles("test029", "schema029");
+ }
+ public void test030() throws Exception {
+ doTestFiles("test030", "schema030");
+ }
+
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java
new file mode 100644
index 00000000..29a5cabc
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.editor.impl.DocumentImpl;
+import com.intellij.testFramework.ExpectedHighlightingData;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+
+import java.util.Collections;
+
+/**
+ * @author Irina.Chernushina on 2/9/2017.
+ */
+public class JsonSchemaSelfHighligthingTest extends JsonSchemaHeavyAbstractTest {
+ public static final String BASE_PATH = "/tests/testData/jsonSchema/selfHighlighting";
+
+ @Override
+ protected String getBasePath() {
+ return BASE_PATH;
+ }
+
+ public void testPatterns() throws Exception {
+ enableInspectionTool(new JsonSchemaComplianceInspection());
+ skeleton(new Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration pattern =
+ new UserDefinedJsonSchemaConfiguration("pattern", JsonSchemaVersion.SCHEMA_4, moduleDir + "/patternSchema.json", false, Collections.emptyList());
+ addSchema(pattern);
+ myDoCompletion = false;
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/patternSchema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checkHighlighting(new ExpectedHighlightingData(new DocumentImpl("{\n" +
+ " \"properties\": {\n" +
+ " \"withPattern\": {\n" +
+ " \"pattern\": <warning descr=\"Unclosed character class near index 3\n" +
+ "^[]$\n" +
+ " ^\">\"^[]$\"</warning>\n" +
+ " },\n" +
+ " \"everythingFine\": {\n" +
+ " \"pattern\": \"^[a]$\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"patternProperties\": {\n" +
+ " <warning descr=\"Unclosed character class near index 8\n" +
+ ".*p[0-9.*\n" +
+ " ^\">\"p[0-9<error>\"</error></warning>: {},\n" +
+ " <warning descr=\"Unclosed character class near index 8\n" +
+ ".*b[0-7.*\n" +
+ " ^\">\"b[0-7<error>\"</error></warning>: {}\n" +
+ " }\n" +
+ "}"), true, true, false, myFile));
+ }
+ });
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java
new file mode 100644
index 00000000..d89a8ae0
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java
@@ -0,0 +1,42 @@
+package com.jetbrains.jsonSchema;
+
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.SchemaType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonSchemaTestProvider implements JsonSchemaFileProvider {
+ private final VirtualFile mySchemaFile;
+ private final Predicate<? super VirtualFile> myAvailabilityPredicate;
+
+ public JsonSchemaTestProvider(VirtualFile schemaFile, Predicate<? super VirtualFile> availabilityPredicate) {
+ mySchemaFile = schemaFile;
+ myAvailabilityPredicate = availabilityPredicate;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull VirtualFile file) {
+ return myAvailabilityPredicate.apply(file);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "test";
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSchemaFile() {
+ return mySchemaFile;
+ }
+
+ @NotNull
+ @Override
+ public SchemaType getSchemaType() {
+ return SchemaType.userSchema;
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java
new file mode 100644
index 00000000..740726f7
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java
@@ -0,0 +1,39 @@
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory;
+import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+
+
+public class JsonSchemaTestServiceImpl extends JsonSchemaServiceImpl {
+
+ public static void setProvider(JsonSchemaFileProvider newProvider) {
+ provider = newProvider;
+ }
+
+ private static JsonSchemaFileProvider provider;
+
+ public JsonSchemaTestServiceImpl(@NotNull Project project) {
+ super(project);
+ }
+
+
+ @NotNull
+ @Override
+ protected JsonSchemaProviderFactory[] getProviderFactories() {
+ return new JsonSchemaProviderFactory[]{
+ new JsonSchemaProviderFactory() {
+ @NotNull
+ @Override
+ public List<JsonSchemaFileProvider> getProviders(@NotNull final Project project) {
+ return provider == null ? Collections.emptyList() : Collections.singletonList(provider);
+ }
+ }
+ };
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java
new file mode 100644
index 00000000..4274441e
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java
@@ -0,0 +1,66 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.fixes;
+
+import com.intellij.codeInspection.InspectionProfileEntry;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.Predicate;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+
+public class JsonSchemaQuickFixTest extends JsonSchemaQuickFixTestBase {
+ @Override
+ protected String getTestFileName() {
+ return "config.json";
+ }
+
+ @Override
+ protected InspectionProfileEntry getInspectionProfile() {
+ return new JsonSchemaComplianceInspection();
+ }
+
+ @Override
+ protected Predicate<VirtualFile> getAvailabilityPredicate() {
+ return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf(
+ JsonLanguage.INSTANCE);
+ }
+
+ public void testAddMissingProperty() throws Exception {
+ doTest("{\n" +
+ " \"properties\": {\n" +
+ " \"a\": {\n" +
+ " \"default\": \"q\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"a\", \"b\"]\n" +
+ "}", "<warning>{\"c\": 5}</warning>", "Add missing properties 'a', 'b'", "{\"c\": 5,\n" +
+ " \"a\": \"q\",\n" +
+ " \"b\":\n" +
+ "}");
+ }
+
+ // todo fix working with live template in test; test-only problem
+ /*public void testAddMissingStringProperty() throws Exception {
+ doTest("{\n" +
+ " \"properties\": {\n" +
+ " \"a\": {\n" +
+ " \"type\": \"string\"" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\"a\"]\n" +
+ "}", "<warning>{\"c\": 5}</warning>", "Add missing property 'a'", "{\"c\": 5,\n" +
+ " \"a\": \"<caret>\"" +
+ "\n}");
+ }*/
+
+ public void testRemoveProhibitedProperty() throws Exception {
+ doTest("{\n" +
+ " \"properties\": {\n" +
+ " \"a\": {},\n" +
+ " \"c\": {}\n" +
+ " },\n" +
+ " \"additionalProperties\": false\n" +
+ "}", "{\"a\": 5, <warning>\"b\": 6</warning>, \"c\": 7}", "Remove prohibited property 'b'", "{\"a\": 5,\n" +
+ " \"c\": 7}");
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java
new file mode 100644
index 00000000..ad395dd0
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java
@@ -0,0 +1,49 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.fixes;
+
+import com.intellij.codeInsight.EditorInfo;
+import com.intellij.codeInsight.daemon.impl.HighlightInfo;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.jsonSchema.JsonSchemaHighlightingTestBase;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+public abstract class JsonSchemaQuickFixTestBase extends JsonSchemaHighlightingTestBase {
+ protected void doTest(@Language("JSON") @NotNull String schema, @NotNull String text, String fixName, String afterFix) throws Exception {
+ PsiFile file = configureInitially(schema, text);
+ HashMap<VirtualFile, EditorInfo> map = new HashMap<>();
+ map.put(file.getVirtualFile(), new EditorInfo(file.getText()));
+ List<Editor> editors = openEditors(map);
+ Collection<HighlightInfo> infos = doDoTest(true, false);
+ PsiFile psiFile = getPsiFile(editors.get(0).getDocument());
+ findAndInvokeIntentionAction(infos, fixName, editors.get(0), psiFile);
+ String fileText = getFile().getText();
+ int caretIndex = afterFix.indexOf("<caret>");
+ if (caretIndex >= 0) {
+ int caretOffset = getEditor().getCaretModel().getOffset();
+ fileText = fileText.substring(0, caretOffset - 1) + "<caret>" + fileText.substring(caretOffset - 1);
+ }
+ assertEquals(afterFix, fileText);
+ }
+
+ @NotNull
+ @Override
+ protected PsiFile doCreateFile(@NotNull String text) throws Exception {
+ File dir = createTempDir("json_schema_test_r", true);
+ File child = new File(dir, getTestFileName());
+ //noinspection ResultOfMethodCallIgnored
+ child.createNewFile();
+ FileUtil.writeToFile(child, text);
+ VirtualFile schemaFile = getVirtualFile(child);
+ schemaFile.setWritable(true);
+ return getPsiManager().findFile(schemaFile);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java
new file mode 100644
index 00000000..22d92d93
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java
@@ -0,0 +1,41 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionTestCase;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.testFramework.EditorTestUtil;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public abstract class JsonBySchemaCompletionBaseTest extends CompletionTestCase {
+ protected void testBySchema(@Language("JSON") @NotNull final String schema, final @NotNull String text, final @NotNull String extension,
+ final @NotNull String... variants) throws Exception {
+ final int position = EditorTestUtil.getCaretPosition(text);
+ assertThat(position).isGreaterThan(0);
+ final String completionText = text.replace("<caret>", "IntelliJIDEARulezzz");
+
+ final PsiFile file = createFile(myModule, "tslint." + extension, completionText);
+ final PsiElement element = file.findElementAt(position);
+ assertThat(element).isNotNull();
+
+ final PsiFile schemaFile = createFile(myModule, "testSchema.json", schema);
+ final JsonSchemaObject schemaObject = JsonSchemaReader.readFromFile(myProject, schemaFile.getVirtualFile());
+ assertThat(schemaObject).isNotNull();
+
+ final List<LookupElement> foundVariants = JsonSchemaCompletionContributor.getCompletionVariants(schemaObject, element, element);
+ Collections.sort(foundVariants, Comparator.comparing(LookupElement::getLookupString));
+ myItems = foundVariants.toArray(LookupElement.EMPTY_ARRAY);
+ assertStringItems(variants);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt
new file mode 100644
index 00000000..e6c2af5c
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt
@@ -0,0 +1,349 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl
+
+import com.intellij.codeInsight.lookup.LookupElementPresentation
+import com.intellij.testFramework.assertions.Assertions.assertThat
+import com.jetbrains.jsonSchema.JsonSchemaHighlightingTest
+import org.intellij.lang.annotations.Language
+import org.junit.Assert
+
+class JsonBySchemaCompletionTest : JsonBySchemaCompletionBaseTest() {
+ fun testTopLevel() {
+ testImpl("""{"properties": {"prima": {}, "proto": {}, "primus": {}}}""", "{<caret>}", "\"prima\"", "\"primus\"", "\"proto\"")
+ }
+
+ fun testTopLevelVariant() {
+ testImpl("""{"properties": {"prima": {}, "proto": {}, "primus": {}}}""", "{\"pri<caret>\"}", "prima", "primus", "proto")
+ }
+
+ fun testBoolean() {
+ testImpl("""{"properties": {"prop": {"type": "boolean"}}}""", "{\"prop\": <caret>}", "false", "true")
+ }
+
+ fun testEnum() {
+ testImpl("""{"properties": {"prop": {"enum": ["prima", "proto", "primus"]}}}""",
+ """{"prop": <caret>}""", "\"prima\"", "\"primus\"", "\"proto\"")
+ }
+
+ fun testTopLevelAnyOfValues() {
+ testImpl("""{"properties": {"prop": {"anyOf": [{"enum": ["prima", "proto", "primus"]},""" + "{\"type\": \"boolean\"}]}}}",
+ """{"prop": <caret>}""", "\"prima\"", "\"primus\"", "\"proto\"", "false", "true")
+ }
+
+ fun testTopLevelAnyOf() {
+ testImpl(
+ """{"anyOf": [ {"properties": {"prima": {}, "proto": {}, "primus": {}}},""" + "{\"properties\": {\"abrakadabra\": {}}}]}",
+ """{<caret>}""", "\"abrakadabra\"", "\"prima\"", "\"primus\"", "\"proto\"")
+ }
+
+ fun testSimpleHierarchy() {
+ testImpl("""{"properties": {"top": {"properties": {"prima": {}, "proto": {}, "primus": {}}}}}""",
+ """{"top": {<caret>}}""", "\"prima\"", "\"primus\"", "\"proto\"")
+ }
+
+ fun testObjectsInsideArray() {
+ val schema = """{"properties": {"prop": {"type": "array", "items":
+ {"type": "object","properties": {"innerType":{}, "innerValue":{}}, "additionalProperties": false}}}}"""
+ testImpl(schema, """{"prop": [{<caret>}]}""", "\"innerType\"", "\"innerValue\"")
+ }
+
+ fun testObjectValuesInsideArray() {
+ val schema = """{"properties": {"prop": {"type": "array", "items":
+ {"type": "object","properties": {"innerType":{"enum": [115,117, "nothing"]}, "innerValue":{}}, "additionalProperties": false}}}}"""
+ testImpl(schema, """{"prop": [{"innerType": <caret>}]}""", "\"nothing\"", "115", "117")
+ }
+
+ fun testLowLevelOneOf() {
+ val schema = """{"properties": {"prop": {"type": "array", "items":
+ {"type": "object","properties": {"innerType":{"oneOf": [{"properties": {"a1": {}, "a2": {}}},
+ {"properties": {"b1": {}, "b2": {}}}]}, "innerValue":{}}, "additionalProperties": false}}}}"""
+ testImpl(schema, """{"prop": [{"innerType": {<caret>}}]}""", "\"a1\"", "\"a2\"", "\"b1\"", "\"b2\"")
+ }
+
+ fun testArrayValuesInsideObject() {
+ val schema = """{"properties": {"prop": {"type": "array","items": {"enum": [1,2,3]}}}}"""
+ testImpl(schema, """{"prop": [<caret>]}""", "1", "2", "3")
+ }
+
+ fun testAllOfTerminal() {
+ val schema = """{"allOf": [{"type": "object", "properties": {"first": {}}}, {"properties": {"second": {"enum": [33,44]}}}]}"""
+ testImpl(schema, """{"<caret>"}""", "first", "second")
+ }
+
+ fun testAllOfInTheMiddle() {
+ val schema = """{"allOf": [{"type": "object", "properties": {"first": {}}}, {"properties": {"second": {"enum": [33,44]}}}]}"""
+ testImpl(schema, """{"second": <caret>}""", "33", "44")
+ }
+
+ fun testValueCompletion() {
+ val schema = """{
+ "properties": {
+ "top": {
+ "enum": ["test", "me"]
+ }
+ }
+}"""
+ testImpl(schema, """{"top": <caret>}""", "\"me\"", "\"test\"")
+ }
+
+ fun testTopLevelArrayPropNameCompletion() {
+ val schema = parcelShopSchema()
+ testImpl(schema, "[{<caret>}]", "\"address\"")
+ testImpl(schema, """[{"address": {<caret>}}]""", "\"fax\"", "\"houseNumber\"")
+ testImpl(schema, """[{"address": {"houseNumber": <caret>}}]""", "1", "2")
+ }
+
+ fun testPatternPropertyCompletion() {
+ val schema = """{
+ "patternProperties": {
+ "C": {
+ "enum": ["test", "em"]
+ }
+ }
+}"""
+ testImpl(schema, """{"Cyan": <caret>}""", "\"em\"", "\"test\"")
+ }
+
+ fun testRootObjectRedefined() {
+ testImpl(JsonSchemaHighlightingTest.rootObjectRedefinedSchema(), "{<caret>}", "\"r1\"", "\"r2\"")
+ }
+
+ fun testSimpleNullCompletion() {
+ val schema = """{
+ "properties": {
+ "null": {
+ "type": "null"
+ }
+ }
+}"""
+ testImpl(schema, """{"null": <caret>}""", "null")
+ }
+
+ fun testNullCompletionInEnum() {
+ val schema = """{
+ "properties": {
+ "null": {
+ "type": ["null", "integer"],
+ "enum": [null, 1, 2]
+ }
+ }
+}"""
+ testImpl(schema, """{"null": <caret>}""", "1", "2", "null")
+ }
+
+ fun testNullCompletionInTypeVariants() {
+ val schema = """{
+ "properties": {
+ "null": {
+ "type": ["null", "boolean"]
+ }
+ }
+}"""
+ testImpl(schema, """{"null": <caret>}""", "false", "null", "true")
+ }
+
+ fun testDescriptionFromDefinitionInCompletion() {
+ val schema = """{
+ "definitions": {
+ "target": {
+ "description": "Target description"
+ }
+ },
+ "properties": {
+ "source": {
+ "${"$"}ref": "#/definitions/target"
+ }
+ }
+}"""
+ testImpl(schema, "{<caret>}", "\"source\"")
+ Assert.assertEquals(1, myItems.size.toLong())
+ val presentation = LookupElementPresentation()
+ myItems[0].renderElement(presentation)
+ Assert.assertEquals("Target description", presentation.typeText)
+ }
+
+ fun testDescriptionFromTitleInCompletion() {
+ val schema = """{
+ "definitions": {
+ "target": {
+ "title": "Target title",
+ "description": "Target description"
+ }
+ },
+ "properties": {
+ "source": {
+ "${"$"}ref": "#/definitions/target"
+ }
+ }
+}"""
+ testImpl(schema, "{<caret>}", "\"source\"")
+ Assert.assertEquals(1, myItems.size.toLong())
+ val presentation = LookupElementPresentation()
+ myItems[0].renderElement(presentation)
+ Assert.assertEquals("Target title", presentation.typeText)
+ }
+
+ fun testAnyOfInsideAllOfWithInnerProperties() {
+ val schema = """
+{
+ "definitions": {
+ "aaa": {
+ "properties": {
+ "aaa_prop": {}
+ }
+ },
+ "bbb": {
+ "properties": {
+ "bbb_prop": {}
+ }
+ },
+ "excl1": {
+ "properties": {
+ "excl1_prop": {}
+ }
+ },
+ "excl2": {
+ "properties": {
+ "excl2_prop": {}
+ }
+ }
+ },
+ "allOf": [
+ {"${"$"}ref": "#/definitions/aaa"},
+ {"${"$"}ref": "#/definitions/bbb"},
+ {
+ "anyOf": [
+ {"${"$"}ref": "#/definitions/excl1"},
+ {"${"$"}ref": "#/definitions/excl2"}
+ ]
+ }
+ ]
+}"""
+ testImpl(schema, "{<caret>}", "\"aaa_prop\"", "\"bbb_prop\"", "\"excl1_prop\"", "\"excl2_prop\"")
+ }
+
+ private fun parcelShopSchema(): String {
+ return """{
+ "${"$"}schema": "http://json-schema.org/draft-04/schema#",
+
+ "title": "parcelshop search response schema",
+
+ "definitions": {
+ "address": {
+ "type": "object",
+ "properties": {
+ "houseNumber": { "type": "integer", "enum": [1,2]},
+ "fax": { "${"$"}ref": "#/definitions/phone" }
+ }
+ },
+ "phone": {
+ "type": "object",
+ "properties": {
+ "countryPrefix": { "type": "string" },
+ "number": { "type": "string" }
+ }
+ }
+ },
+
+ "type": "array",
+
+ "items": {
+ "type": "object",
+ "properties": {
+ "address": { "${"$"}ref": "#/definitions/address" }
+ }
+ }
+}"""
+ }
+
+ private fun testImpl(@Language("JSON") schema: String, text: String,
+ vararg variants: String) {
+ testBySchema(schema, text, ".json", *variants)
+ }
+
+ private val ifThenElseSchema: String
+ get() {
+ @Suppress("UnnecessaryVariable")
+ @Language("JSON") val schema = """{
+ "if": {
+ "properties": {
+ "a": {
+ "type": "string"
+ }
+ },
+ "required": ["a"]
+ },
+ "then": {
+ "properties": {
+ "b": {
+ "type": "number",
+ "description": "Target b description"
+ }
+ },
+ "required": ["b"]
+ },
+ "else": {
+ "properties": {
+ "c": {
+ "type": "boolean",
+ "description": "Target c description"
+ }
+ },
+ "required": ["c"]
+ }
+ }"""
+ return schema
+ }
+
+ fun testIfThenElseV7EmptyPropName() {
+ testImpl(ifThenElseSchema, "{<caret>}", "\"c\"")
+ Assert.assertEquals(1, myItems.size.toLong())
+ val presentation = LookupElementPresentation()
+ myItems[0].renderElement(presentation)
+ Assert.assertEquals("Target c description", presentation.typeText)
+ }
+
+ fun testIfThenElseV7ThenPropName() {
+ testImpl(ifThenElseSchema, """{"a": "a", <caret>}""", "\"b\"")
+ Assert.assertEquals(1, myItems.size.toLong())
+ val presentation = LookupElementPresentation()
+ myItems[0].renderElement(presentation)
+ Assert.assertEquals("Target b description", presentation.typeText)
+ }
+
+ fun testIfThenElseV7ElsePropName() {
+ testImpl(ifThenElseSchema, """{"a": 5, <caret>}""", "\"c\"")
+ Assert.assertEquals(1, myItems.size.toLong())
+ val presentation = LookupElementPresentation()
+ myItems[0].renderElement(presentation)
+ Assert.assertEquals("Target c description", presentation.typeText)
+ }
+
+ fun testIfThenElseV7ElsePropValue() {
+ testImpl(ifThenElseSchema, """{"a": 5, "c": <caret>}""", "false", "true")
+ assertThat(myItems).hasSize(2)
+ }
+
+ fun testNestedPropsMerging() {
+ testImpl("""{
+ "allOf": [
+ {
+ "properties": {
+ "severity": {
+ "type": "string",
+ "enum": ["a", "b"]
+ }
+ }
+ },
+ {
+ "properties": {
+ "severity": {
+ }
+ }
+ }
+ ]
+}""","""{
+ "severity": <caret>
+}""", "\"a\"", "\"b\"")
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java
new file mode 100644
index 00000000..fbbea9ab
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java
@@ -0,0 +1,163 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.junit.Assert;
+
+/**
+ * @author Irina.Chernushina on 3/4/2017.
+ */
+public class JsonBySchemaHeavyCompletionTest extends JsonBySchemaHeavyCompletionTestBase {
+ @Override
+ protected String getExtensionWithoutDot() {
+ return "json";
+ }
+
+ @Override
+ protected String getBasePath() {
+ return "/tests/testData/jsonSchema/completion";
+ }
+
+ public void testInsertEnumValue() throws Exception {
+ baseInsertTest(getTestName(true), "testValue");
+ }
+
+ public void testInsertPropertyName() throws Exception {
+ baseInsertTest("insertPropertyName", "testName");
+ }
+
+ public void testInsertNameWithDefaultStringValue() throws Exception {
+ baseInsertTest("insertPropertyName", "testNameWithDefaultStringValue");
+ }
+
+ public void testIncompleteNameWithDefaultStringValue() throws Exception {
+ baseInsertTest("insertPropertyName", "testIncompleteNameWithDefaultStringValue");
+ }
+
+ public void testInsertNameWithDefaultIntegerValue() throws Exception {
+ baseInsertTest("insertPropertyName", "testNameWithDefaultIntegerValue");
+ }
+
+ public void testInsertIntegerType() throws Exception {
+ baseInsertTest("insertPropertyName", "testIntegerType");
+ }
+
+ public void testInsertStringType() throws Exception {
+ baseInsertTest("insertPropertyName", "testStringType");
+ }
+
+ public void testInsertObjectType() throws Exception {
+ baseInsertTest("insertPropertyName", "testObjectType");
+ }
+
+ public void testInsertArrayType() throws Exception {
+ baseInsertTest("insertPropertyName", "testArrayType");
+ }
+
+ public void testInsertBooleanType() throws Exception {
+ baseInsertTest("insertPropertyName", "testBooleanType");
+ }
+
+ //no quotes
+ public void testNameWithDefaultStringValueNoQuotes() throws Exception {
+ baseInsertTest("insertPropertyName", "testNameWithDefaultStringValueNoQuotes");
+ }
+
+ public void testNameWithDefaultIntegerValueNoQuotesComma() throws Exception {
+ baseInsertTest("insertPropertyName", "testNameWithDefaultIntegerValueNoQuotesComma");
+ }
+
+ //comma
+ public void testInsertIntegerTypeComma() throws Exception {
+ baseInsertTest("insertPropertyName", "testIntegerTypeComma");
+ }
+
+ public void testInsertBooleanTypeComma() throws Exception {
+ baseInsertTest("insertPropertyName", "testBooleanTypeComma");
+ }
+
+ public void testStringTypeComma() throws Exception {
+ baseInsertTest("insertPropertyName", "testStringTypeComma");
+ }
+
+ public void testNameWithDefaultStringValueComma() throws Exception {
+ baseInsertTest("insertPropertyName", "testNameWithDefaultStringValueComma");
+ }
+
+ public void testWhitespaceAfterColon() throws Exception {
+ baseInsertTest("addWhitespaceAfterColon", "colon");
+ }
+
+ public void testArrayLiteral() throws Exception {
+ baseInsertTest("insertArrayOrObjectLiteral", "arrayLiteral");
+ complete();
+ assertStringItems("1","2","3");
+ }
+
+ public void testObjectLiteral() throws Exception {
+ baseInsertTest("insertArrayOrObjectLiteral", "objectLiteral");
+ complete();
+ assertStringItems("\"insideTopObject1\"","\"insideTopObject2\"");
+ }
+
+ public void testOneOfWithNotFilledPropertyValue() throws Exception {
+ baseCompletionTest("oneOfWithEnumValue", "oneOfWithEmptyPropertyValue", "\"business\"", "\"home\"");
+ }
+
+ public void testRequiredPropsFirst() throws Exception {
+ baseTestNoSchema("requiredProps", "requiredPropsFirst", () -> {
+ complete();
+ assertStringItems("a", "b");
+ });
+ }
+
+ public void testRequiredPropsLast() throws Exception {
+ baseTestNoSchema("requiredProps", "requiredPropsLast", () -> {
+ complete();
+ assertStringItems("b");
+ });
+ }
+
+ public void testEditingSchemaAffectsCompletion() throws Exception {
+ baseTest(getTestName(true), "testEditing", () -> {
+ complete();
+ assertStringItems("\"preserve\"", "\"react\"", "\"react-native\"");
+
+ final PsiFile schema = myFile.getParent().findFile("Schema.json");
+ final int idx = schema.getText().indexOf("react-native");
+ Assert.assertTrue(idx > 0);
+ PsiElement element = schema.findElementAt(idx);
+ element = element instanceof JsonStringLiteral ? element : PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class);
+ Assert.assertTrue(element instanceof JsonStringLiteral);
+
+ final PsiFile dummy = PsiFileFactory.getInstance(myProject).createFileFromText("test.json", JsonFileType.INSTANCE,
+ "{\"a\": \"completelyChanged\"}");
+ Assert.assertTrue(dummy instanceof JsonFile);
+ final JsonValue top = ((JsonFile)dummy).getTopLevelValue();
+ final JsonValue newLiteral = ((JsonObject)top).findProperty("a").getValue();
+
+ PsiElement finalElement = element;
+ WriteAction.run(() -> finalElement.replace(newLiteral));
+
+ complete();
+ assertStringItems("\"completelyChanged\"", "\"preserve\"", "\"react\"");
+ });
+ }
+
+ public void testGuessType() throws Exception {
+ baseInsertTest("guessType", "test");
+ }
+
+ public void testDontGuessType() throws Exception {
+ baseInsertTest("dontGuessType", "test");
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java
new file mode 100644
index 00000000..bf5d5f8e
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java
@@ -0,0 +1,83 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CodeCompletionHandlerBase;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.jetbrains.jsonSchema.JsonSchemaHeavyAbstractTest;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+
+public abstract class JsonBySchemaHeavyCompletionTestBase extends JsonSchemaHeavyAbstractTest {
+ protected void baseCompletionTest(@SuppressWarnings("SameParameterValue") final String folder,
+ @SuppressWarnings("SameParameterValue") final String testFile, @NotNull String... items) throws Exception {
+ baseTest(folder, testFile, () -> {
+ complete();
+ assertStringItems(items);
+ });
+ }
+
+ protected void baseInsertTest(@SuppressWarnings("SameParameterValue") final String folder, final String testFile) throws Exception {
+ baseTest(folder, testFile, () -> {
+ final CodeCompletionHandlerBase handlerBase = new CodeCompletionHandlerBase(CompletionType.BASIC);
+ handlerBase.invokeCompletion(getProject(), getEditor());
+ if (myItems != null) {
+ selectItem(myItems[0]);
+ }
+ try {
+ checkResultByFile("/" + folder + "/" + testFile + "_after." + getExtensionWithoutDot());
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ protected abstract String getExtensionWithoutDot();
+
+ protected void baseTest(@NotNull final String folder, @NotNull final String testFile, @NotNull final Runnable checker) throws Exception {
+ skeleton(new JsonSchemaHeavyAbstractTest.Callback() {
+ @Override
+ public void registerSchemes() {
+ final String moduleDir = getModuleDir(getProject());
+
+ final UserDefinedJsonSchemaConfiguration base =
+ new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/Schema.json", false,
+ Collections
+ .singletonList(new UserDefinedJsonSchemaConfiguration.Item(testFile + "." + getExtensionWithoutDot(), true, false))
+ );
+ addSchema(base);
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/" + folder + "/" + testFile + "." + getExtensionWithoutDot(), "/" + folder + "/Schema.json");
+ }
+
+ @Override
+ public void doCheck() {
+ checker.run();
+ }
+ });
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected void baseTestNoSchema(@NotNull final String folder, @NotNull final String testFile, @NotNull final Runnable checker) throws Exception {
+ skeleton(new JsonSchemaHeavyAbstractTest.Callback() {
+ @Override
+ public void registerSchemes() {
+ }
+
+ @Override
+ public void configureFiles() {
+ configureByFiles(null, "/" + folder + "/" + testFile + "." + getExtensionWithoutDot());
+ }
+
+ @Override
+ public void doCheck() {
+ checker.run();
+ }
+ });
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java
new file mode 100644
index 00000000..fddaf672
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonTestCase;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.indexing.FileContentImpl;
+
+import java.util.Map;
+
+import static com.jetbrains.jsonSchema.impl.JsonCachedValues.*;
+
+public class JsonSchemaFileValuesIndexTest extends JsonTestCase {
+
+ public void testEmpty() {
+ final VirtualFile file = myFixture.configureByFile("indexing/empty.json").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertAllCacheNulls(map);
+ }
+
+ public void testSimple() {
+ final VirtualFile file = myFixture.configureByFile("indexing/empty.json").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertAllCacheNulls(map);
+ }
+
+ public void testValid() {
+ final VirtualFile file = myFixture.configureByFile("indexing/valid.json").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertEquals("the-id", map.get(ID_CACHE_KEY));
+ assertCacheNull(map.get(URL_CACHE_KEY));
+ }
+
+ public void testValid2() {
+ final VirtualFile file = myFixture.configureByFile("indexing/valid2.json5").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertEquals("the-schema", map.get(URL_CACHE_KEY));
+ assertCacheNull(map.get(ID_CACHE_KEY));
+ }
+
+ public void testInvalid() {
+ final VirtualFile file = myFixture.configureByFile("indexing/invalid.json").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertAllCacheNulls(map);
+ }
+
+ public void testStopsOnAllFound() {
+ final VirtualFile file = myFixture.configureByFile("indexing/duplicates.json5").getVirtualFile();
+ Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file));
+ assertEquals("the-schema", map.get(URL_CACHE_KEY));
+ assertEquals("the-id", map.get(ID_CACHE_KEY));
+ assertEquals("the-obsolete-id", map.get(OBSOLETE_ID_CACHE_KEY));
+ }
+
+ private static void assertCacheNull(String value) {
+ assertEquals(JsonSchemaFileValuesIndex.NULL, value);
+ }
+
+ private static void assertAllCacheNulls(Map<String, String> map) {
+ map.values().forEach(JsonSchemaFileValuesIndexTest::assertCacheNull);
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java
new file mode 100644
index 00000000..5600c3d0
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java
@@ -0,0 +1,161 @@
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionTestCase;
+import com.intellij.codeInsight.daemon.impl.HighlightInfo;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.concurrency.Semaphore;
+import com.jetbrains.jsonSchema.JsonSchemaTestServiceImpl;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection;
+import org.junit.Assert;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author Irina.Chernushina on 8/29/2015.
+ */
+public class JsonSchemaReadTest extends CompletionTestCase {
+ @Override
+ protected String getTestDataPath() {
+ return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema";
+ }
+
+ public void testReadSchemaItself() throws Exception {
+ final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/schema.json");
+ final JsonSchemaObject read = getSchemaObject(file);
+
+ Assert.assertEquals("http://json-schema.org/draft-04/schema#", read.getId());
+ Assert.assertTrue(read.getDefinitionsMap().containsKey("positiveInteger"));
+ Assert.assertTrue(read.getProperties().containsKey("multipleOf"));
+ Assert.assertTrue(read.getProperties().containsKey("type"));
+ Assert.assertTrue(read.getProperties().containsKey("additionalProperties"));
+ Assert.assertEquals(2, read.getProperties().get("additionalItems").getAnyOf().size());
+ Assert.assertEquals("#", read.getProperties().get("additionalItems").getAnyOf().get(1).getRef());
+
+ final JsonSchemaObject required = read.getProperties().get("required");
+ Assert.assertEquals("#/definitions/stringArray", required.getRef());
+
+ final JsonSchemaObject minLength = read.getProperties().get("minLength");
+ Assert.assertEquals("#/definitions/positiveIntegerDefault0", minLength.getRef());
+ }
+
+ public void testMainSchemaHighlighting() {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(myProject);
+ final List<JsonSchemaFileProvider> providers = new JsonSchemaProjectSelfProviderFactory().getProviders(myProject);
+ Assert.assertEquals(JsonSchemaProjectSelfProviderFactory.TOTAL_PROVIDERS, providers.size());
+ for (JsonSchemaFileProvider provider: providers) {
+ final VirtualFile mainSchema = provider.getSchemaFile();
+ assertNotNull(mainSchema);
+ assertTrue(service.isSchemaFile(mainSchema));
+
+ enableInspectionTool(new JsonSchemaComplianceInspection());
+ Disposer.register(getTestRootDisposable(), new Disposable() {
+ @Override
+ public void dispose() {
+ JsonSchemaTestServiceImpl.setProvider(null);
+ }
+ });
+
+ configureByExistingFile(mainSchema);
+ final List<HighlightInfo> infos = doHighlighting();
+ for (HighlightInfo info : infos) {
+ if (!HighlightSeverity.INFORMATION.equals(info.getSeverity())) {
+ fail(String.format("%s in: %s", info.getDescription(),
+ myEditor.getDocument().getText(new TextRange(info.getStartOffset(), info.getEndOffset()))));
+ }
+ }
+ }
+ }
+
+ private JsonSchemaObject getSchemaObject(File file) throws Exception {
+ Assert.assertTrue(file.exists());
+ final VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
+ Assert.assertNotNull(virtualFile);
+ return JsonSchemaReader.readFromFile(myProject, virtualFile);
+ }
+
+ public void testReadSchemaWithCustomTags() throws Exception {
+ final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/withNotesCustomTag.json");
+ final JsonSchemaObject read = getSchemaObject(file);
+ Assert.assertTrue(read.getDefinitionsMap().get("common").getProperties().containsKey("id"));
+ }
+
+ public void testArrayItemsSchema() throws Exception {
+ final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/arrayItemsSchema.json");
+ final JsonSchemaObject read = getSchemaObject(file);
+ final Map<String, JsonSchemaObject> properties = read.getProperties();
+ Assert.assertEquals(1, properties.size());
+ final JsonSchemaObject object = properties.get("color-hex-case");
+ final List<JsonSchemaObject> oneOf = object.getOneOf();
+ Assert.assertEquals(2, oneOf.size());
+
+ final JsonSchemaObject second = oneOf.get(1);
+ final List<JsonSchemaObject> list = second.getItemsSchemaList();
+ Assert.assertEquals(2, list.size());
+
+ final JsonSchemaObject firstItem = list.get(0);
+ Assert.assertEquals("#/definitions/lowerUpper", firstItem.getRef());
+ final JsonSchemaObject definition = read.findRelativeDefinition(firstItem.getRef());
+ Assert.assertNotNull(definition);
+
+ final List<Object> anEnum = definition.getEnum();
+ Assert.assertEquals(2, anEnum.size());
+ Assert.assertTrue(anEnum.contains("\"lower\""));
+ Assert.assertTrue(anEnum.contains("\"upper\""));
+ }
+
+ public void testReadSchemaWithWrongRequired() throws Exception {
+ doTestSchemaReadNotHung(new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/WithWrongRequired.json"));
+ }
+
+ public void testReadSchemaWithWrongItems() throws Exception {
+ doTestSchemaReadNotHung(new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/WithWrongItems.json"));
+ }
+
+ private void doTestSchemaReadNotHung(final File file) throws Exception {
+ // because of threading
+ if (Runtime.getRuntime().availableProcessors() < 2) return;
+
+ Assert.assertTrue(file.exists());
+
+ final AtomicBoolean done = new AtomicBoolean();
+ final AtomicReference<Exception> error = new AtomicReference<>();
+ final Semaphore semaphore = new Semaphore();
+ semaphore.down();
+ final Thread thread = new Thread(() -> {
+ try {
+ ReadAction.run(() -> getSchemaObject(file));
+ done.set(true);
+ }
+ catch (Exception e) {
+ error.set(e);
+ }
+ finally {
+ semaphore.up();
+ }
+ }, getClass().getName() + ": read test json schema " + file.getName());
+ thread.setDaemon(true);
+ try {
+ thread.start();
+ semaphore.waitFor(TimeUnit.SECONDS.toMillis(120));
+ if (error.get() != null) throw error.get();
+ Assert.assertTrue("Reading test schema hung!", done.get());
+ } finally {
+ thread.interrupt();
+ }
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java b/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java
new file mode 100644
index 00000000..8abd1fd5
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java
@@ -0,0 +1,14 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.json5;
+
+import com.jetbrains.jsonSchema.impl.JsonBySchemaCompletionBaseTest;
+
+public class Json5ByJsonSchemaCompletionTest extends JsonBySchemaCompletionBaseTest {
+ public void testTopLevel() throws Exception {
+ testBySchema("{\"properties\": {\"prima\": {}, \"proto\": {}, \"primus\": {}}}", "{pri<caret>}", "json5", "prima", "primus", "proto");
+ }
+
+ public void testAlreadyInsertedProperty() throws Exception {
+ testBySchema("{\"properties\": {\"prima\": {}, \"proto\": {}, \"primus\": {}}}", "{prima: 1, pri<caret>}", "json5", "primus", "proto");
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java
new file mode 100644
index 00000000..72e14fcc
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.jetbrains.jsonSchema.schemaFile;
+
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.JsonSchemaHeavyAbstractTest;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.junit.Assert;
+
+import java.util.Collections;
+
+/**
+ * @author Irina.Chernushina on 4/1/2016.
+ */
+public class JsonSchemaFileResolveTest extends JsonSchemaHeavyAbstractTest {
+ @Override
+ protected String getBasePath() {
+ return "/tests/testData/jsonSchema/schemaFile/resolve";
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myDoCompletion = false;
+ }
+
+ public void testResolveLocalRef() throws Exception {
+ skeleton(new Callback() {
+ @Override
+ public void doCheck() {
+ final int offset = getEditor().getCaretModel().getCurrentCaret().getOffset();
+ final PsiElement atOffset = PsiTreeUtil.findElementOfClassAtOffset(myFile, offset, PsiElement.class, false);
+ Assert.assertNotNull(atOffset);
+ PsiReference position = myFile.findReferenceAt(offset);
+ Assert.assertNotNull(position);
+ PsiElement resolve = position.resolve();
+ Assert.assertNotNull(resolve);
+ Assert.assertEquals("{\n" +
+ " \"type\": \"string\",\n" +
+ " \"enum\": [\"one\", \"two\"]\n" +
+ " }", resolve.getText());
+ }
+
+ @Override
+ public void configureFiles() throws Exception {
+ configureByFile("localRefSchema.json");
+ }
+
+ @Override
+ public void registerSchemes() {
+ final String path = VfsUtilCore.getRelativePath(myFile.getVirtualFile(), myProject.getBaseDir());
+ final UserDefinedJsonSchemaConfiguration info =
+ new UserDefinedJsonSchemaConfiguration("test", JsonSchemaVersion.SCHEMA_4, path, false, Collections.emptyList());
+ JsonSchemaFileResolveTest.this.addSchema(info);
+ }
+ });
+ }
+}
diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java
new file mode 100644
index 00000000..30f77bba
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.jsonSchema.schemaFile;
+
+import com.jetbrains.jsonSchema.*;
+import com.jetbrains.jsonSchema.fixes.JsonSchemaQuickFixTest;
+import com.jetbrains.jsonSchema.impl.JsonBySchemaCompletionTest;
+import com.jetbrains.jsonSchema.impl.JsonBySchemaHeavyCompletionTest;
+import com.jetbrains.jsonSchema.impl.JsonSchemaReadTest;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * @author Irina.Chernushina on 4/12/2017.
+ */
+@SuppressWarnings({"JUnitTestClassNamingConvention"})
+public class JsonSchemaTestSuite {
+ public static Test suite() {
+ final TestSuite suite = new TestSuite(JsonSchemaTestSuite.class.getSimpleName());
+ suite.addTestSuite(JsonSchemaCrossReferencesTest.class);
+ suite.addTestSuite(JsonSchemaDocumentationTest.class);
+ suite.addTestSuite(JsonSchemaHighlightingTest.class);
+ suite.addTestSuite(JsonSchemaReSharperHighlightingTest.class);
+ suite.addTestSuite(JsonSchemaPatternComparatorTest.class);
+ suite.addTestSuite(JsonSchemaSelfHighligthingTest.class);
+ suite.addTestSuite(JsonBySchemaCompletionTest.class);
+ suite.addTestSuite(JsonBySchemaHeavyCompletionTest.class);
+ suite.addTestSuite(JsonSchemaReadTest.class);
+ suite.addTestSuite(JsonSchemaFileResolveTest.class);
+ suite.addTestSuite(JsonSchemaPerformanceTest.class);
+ suite.addTestSuite(JsonSchemaQuickFixTest.class);
+ return suite;
+ }
+} \ No newline at end of file
diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java
new file mode 100644
index 00000000..977698e0
--- /dev/null
+++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.jetbrains.jsonSchema.schemaFile;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+
+/**
+ * @author Irina.Chernushina on 4/1/2016.
+ */
+public class TestJsonSchemaMappingsProjectConfiguration extends JsonSchemaMappingsProjectConfiguration {
+ public TestJsonSchemaMappingsProjectConfiguration(Project project) {
+ super(project);
+ }
+}