├── .github └── FUNDING.yml ├── .gitignore ├── .jvmopts ├── .scalafmt.conf ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmarks └── src │ └── main │ └── scala │ └── org │ └── psliwa │ └── idea │ └── composerJson │ └── benchmarks │ └── VersionSuggestionsBenchmark.scala ├── build.sbt ├── proguard.pro ├── project ├── Versions.scala ├── build.properties └── plugins.sbt └── src ├── main ├── java │ └── org │ │ └── psliwa │ │ └── idea │ │ └── composerJson │ │ ├── settings │ │ ├── ComposerJsonConfigurable.form │ │ ├── ComposerJsonConfigurable.java │ │ ├── EnabledItem.java │ │ ├── PatternItem.java │ │ └── TextItem.java │ │ └── ui │ │ ├── ChooserDialog.form │ │ └── ChooserDialog.java ├── resources │ ├── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ └── org │ │ └── psliwa │ │ └── idea │ │ └── composerJson │ │ ├── composer-schema.json │ │ ├── icons │ │ ├── composer.png │ │ ├── composer@x2.png │ │ ├── packagist.png │ │ └── packagist@2x.png │ │ └── messages │ │ └── ComposerBundle.properties └── scala │ └── org │ └── psliwa │ └── idea │ └── composerJson │ ├── ComposerBundle.scala │ ├── Icons.scala │ ├── composer │ ├── InstalledPackages.scala │ ├── command │ │ └── PackagesInstaller.scala │ ├── model │ │ ├── PackageDescriptor.scala │ │ ├── PackageName.scala │ │ ├── Packages.scala │ │ ├── repository │ │ │ ├── CallbackRepository.scala │ │ │ ├── ComposedRepository.scala │ │ │ ├── InMemoryRepository.scala │ │ │ ├── Repository.scala │ │ │ ├── RepositoryInfo.scala │ │ │ ├── RepositoryProvider.scala │ │ │ ├── RepositoryProviderWrapper.scala │ │ │ ├── SkipBuiltInPackagesVersionRepository.scala │ │ │ └── TestingRepositoryProvider.scala │ │ └── version │ │ │ ├── Constraint.scala │ │ │ ├── Parser.scala │ │ │ ├── SemanticVersion.scala │ │ │ ├── VersionEquivalents.scala │ │ │ └── VersionSuggestions.scala │ ├── parsers │ │ ├── JsonParsers.scala │ │ └── RepositoryPackages.scala │ └── repository │ │ ├── DefaultRepositoryProvider.scala │ │ └── Packagist.scala │ ├── intellij │ ├── CharContainsMatcher.scala │ ├── Notifications.scala │ ├── NotificationsHandler.scala │ ├── Patterns.scala │ ├── PsiElementOffsetFinder.scala │ ├── PsiElementWrapper.scala │ ├── PsiElements.scala │ ├── PsiExtractors.scala │ ├── codeAssist │ │ ├── AbstractCompletionContributor.scala │ │ ├── AbstractInspection.scala │ │ ├── AbstractReferenceContributor.scala │ │ ├── AutoPopupInsertHandler.scala │ │ ├── BaseLookupElement.scala │ │ ├── ComposerJsonSchemaExclusion.scala │ │ ├── CreatePropertyQuickFix.scala │ │ ├── IntentionActionQuickFixAdapter.scala │ │ ├── PropertyValueInsertHandler.scala │ │ ├── QuickFix.scala │ │ ├── QuickFixIntentionActionAdapter.scala │ │ ├── QuoteInsertHandler.scala │ │ ├── References.scala │ │ ├── RemoveJsonElementQuickFix.scala │ │ ├── SetPropertyValueQuickFix.scala │ │ ├── composer │ │ │ ├── CompletionContributor.scala │ │ │ ├── CustomRepositoriesEditorNotificationProvider.scala │ │ │ ├── ExcludePatternAction.scala │ │ │ ├── InstallPackagesAction.scala │ │ │ ├── MisconfigurationInspection.scala │ │ │ ├── NotInstalledPackageInspection.scala │ │ │ ├── NotInstalledPackages.scala │ │ │ ├── PackageDocumentationProvider.scala │ │ │ ├── PackageVersionAnnotator.scala │ │ │ ├── PackagesLoader.scala │ │ │ ├── RepositoryUpdater.scala │ │ │ ├── infoRenderer │ │ │ │ ├── PackageInfo.scala │ │ │ │ ├── PackageInfoCaretListener.scala │ │ │ │ ├── PackageInfoInspection.scala │ │ │ │ ├── PackageInfoOverlay.scala │ │ │ │ └── PackageInfoOverlayView.scala │ │ │ └── package.scala │ │ ├── file │ │ │ ├── CreateFilesystemItemQuickFix.scala │ │ │ ├── FilePathInspection.scala │ │ │ ├── FilePathReferenceContributor.scala │ │ │ ├── FilePathReferenceProvider.scala │ │ │ ├── PackageReferenceProvider.scala │ │ │ ├── PackageVersionReferenceProvider.scala │ │ │ ├── UrlPsiReference.scala │ │ │ ├── UrlReferenceContributor.scala │ │ │ └── UrlReferenceProvider.scala │ │ ├── package.scala │ │ ├── php │ │ │ ├── PhpCallbackAnnotator.scala │ │ │ ├── PhpCallbackReference.scala │ │ │ ├── PhpClassInsertHandler.scala │ │ │ ├── PhpNamespaceReference.scala │ │ │ ├── PhpReferenceContributor.scala │ │ │ └── PhpUtils.scala │ │ ├── problem │ │ │ ├── CheckResult.scala │ │ │ ├── Condition.scala │ │ │ ├── ProblemChecker.scala │ │ │ ├── ProblemDescriptor.scala │ │ │ ├── PropertyPath.scala │ │ │ └── checker │ │ │ │ ├── Checker.scala │ │ │ │ ├── MultiplePropertiesChecker.scala │ │ │ │ └── PropertyChecker.scala │ │ ├── schema │ │ │ ├── CompletionContributor.scala │ │ │ ├── RemoveQuotesQuickFix.scala │ │ │ ├── SchemaDocumentationProvider.scala │ │ │ ├── SchemaInspection.scala │ │ │ └── ShowValidValuesQuickFix.scala │ │ └── scripts │ │ │ ├── ScriptAliasReference.scala │ │ │ ├── ScriptsPsiElementPattern.scala │ │ │ ├── ScriptsReference.scala │ │ │ └── ScriptsReferenceContributor.scala │ └── filetype │ │ ├── ComposerJsonFileType.scala │ │ └── ComposerJsonFileTypeFactory.scala │ ├── json │ ├── Format.scala │ ├── Schema.scala │ └── SchemaLoader.scala │ ├── package.scala │ ├── settings │ ├── AppSettings.scala │ ├── ProjectSettings.scala │ └── TabularSettings.scala │ └── util │ ├── CharOffsetFinder.scala │ ├── Files.scala │ ├── Funcs.scala │ ├── IO.scala │ ├── ImplicitConversions.scala │ ├── OffsetFinder.scala │ ├── StringOps.scala │ ├── TryMonoid.scala │ └── parsers │ ├── Implicits.scala │ ├── JSON.scala │ ├── Location.scala │ ├── ParserMonad.scala │ ├── ParserOps.scala │ ├── Parsers.scala │ ├── Result.scala │ └── package.scala └── test ├── resources └── org │ └── psliwa │ └── idea │ └── composerJson │ ├── composer.lock │ └── inspection │ ├── doctrine │ ├── bin │ │ ├── doctrine │ │ └── doctrine.php │ ├── composer.json │ └── lib │ │ └── .gitkeep │ ├── laravel │ ├── app │ │ ├── commands │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── database │ │ │ ├── migrations │ │ │ │ └── .gitkeep │ │ │ └── seeds │ │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ └── tests │ │ │ └── TestCase.php │ └── composer.json │ ├── symfony │ ├── composer.json │ └── src │ │ └── Symfony │ │ └── Component │ │ ├── HttpFoundation │ │ └── Resources │ │ │ └── stubs │ │ │ └── .gitkeep │ │ └── Intl │ │ └── Resources │ │ └── stubs │ │ └── functions.php │ └── symfony_standard │ ├── app │ └── config │ │ └── parameters.yml │ ├── composer.json │ ├── src │ └── classes.php │ └── web │ └── .gitkeep └── scala └── org └── psliwa └── idea └── composerJson ├── BasePropSpec.scala ├── composer ├── InstalledPackagesTest.scala ├── model │ ├── repository │ │ ├── ComposedRepositoryTest.scala │ │ ├── RepositoryGenerators.scala │ │ └── RepositoryProviderWrapperTest.scala │ └── version │ │ ├── ConstraintTest │ │ ├── ParsePresentationTest.scala │ │ ├── PresentationStringTest.scala │ │ ├── ReplaceTest.scala │ │ └── UpperBoundTest.scala │ │ ├── ParserTest.scala │ │ ├── SemanticVersionTest.scala │ │ ├── VersionComparatorTest.scala │ │ ├── VersionEquivalentsTest.scala │ │ ├── VersionGenerators.scala │ │ ├── VersionNsrEquivalentsTest.scala │ │ └── VersionSuggestionsTest.scala ├── parsers │ └── JsonParsersTest.scala └── repository │ ├── DefaultRepositoryFactoryTest.scala │ ├── DefaultRepositoryProviderTest.scala │ ├── LoadPackagesTest.scala │ ├── LoadVersionsTest.scala │ └── RepositoryFromUrlTest.scala ├── fixtures └── ComposerFixtures.scala ├── intellij └── codeAssist │ ├── CompletionTest.scala │ ├── DocumentationTest.scala │ ├── FilePathReferences.scala │ ├── InspectionTest.scala │ ├── ValidComposerJsonFilesInspectionTest.scala │ ├── composer │ ├── AbstractPackagesTest.scala │ ├── MisconfigurationInspectionTest.scala │ ├── MisconfigurationQuickFixesTest.scala │ ├── NotInstalledPackageInspectionTest.scala │ ├── PackageDocumentationProviderTest.scala │ ├── PackageSuggestionsTest.scala │ ├── PackageVersionInspectionTest.scala │ ├── PackageVersionQuickFixesTest.scala │ ├── PackagesCompletionTest.scala │ ├── RepositoryUpdaterTest.scala │ └── infoRenderer │ │ └── PackageInfoInspectionTest.scala │ ├── file │ ├── FilePathInspectionTest.scala │ ├── FilePathQuickFixesTest.scala │ ├── FilePathReferenceTest.scala │ └── UrlReferenceTest.scala │ ├── php │ ├── PhpInspectionTest.scala │ └── PhpReferenceTest.scala │ ├── schema │ ├── CharContainsMatcherTest.scala │ ├── FilePathTest.scala │ ├── SchemaDocumentationProviderTest.scala │ ├── SchemaInspectionTest.scala │ ├── SchemaQuickFixesTest.scala │ ├── StructureCompletionTest.scala │ └── StructureSuggestionsTest.scala │ └── scripts │ ├── ScriptAliasReferenceTest.scala │ └── ScriptsReferenceTest.scala ├── json ├── FormatTest.scala ├── SchemaConversions.scala ├── SchemaLoadingTest.scala └── SchemaTest.scala └── settings └── PatternItemTest.scala /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [psliwa] 2 | custom: https://www.paypal.me/psliwa 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /target/* 3 | /benchmarks/target/* 4 | !target/.gitkeep 5 | composer-json-plugin.zip 6 | *.iml 7 | IDEAS.md 8 | /project/project 9 | /project/target 10 | /idea/ 11 | /subprojects 12 | proguard/ 13 | proguard.jar 14 | /out 15 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -XX:MaxMetaspaceSize=1g 2 | -Xss1m 3 | -Xms1536m 4 | -Xmx2G 5 | -XX:ReservedCodeCacheSize=128m 6 | -XX:+CMSClassUnloadingEnabled 7 | -Dfile.encoding=UTF8 8 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=2.2.1 2 | 3 | maxColumn = 120 4 | 5 | align.tokens = [":|"] 6 | align.arrowEnumeratorGenerator=true 7 | align.openParenCallSite=true 8 | align.openParenDefnSite=true 9 | rewrite.rules = [SortImports, SortModifiers] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: scala 4 | 5 | jdk: 6 | - openjdk8 7 | 8 | env: 9 | - "IDEA_VERSION=2019.2;PHP_PLUGIN_VERSION=192.5728.108" 10 | - "IDEA_VERSION=2019.2.3;PHP_PLUGIN_VERSION=192.6817.12" 11 | - "IDEA_VERSION=2019.2.4;PHP_PLUGIN_VERSION=192.7142.41" 12 | - "IDEA_VERSION=2019.3;PHP_PLUGIN_VERSION=193.5233.102" 13 | 14 | install: 15 | - sbt -DIDEA_VERSION=$IDEA_VERSION -DPHP_PLUGIN_VERSION=$PHP_PLUGIN_VERSION "; test ; benchmarks/jmh:run" 16 | 17 | script: 18 | - sbt -DIDEA_VERSION=$IDEA_VERSION -DPHP_PLUGIN_VERSION=$PHP_PLUGIN_VERSION "; release" 19 | 20 | matrix: 21 | allow_failures: [] 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This plugin uses SBT as build tool. At the beginning, Intellij Ultimate 4 | Edition SDK will be downloaded (into `idea` directory), so be patient. 5 | Intellij Ultimate Edition license is nice to have. 6 | 7 | Following custom SBT commands are defined: 8 | 9 | * `sbt runIDE` - run testing Intellij with the plugin installed - on 10 | first startup you have to install php plugin manually and restart 11 | testing Intellij 12 | * `sbt release` - compile plugin from scratch, prepare package, create 13 | zip and shrink it 14 | * `sbt createRunConfiguration` - creates run configuration in IntelliJ. 15 | Thanks to that you can run IntelliJ with the plugin installed using 16 | `ideaComposerPlugin` run configuration. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Piotr Śliwa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP composer.json support 2 | [![Build Status](https://travis-ci.org/psliwa/idea-composer-plugin.svg?branch=master)](https://travis-ci.org/psliwa/idea-composer-plugin) 3 | [![Version](http://phpstorm.espend.de/badge/7631/version)](https://plugins.jetbrains.com/plugin/7631) 4 | [![Downloads](http://phpstorm.espend.de/badge/7631/downloads)](https://plugins.jetbrains.com/plugin/7631) 5 | [![Downloads last month](http://phpstorm.espend.de/badge/7631/last-month)](https://plugins.jetbrains.com/plugin/7631) 6 | [![Donate using Paypal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.me/psliwa) 7 | [![Donate using Bitcoin](https://img.shields.io/badge/donate-bitcoin-yellow.svg)](https://blockchain.info/address/1Q6f6ZAqYFVzSaBf9AZJ6Ba948jjmQJU4A) 8 | 9 | 10 | Adds code completion, inspections and more to composer.json file. 11 | 12 | This plugin provides: 13 | 14 | * completion for: 15 | * composer.json schema 16 | * package names and version (in require, require-dev etc) from packagist repository and custom repositories defined in composer.json file ("composer", "package" and "path" repository types are supported right now) 17 | * filepath completion (in bin, autoload etc) 18 | * class and static method names in "scripts" properties 19 | * namespaces eg. in "autoload.psr-0" property 20 | 21 | * inspections for: 22 | * composer.json schema + quick fixes (remove entry / property, create property etc.). Schema inspections and completions are synced to [eea4098 commit of composer/composer][3] repository. 23 | * filepath existence (in bin, autoload etc) + quick fixes (remove entry, create file / directory) 24 | * misconfiguration + quick fixes 25 | * version constraints misconfiguration + quick fixes 26 | * not installed packages + install quick fix 27 | * scripts callbacks (class names and method signature) 28 | 29 | * navigation for (eg. by Ctrl+LMB): 30 | * class and method names in "scripts" properties 31 | * files and directories in properties that store file path (eg. "bin") 32 | * package directory (eg. in "require", "require-dev") 33 | * urls and emails (eg. in "homepage") 34 | 35 | * documentation: 36 | * external documentation (`shift+f1`) for packages 37 | * quick docs (`ctrl+q`) and external docs (`shift+f1`) for properties 38 | 39 | * others: 40 | * show current installed version from `composer.lock` 41 | 42 | [There][2] you can find plugin homepage. 43 | 44 | ## This plugin in work 45 | 46 | ![Screen][1] 47 | 48 | ## What's next? 49 | 50 | * If you have feature ideas, please create an issue! I have created a lot of features that used to be useful 51 | for me during my daily job, so I waiting for yours ideas too ;) 52 | 53 | [1]: https://plugins.jetbrains.com/files/7631/screenshot_14847.png 54 | [2]: https://plugins.jetbrains.com/plugin/7631 55 | [3]: https://github.com/composer/composer/commit/eea4098f9800ddb536a907d637b7e084bfe15b7c 56 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/psliwa/idea/composerJson/benchmarks/VersionSuggestionsBenchmark.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.benchmarks 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.openjdk.jmh.annotations._ 6 | import org.psliwa.idea.composerJson.composer.model.version.VersionSuggestions 7 | 8 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 9 | @BenchmarkMode(Array(Mode.SingleShotTime)) 10 | @State(Scope.Thread) 11 | @Fork(value = 1) 12 | @Warmup(iterations = 500) 13 | @Measurement(iterations = 300) 14 | class VersionSuggestionsBenchmark { 15 | // format: off 16 | val versions: List[String] = scala.util.Random.shuffle(for { 17 | major <- 0 to 6 18 | minor <- 0 to 6 19 | patch <- 0 to 6 20 | version <- List(s"$major.$minor.$patch", s"v$major.$minor.$patch", s"$major.$minor.${patch}_2") 21 | } yield version).toList 22 | // format: on 23 | 24 | @Benchmark 25 | def versionSuggestions(): Unit = { 26 | VersionSuggestions.suggestionsForVersions(versions, "") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /proguard.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontoptimize 3 | -dontwarn scala.** 4 | -dontwarn com.intellij.uiDesigner.core.** 5 | -dontwarn org.jetbrains.** 6 | -dontwarn com.intellij.memory.** 7 | -dontwarn javax.xml.bind.ModuleUtil 8 | -dontwarn module-info 9 | 10 | -keep class org.psliwa.idea.composerJson.** 11 | -keepclassmembers class org.psliwa.idea.composerJson.** { 12 | public (...); 13 | } 14 | -------------------------------------------------------------------------------- /project/Versions.scala: -------------------------------------------------------------------------------- 1 | object Versions { 2 | val idea = "2019.3" 3 | val phpPlugin = "193.5233.102" 4 | 5 | val scala = "2.13.1" 6 | val scalaz = "7.2.29" 7 | val scalaParsers = "1.1.2" 8 | val scalaParallelCollections = "0.2.0" 9 | val sprayJson = "1.3.5" 10 | 11 | val junitInterface = "0.11" 12 | val scalacheck = "1.14.2" 13 | val scalatest = "3.0.8" 14 | } 15 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.3 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url("jetbrains-sbt", url("https://dl.bintray.com/jetbrains/sbt-plugins"))( 2 | Resolver.ivyStylePatterns 3 | ) 4 | 5 | addSbtPlugin("org.jetbrains" % "sbt-idea-plugin" % "3.3.3") 6 | addSbtPlugin("org.jetbrains" % "sbt-ide-settings" % "1.0.0") 7 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7") 8 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.2.0") 9 | -------------------------------------------------------------------------------- /src/main/java/org/psliwa/idea/composerJson/settings/EnabledItem.java: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class EnabledItem { 6 | private String name; 7 | private boolean enabled; 8 | 9 | public EnabledItem(@NotNull String name, boolean enabled) { 10 | this.name = name; 11 | this.enabled = enabled; 12 | } 13 | 14 | public String getName() { 15 | return name; 16 | } 17 | 18 | public void setName(String name) { 19 | this.name = name; 20 | } 21 | 22 | public boolean isEnabled() { 23 | return enabled; 24 | } 25 | 26 | public void setEnabled(boolean enabled) { 27 | this.enabled = enabled; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | 35 | EnabledItem that = (EnabledItem) o; 36 | 37 | if (enabled != that.enabled) return false; 38 | return name.equals(that.name); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | int result = name.hashCode(); 44 | result = 31 * result + (enabled ? 1 : 0); 45 | return result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/psliwa/idea/composerJson/settings/PatternItem.java: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.regex.Pattern; 7 | import java.util.regex.PatternSyntaxException; 8 | 9 | public final class PatternItem implements Comparable, Cloneable { 10 | @NotNull 11 | private String pattern = ""; 12 | 13 | private Pattern regexPattern; 14 | 15 | public PatternItem(String pattern) { 16 | setPattern(pattern); 17 | } 18 | 19 | @NotNull 20 | public String getPattern() { 21 | return pattern; 22 | } 23 | 24 | public void setPattern(@Nullable String pattern) { 25 | this.pattern = pattern == null ? "" : pattern; 26 | } 27 | 28 | public boolean matches(String text) { 29 | try { 30 | return getRegexPattern().matcher(text).matches(); 31 | } catch(PatternSyntaxException e) { 32 | return false; 33 | } 34 | } 35 | 36 | private Pattern getRegexPattern() { 37 | if(regexPattern == null) { 38 | int index, previousIndex = 0; 39 | StringBuilder product = new StringBuilder("^"); 40 | 41 | while((index = pattern.indexOf('*', previousIndex)) >= 0) { 42 | product.append(Pattern.quote(pattern.substring(previousIndex, index))) 43 | .append(".*"); 44 | previousIndex = index + 1; 45 | } 46 | 47 | product.append(Pattern.quote(pattern.substring(previousIndex))); 48 | product.append("$"); 49 | 50 | regexPattern = Pattern.compile(product.toString()); 51 | } 52 | 53 | return regexPattern; 54 | } 55 | 56 | @Override 57 | public int compareTo(@NotNull PatternItem o) { 58 | return this.pattern.compareTo(o.pattern); 59 | } 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) return true; 64 | if (o == null || getClass() != o.getClass()) return false; 65 | 66 | PatternItem that = (PatternItem) o; 67 | 68 | return pattern.equals(that.pattern); 69 | } 70 | 71 | @Override 72 | public int hashCode() { 73 | return pattern.hashCode(); 74 | } 75 | 76 | @Override 77 | public PatternItem clone() { 78 | try { 79 | return (PatternItem) super.clone(); 80 | } catch (CloneNotSupportedException e) { 81 | throw new AssertionError(e); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/java/org/psliwa/idea/composerJson/settings/TextItem.java: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Objects; 7 | 8 | public final class TextItem implements Comparable, Cloneable { 9 | private String text = ""; 10 | 11 | public TextItem(String text) { 12 | setText(text); 13 | } 14 | 15 | @NotNull 16 | public String getText() { 17 | return text; 18 | } 19 | 20 | public void setText(@Nullable String text) { 21 | this.text = text == null ? "" : text; 22 | } 23 | 24 | @Override 25 | public int compareTo(@NotNull TextItem o) { 26 | return text.compareTo(o.text); 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | 34 | TextItem textItem = (TextItem) o; 35 | 36 | return Objects.equals(text, textItem.text); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(text); 42 | } 43 | 44 | public TextItem clone() { 45 | try { 46 | return (TextItem) super.clone(); 47 | } catch (CloneNotSupportedException e) { 48 | throw new AssertionError(e); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/psliwa/idea/composerJson/ui/ChooserDialog.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/composer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/main/resources/org/psliwa/idea/composerJson/icons/composer.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/composer@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/main/resources/org/psliwa/idea/composerJson/icons/composer@x2.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/packagist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/main/resources/org/psliwa/idea/composerJson/icons/packagist.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/packagist@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/main/resources/org/psliwa/idea/composerJson/icons/packagist@2x.png -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/ComposerBundle.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson 2 | 3 | import java.util.ResourceBundle 4 | 5 | import com.intellij.CommonBundle 6 | import org.jetbrains.annotations.PropertyKey 7 | 8 | object ComposerBundle { 9 | final private val BundleName = "org.psliwa.idea.composerJson.messages.ComposerBundle" 10 | private val Bundle = ResourceBundle.getBundle(BundleName) 11 | 12 | def message(@PropertyKey(resourceBundle = BundleName) key: String, params: AnyRef*): String = { 13 | CommonBundle.message(Bundle, key, params: _*) 14 | } 15 | 16 | def message(@PropertyKey(resourceBundle = BundleName) key: String): String = message(key, Nil: _*) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/Icons.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson 2 | 3 | import javax.swing.Icon 4 | 5 | import com.intellij.openapi.util.IconLoader 6 | 7 | object Icons { 8 | val Composer: Icon = IconLoader.getIcon("icons/composer.png") 9 | val Packagist: Icon = IconLoader.getIcon("icons/packagist.png") 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/PackageDescriptor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import com.intellij.psi.PsiElement 5 | import org.psliwa.idea.composerJson.composer.InstalledPackages 6 | 7 | import scala.util.matching.Regex 8 | 9 | case class PackageDescriptor(name: PackageName, 10 | version: String, 11 | isDev: Boolean, 12 | homepage: Option[String], 13 | replacedBy: Option[PackageDescriptor]) 14 | 15 | object PackageDescriptor { 16 | def apply(name: String, 17 | version: String, 18 | isDev: Boolean = false, 19 | homepage: Option[String] = None, 20 | replacedBy: Option[PackageDescriptor] = None): PackageDescriptor = { 21 | PackageDescriptor(PackageName(name), version, isDev, homepage, replacedBy) 22 | } 23 | 24 | def documentationUrl(element: PsiElement, name: PackageName): Option[String] = 25 | documentationUrl(element.getContainingFile.getVirtualFile, name) 26 | 27 | def fixName(name: String): String = { 28 | def firstPackageLetter(m: Regex.Match) = m.start > 0 && name.charAt(m.start - 1) == '/' 29 | def dashAhead(m: Regex.Match) = m.start > 0 && name.charAt(m.start - 1) == '-' 30 | def firstVendorLetter(m: Regex.Match) = m.start == 0 31 | def letterPrefix(m: Regex.Match) = if (firstVendorLetter(m) || firstPackageLetter(m) || dashAhead(m)) "" else "-" 32 | 33 | "([A-Z])".r.replaceAllIn(name, (m: Regex.Match) => letterPrefix(m) + m.group(0).toLowerCase) 34 | } 35 | 36 | private def documentationUrl(composerJsonFile: VirtualFile, name: PackageName): Option[String] = { 37 | InstalledPackages.forFile(composerJsonFile).get(name).flatMap(_.homepage).orElse(packagistUrl(name)) 38 | } 39 | 40 | private def packagistUrl(name: PackageName): Option[String] = { 41 | name.`vendor/project` match { 42 | case Some((vendor, project)) => 43 | Some(s"https://packagist.org/packages/$vendor/$project") 44 | case None => 45 | None 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/PackageName.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model 2 | 3 | case class PackageName(presentation: String) { 4 | private val parts = presentation.split('/').toList 5 | 6 | val vendor: Option[String] = parts match { 7 | case List(vendor, _) => Some(vendor) 8 | case _ => None 9 | } 10 | 11 | val project: String = parts.last 12 | 13 | def `vendor/project`: Option[(String, String)] = vendor.map(_ -> project) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/Packages.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model 2 | 3 | case class Packages private (packages: Map[String, PackageDescriptor]) { 4 | def get(name: PackageName): Option[PackageDescriptor] = packages.get(name.presentation.toLowerCase) 5 | def descriptors: List[PackageDescriptor] = packages.values.toList 6 | def isEmpty: Boolean = packages.isEmpty 7 | def nonEmpty: Boolean = packages.nonEmpty 8 | } 9 | 10 | object Packages { 11 | def apply(packages: PackageDescriptor*): Packages = 12 | Packages(packages.map(pkg => pkg.name.presentation.toLowerCase -> pkg).toMap) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/CallbackRepository.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | 5 | private class CallbackRepository[Package](packages: => Seq[Package], versions: PackageName => Seq[String]) 6 | extends Repository[Package] { 7 | override def getPackages: Seq[Package] = packages 8 | override def getPackageVersions(packageName: PackageName): Seq[String] = versions(packageName) 9 | override def map[NewPackage](f: Package => NewPackage): Repository[NewPackage] = 10 | new CallbackRepository(packages.map(f), versions) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/ComposedRepository.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | 5 | private class ComposedRepository[Package](repositories: List[Repository[Package]]) extends Repository[Package] { 6 | override def getPackages: Seq[Package] = { 7 | repositories.flatMap(_.getPackages) 8 | } 9 | 10 | override def getPackageVersions(packageName: PackageName): Seq[String] = { 11 | repositories 12 | .flatMap(_.getPackageVersions(packageName)) 13 | } 14 | 15 | override def map[NewPackage](f: Package => NewPackage): Repository[NewPackage] = 16 | new ComposedRepository(repositories.map(_ map f)) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/InMemoryRepository.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | 5 | private case class InMemoryRepository[Package](packages: Seq[Package], versions: Map[PackageName, Seq[String]] = Map()) 6 | extends Repository[Package] { 7 | override def getPackages: Seq[Package] = packages 8 | override def getPackageVersions(packageName: PackageName): Seq[String] = versions.getOrElse(packageName, Nil) 9 | override def map[NewPackage](f: Package => NewPackage): Repository[NewPackage] = 10 | InMemoryRepository(packages.map(f), versions) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/Repository.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | 5 | trait Repository[+Package] { 6 | def getPackages: Seq[Package] 7 | def getPackageVersions(packageName: PackageName): Seq[String] 8 | def map[NewPackage](f: Package => NewPackage): Repository[NewPackage] 9 | } 10 | 11 | object Repository { 12 | def inMemory[Package](packages: Seq[Package], versions: Map[String, Seq[String]] = Map()): Repository[Package] = { 13 | InMemoryRepository(packages, versions.map { case (packageName, versions) => PackageName(packageName) -> versions }) 14 | } 15 | 16 | def callback[Package](packages: => Seq[Package], versions: PackageName => Seq[String]): Repository[Package] = { 17 | new SkipBuiltInPackagesVersionRepository(new CallbackRepository[Package](packages, versions)) 18 | } 19 | 20 | def composed[Package](repositories: List[Repository[Package]]): Repository[Package] = { 21 | new SkipBuiltInPackagesVersionRepository(new ComposedRepository(repositories)) 22 | } 23 | 24 | def empty[Package]: Repository[Package] = EmptyRepository 25 | } 26 | 27 | private object EmptyRepository extends Repository[Nothing] { 28 | override def getPackages: Seq[Nothing] = Nil 29 | override def getPackageVersions(packageName: PackageName): Seq[String] = Nil 30 | override def map[NewPackage](f: Nothing => NewPackage): Repository[NewPackage] = this 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/RepositoryInfo.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | case class RepositoryInfo(urls: List[String], packagist: Boolean, repository: Option[Repository[String]] = None) 4 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/RepositoryProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | trait RepositoryProvider[Package] { 4 | def repositoryFor(file: String): Repository[Package] 5 | 6 | /** 7 | * @return Return true when repositoryInfo was changed, false otherwise 8 | */ 9 | def updateRepository(file: String, info: RepositoryInfo): Boolean 10 | def hasDefaultRepository(file: String): Boolean 11 | } 12 | 13 | object EmptyRepositoryProvider extends RepositoryProvider[Nothing] { 14 | override def repositoryFor(file: String): Repository[Nothing] = EmptyRepository 15 | override def updateRepository(file: String, info: RepositoryInfo): Boolean = false 16 | override def hasDefaultRepository(file: String): Boolean = true 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/RepositoryProviderWrapper.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | class RepositoryProviderWrapper[Package]( 4 | repositoryProvider: RepositoryProvider[Package], 5 | defaultRepository: Repository[Package], 6 | useDefaultRepository: String => Boolean 7 | ) extends RepositoryProvider[Package] { 8 | override def repositoryFor(file: String): Repository[Package] = { 9 | if (useDefaultRepository(file)) defaultRepository 10 | else repositoryProvider.repositoryFor(file) 11 | } 12 | override def updateRepository(file: String, info: RepositoryInfo): Boolean = 13 | repositoryProvider.updateRepository(file, info) 14 | override def hasDefaultRepository(file: String): Boolean = repositoryProvider.hasDefaultRepository(file) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/SkipBuiltInPackagesVersionRepository.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | 5 | private class SkipBuiltInPackagesVersionRepository[Package](underlyingRepository: Repository[Package]) 6 | extends Repository[Package] { 7 | override def getPackages: Seq[Package] = underlyingRepository.getPackages 8 | 9 | override def getPackageVersions(packageName: PackageName): Seq[String] = { 10 | packageName.vendor match { 11 | case Some(_) => underlyingRepository.getPackageVersions(packageName) 12 | case None => Seq.empty 13 | } 14 | } 15 | 16 | override def map[NewPackage](f: Package => NewPackage): Repository[NewPackage] = 17 | new SkipBuiltInPackagesVersionRepository[NewPackage](underlyingRepository.map(f)) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/model/repository/TestingRepositoryProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import scala.collection.mutable 4 | 5 | //RepositoryProvider only for testing 6 | class TestingRepositoryProvider extends RepositoryProvider[Nothing] { 7 | 8 | val infos: mutable.Map[String, RepositoryInfo] = mutable.Map[String, RepositoryInfo]() 9 | 10 | override def repositoryFor(file: String): Repository[Nothing] = EmptyRepository 11 | override def updateRepository(file: String, info: RepositoryInfo): Boolean = { 12 | val changed = infos.getOrElse(file, null) != info 13 | infos(file) = info 14 | 15 | changed 16 | } 17 | override def hasDefaultRepository(file: String): Boolean = true 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/parsers/RepositoryPackages.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.parsers 2 | 3 | case class RepositoryPackages(packages: Map[String, Seq[String]], includes: Seq[String]) 4 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/composer/repository/Packagist.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.repository 2 | 3 | import org.psliwa.idea.composerJson.composer.model.PackageName 4 | import org.psliwa.idea.composerJson.composer.parsers.JsonParsers.{parsePackageNames, parseVersions} 5 | import org.psliwa.idea.composerJson.util.IO 6 | 7 | import scala.util.Try 8 | 9 | object Packagist { 10 | 11 | val defaultUrl: String = "https://packagist.org/" 12 | 13 | val privatePackagistUrl: String = "https://repo.packagist.com" 14 | 15 | def loadPackages(url: String): Try[Seq[String]] = loadPackagesFromPackagist(url).flatMap(parsePackageNames) 16 | def loadVersions(url: String)(packageName: PackageName): Try[Seq[String]] = 17 | loadUri(url)(s"packages/${packageName.presentation}.json").flatMap(parseVersions) 18 | 19 | private[repository] def loadPackagesFromPackagist(url: String): Try[String] = loadUri(url)("packages/list.json") 20 | private[repository] def loadUri(url: String)(uri: String): Try[String] = { 21 | val fixedUrl = if (!url.lastOption.contains('/')) url + "/" else url 22 | IO.loadUrl(fixedUrl + uri) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/CharContainsMatcher.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.codeInsight.completion.PrefixMatcher 4 | 5 | import scala.annotation.tailrec 6 | 7 | private class CharContainsMatcher(prefix: String) extends PrefixMatcher(prefix) { 8 | override def cloneWithPrefix(prefix: String): PrefixMatcher = new CharContainsMatcher(prefix) 9 | override def prefixMatches(name: String): Boolean = { 10 | @tailrec 11 | def loop(prefix: String, name: String): Boolean = { 12 | prefix match { 13 | case "" => true 14 | case _ => 15 | val index = name.indexOf(prefix.head) 16 | if (index >= 0) loop(prefix.tail, name.substring(index + 1)) 17 | else false 18 | } 19 | } 20 | 21 | loop(myPrefix, name) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/Notifications.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.notification 4 | import com.intellij.notification._ 5 | import com.intellij.openapi.project.Project 6 | import org.psliwa.idea.composerJson.ComposerBundle 7 | 8 | object Notifications { 9 | 10 | private val logOnlyGroup = NotificationGroup.logOnlyGroup(ComposerBundle.message("notifications.group.log")) 11 | private val balloonGroup = new NotificationGroup(ComposerBundle.message("notifications.group.balloon"), 12 | NotificationDisplayType.TOOL_WINDOW, 13 | true) 14 | 15 | def info(title: String, message: String, project: Option[Project] = None): Unit = { 16 | notify(title, message, project) 17 | } 18 | 19 | def balloonInfo(title: String, message: String, project: Option[Project] = None): Unit = { 20 | notify(title, message, project, notificationGroup = balloonGroup) 21 | } 22 | 23 | def error(title: String, message: String, project: Option[Project] = None): Unit = { 24 | notify(title, message, project, NotificationType.ERROR) 25 | } 26 | 27 | private def notify(title: String, 28 | message: String, 29 | project: Option[Project] = None, 30 | notificationType: NotificationType = NotificationType.INFORMATION, 31 | notificationGroup: NotificationGroup = logOnlyGroup): Unit = { 32 | notification.Notifications.Bus.notify( 33 | notificationGroup.createNotification(title, 34 | message, 35 | notificationType, 36 | new NotificationListener.UrlOpeningListener(false)), 37 | project.orNull 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/NotificationsHandler.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.openapi.components.ProjectComponent 4 | import com.intellij.openapi.project.Project 5 | import org.psliwa.idea.composerJson.settings.AppSettings 6 | 7 | class NotificationsHandler(project: Project) extends ProjectComponent { 8 | override def projectOpened(): Unit = { 9 | if (AppSettings.getInstance.wasCharityNotificationShown && 10 | !AppSettings.getInstance.wasCharitySummaryNotificationShown && 11 | AppSettings.getInstance.isCharityNotificationStillValid) { 12 | AppSettings.getInstance.charitySummaryNotificationWasShown() 13 | 14 | val title = "Thank you dear programmer! :)" 15 | val text = 16 | """ 17 | |Three weeks ago I released new version of PHP composer.json support plugin with the special message directed to the users. In this message I wanted to encourage everyone to support charity and help others as much as you can. Also I declared myself to pay 1$ for WOŚP for every star on github and every vote on jetbrains plugins page between 24th December 2017 and 14th January 2018. 18 | |

19 | |There was 328 github stars and plugin votes in total in this time frame. Additionally I was given 5$ via paypal, so my donation to WOŚP will be at least 333$. 20 | |

21 | |Up to two weeks I will publish payments confirmation on my twitter account. 22 | |

23 | |Thank you for participation, enjoy! 24 | |

25 | |@psliwa 26 | """.stripMargin 27 | Notifications.balloonInfo(title, text, Some(project)) 28 | } 29 | } 30 | 31 | override def initComponent(): Unit = {} 32 | override def disposeComponent(): Unit = {} 33 | override def getComponentName: String = "NotificationsHandler" 34 | override def projectClosed(): Unit = {} 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/Patterns.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.patterns.{PatternCondition, StringPattern} 4 | import com.intellij.patterns.StandardPatterns._ 5 | import com.intellij.util.ProcessingContext 6 | 7 | import scala.util.matching.Regex 8 | 9 | private object Patterns { 10 | def stringContains(s: String): StringPattern = { 11 | string().`with`(new PatternCondition[String]("contains") { 12 | override def accepts(t: String, context: ProcessingContext): Boolean = t.contains(s) 13 | }) 14 | } 15 | 16 | def stringMatches(r: Regex): StringPattern = { 17 | string().`with`(new PatternCondition[String]("matches") { 18 | override def accepts(t: String, context: ProcessingContext): Boolean = r.findFirstIn(t).isDefined 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/PsiElementOffsetFinder.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import com.intellij.psi.PsiElement 5 | import org.psliwa.idea.composerJson.util.OffsetFinder 6 | 7 | import scala.language.implicitConversions 8 | 9 | private object PsiElementOffsetFinder extends OffsetFinder[JsonObject, PsiElement] { 10 | override protected def stop(haystack: JsonObject)(offset: Int): Boolean = 11 | !haystack.getTextRange.containsOffset(offset) 12 | override def objectAt(haystack: JsonObject, offset: Int): PsiElement = { 13 | haystack.getChildren 14 | .find(_.getTextRange.contains(offset)) 15 | .getOrElse(haystack.findElementAt(offset)) 16 | } 17 | override protected def reverseStop(haystack: JsonObject)(offset: Int): Boolean = stop(haystack)(offset) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/PsiElements.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.json.JsonLanguage 4 | import com.intellij.json.psi._ 5 | import com.intellij.patterns.PlatformPatterns._ 6 | import com.intellij.patterns.PsiElementPattern 7 | import com.intellij.psi.PsiElement 8 | import org.psliwa.idea.composerJson._ 9 | 10 | import scala.annotation.tailrec 11 | 12 | private object PsiElements { 13 | private val booleans = Map("true" -> true, "false" -> false) 14 | 15 | def ensureJsonObject(element: PsiElement): Option[JsonObject] = element match { 16 | case x: JsonObject => Some(x) 17 | case _ => None 18 | } 19 | 20 | def ensureJsonProperty(element: PsiElement): Option[JsonProperty] = element match { 21 | case x: JsonProperty => Some(x) 22 | case _ => None 23 | } 24 | 25 | def ensureJsonArray(element: PsiElement): Option[JsonArray] = element match { 26 | case x: JsonArray => Some(x) 27 | case _ => None 28 | } 29 | 30 | def ensureJsonBoolean(element: PsiElement): Option[JsonBooleanLiteral] = element match { 31 | case x: JsonBooleanLiteral => Some(x) 32 | case _ => None 33 | } 34 | 35 | def ensureJsonStringLiteral(e: PsiElement): Option[JsonStringLiteral] = e match { 36 | case x: JsonStringLiteral => Some(x) 37 | case _ => None 38 | } 39 | 40 | def ensureJsonFile(file: PsiElement): Option[JsonFile] = file match { 41 | case x: JsonFile => Some(x) 42 | case _ => None 43 | } 44 | 45 | def rootPsiElementPattern: PsiElementPattern.Capture[JsonFile] = { 46 | psiElement(classOf[JsonFile]) 47 | .withLanguage(JsonLanguage.INSTANCE) 48 | .inFile(psiFile(classOf[JsonFile]).withName(ComposerJson)) 49 | } 50 | 51 | def getStringValue(value: PsiElement): Option[String] = { 52 | import PsiExtractors.JsonStringLiteral 53 | 54 | value match { 55 | case JsonStringLiteral(x) => Some(x) 56 | case _ => None 57 | } 58 | } 59 | 60 | def getBooleanValue(value: PsiElement): Option[Boolean] = { 61 | ensureJsonBoolean(value) 62 | .map(_.getText) 63 | .flatMap(booleans.get) 64 | } 65 | 66 | def findParentProperty(value: JsonElement): Option[JsonProperty] = { 67 | @tailrec 68 | def loop(element: PsiElement): Option[JsonProperty] = { 69 | Option(element.getParent) match { 70 | case Some(parent) => 71 | ensureJsonProperty(parent) match { 72 | case Some(property) => Some(property) 73 | case None => loop(parent) 74 | } 75 | case None => 76 | None 77 | } 78 | } 79 | 80 | loop(value) 81 | } 82 | 83 | def findProperty(jsonObject: JsonObject, propertyName: String): Option[JsonProperty] = { 84 | import scala.jdk.CollectionConverters._ 85 | jsonObject.getPropertyList.asScala.find(_.getName == propertyName) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/PsiExtractors.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.json.psi._ 4 | import com.intellij.psi.PsiWhiteSpace 5 | import com.intellij.psi.impl.source.tree.LeafPsiElement 6 | import org.psliwa.idea.composerJson.util.ImplicitConversions._ 7 | 8 | private object PsiExtractors { 9 | object JsonFile { 10 | def unapply(x: JsonFile): Option[Option[JsonValue]] = Option(Option(x.getTopLevelValue)) 11 | } 12 | 13 | object JsonObject { 14 | def unapply(x: JsonObject): Option[java.util.List[JsonProperty]] = Some(x.getPropertyList) 15 | } 16 | 17 | object JsonProperty { 18 | def unapply(x: JsonProperty): Option[(String, JsonValue)] = Some((x.getName, x.getValue)) 19 | } 20 | 21 | object JsonArray { 22 | def unapply(x: JsonArray): Option[java.util.List[JsonValue]] = Some(x.getValueList) 23 | } 24 | 25 | object JsonValue { 26 | def unapply(x: JsonValue): Option[String] = Option(x.getText) 27 | } 28 | 29 | object JsonStringLiteral { 30 | def unapply(x: JsonStringLiteral): Option[String] = Some(x.getText.stripQuotes) 31 | } 32 | 33 | object JsonBooleanLiteral { 34 | def unapply(x: JsonBooleanLiteral): Option[Boolean] = Some(x.getText.toBoolean) 35 | } 36 | 37 | object JsonNumberLiteral { 38 | def unapply(x: JsonNumberLiteral): Option[Unit] = Some(()) 39 | } 40 | 41 | object LeafPsiElement { 42 | def unapply(x: LeafPsiElement): Option[String] = Some(x.getText) 43 | } 44 | 45 | object PsiWhiteSpace { 46 | def unapply(x: PsiWhiteSpace): Option[Unit] = Some(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/AbstractInspection.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInspection.{InspectionManager, LocalInspectionTool, ProblemDescriptor, ProblemsHolder} 4 | import com.intellij.psi.{PsiElement, PsiFile} 5 | import org.psliwa.idea.composerJson._ 6 | import org.psliwa.idea.composerJson.intellij.PsiElements 7 | import org.psliwa.idea.composerJson.json.Schema 8 | import PsiElements._ 9 | 10 | abstract class AbstractInspection extends LocalInspectionTool { 11 | val schema: Option[Schema] = ComposerSchema 12 | 13 | final override def checkFile(file: PsiFile, 14 | manager: InspectionManager, 15 | isOnTheFly: Boolean): Array[ProblemDescriptor] = { 16 | if (file.getName != ComposerJson) Array() 17 | else doCheckFile(file, manager, isOnTheFly) 18 | } 19 | 20 | private def doCheckFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array[ProblemDescriptor] = { 21 | val problems = new ProblemsHolder(manager, file, isOnTheFly) 22 | 23 | for { 24 | file <- ensureJsonFile(file) 25 | schema <- schema 26 | topLevelValue <- Option(file.getTopLevelValue) 27 | } yield collectProblems(topLevelValue, schema, problems) 28 | 29 | problems.getResultsArray 30 | } 31 | 32 | protected def collectProblems(element: PsiElement, schema: Schema, problems: ProblemsHolder): Unit 33 | 34 | override def getStaticDescription: String = "" 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/AbstractReferenceContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.json.psi.{JsonArray, JsonObject, JsonProperty} 4 | import com.intellij.patterns.ElementPattern 5 | import com.intellij.patterns.PlatformPatterns._ 6 | import com.intellij.psi.{PsiElement, PsiReferenceContributor, PsiReferenceProvider, PsiReferenceRegistrar} 7 | import org.psliwa.idea.composerJson._ 8 | import org.psliwa.idea.composerJson.intellij.PsiElements 9 | import org.psliwa.idea.composerJson.json.{SArray, SObject, SOr, Schema} 10 | import PsiElements.rootPsiElementPattern 11 | 12 | abstract class AbstractReferenceContributor extends PsiReferenceContributor { 13 | private val schema = ComposerSchema 14 | 15 | final override def registerReferenceProviders(registrar: PsiReferenceRegistrar): Unit = { 16 | schema 17 | .map(schemaToPatterns) 18 | .foreach( 19 | _.foreach { matcher => 20 | registrar.registerReferenceProvider(matcher.pattern, matcher.provider) 21 | } 22 | ) 23 | } 24 | 25 | private def schemaToPatterns(s: Schema): List[ReferenceMatcher] = { 26 | def loop(s: Schema, parent: Capture): List[ReferenceMatcher] = s match { 27 | case SObject(properties, _) => 28 | properties.named.toList.flatMap { 29 | case (name, property) => 30 | loop( 31 | property.schema, 32 | psiElement(classOf[JsonProperty]) 33 | .withName(name) 34 | .withParent(psiElement(classOf[JsonObject]).withParent(parent)) 35 | ) 36 | } 37 | case SArray(item) => 38 | loop(item, psiElement(classOf[JsonArray]).withParent(parent)) 39 | case SOr(items) => items.flatMap(loop(_, parent)) 40 | case _ => schemaToPatterns(s, parent) 41 | } 42 | 43 | loop(s, rootPsiElementPattern) 44 | } 45 | 46 | protected def schemaToPatterns(s: Schema, parent: Capture): List[ReferenceMatcher] 47 | 48 | protected class ReferenceMatcher(val pattern: ElementPattern[_ <: PsiElement], val provider: PsiReferenceProvider) 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/AutoPopupInsertHandler.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInsight.AutoPopupController 4 | import com.intellij.codeInsight.completion.{InsertHandler, InsertionContext} 5 | import com.intellij.codeInsight.lookup.LookupElement 6 | 7 | private[intellij] class AutoPopupInsertHandler( 8 | insertHandler: Option[InsertHandler[LookupElement]], 9 | condition: InsertionContext => Boolean = _ => true 10 | ) extends InsertHandler[LookupElement] { 11 | 12 | def this(insertHandler: InsertHandler[LookupElement]) = { 13 | this(Some(insertHandler)) 14 | } 15 | 16 | override def handleInsert(context: InsertionContext, item: LookupElement): Unit = { 17 | insertHandler.foreach(_.handleInsert(context, item)) 18 | 19 | if (condition(context)) { 20 | val editor = context.getEditor 21 | AutoPopupController.getInstance(editor.getProject).scheduleAutoPopup(editor) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/BaseLookupElement.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import javax.swing.Icon 4 | 5 | import com.intellij.codeInsight.completion.{InsertHandler, InsertionContext} 6 | import com.intellij.codeInsight.lookup.{LookupElement, LookupElementPresentation, LookupValueWithPriority} 7 | import com.intellij.psi.PsiElement 8 | 9 | final private[codeAssist] class BaseLookupElement( 10 | val name: String, 11 | val icon: Option[Icon] = None, 12 | val quoted: Boolean = true, 13 | val insertHandler: Option[InsertHandler[LookupElement]] = None, 14 | val psiElement: Option[PsiElement] = None, 15 | val description: String = "", 16 | val priority: Option[Int] = None 17 | ) extends LookupElement { 18 | 19 | private val presentation = new LookupElementPresentation 20 | presentation.setIcon(icon.orNull) 21 | presentation.setItemText(name) 22 | presentation.setTypeGrayed(true) 23 | presentation.setTypeText(if (description == "") null else description) 24 | presentation.setStrikeout(description.startsWith("DEPRECATED")) 25 | 26 | override def getLookupString: String = name 27 | override def renderElement(presentation: LookupElementPresentation): Unit = presentation.copyFrom(this.presentation) 28 | override def handleInsert(context: InsertionContext): Unit = insertHandler.foreach(_.handleInsert(context, this)) 29 | 30 | def withInsertHandler(insertHandler: InsertHandler[LookupElement]): BaseLookupElement = { 31 | new BaseLookupElement(name, icon, quoted, Some(insertHandler), psiElement, description, priority) 32 | } 33 | 34 | def withPsiElement(psiElement: PsiElement): BaseLookupElement = { 35 | new BaseLookupElement(name, icon, quoted, insertHandler, Some(psiElement), description, priority) 36 | } 37 | 38 | override def getObject: AnyRef = psiElement.getOrElse(this) 39 | 40 | override def equals(other: Any): Boolean = other match { 41 | case that: BaseLookupElement => 42 | name == that.name && 43 | icon == that.icon && 44 | quoted == that.quoted && 45 | insertHandler == that.insertHandler && 46 | psiElement == that.psiElement && 47 | description == that.description 48 | case _ => false 49 | } 50 | 51 | override def hashCode(): Int = { 52 | val state = Seq(name, icon, quoted, insertHandler, psiElement, description) 53 | state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/ComposerJsonSchemaExclusion.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import com.jetbrains.jsonSchema.remote.JsonSchemaCatalogExclusion 5 | import org.psliwa.idea.composerJson.ComposerJson 6 | 7 | class ComposerJsonSchemaExclusion extends JsonSchemaCatalogExclusion { 8 | override def isExcluded(file: VirtualFile): Boolean = file.getName == ComposerJson 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/IntentionActionQuickFixAdapter.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInsight.intention.IntentionAction 4 | import com.intellij.codeInspection.{LocalQuickFix, ProblemDescriptor} 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.fileEditor.FileEditorManager 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.psi.PsiFile 9 | 10 | private class IntentionActionQuickFixAdapter(action: IntentionAction, file: PsiFile) extends LocalQuickFix { 11 | override def getName: String = action.getText 12 | 13 | override def getFamilyName: String = action.getFamilyName 14 | 15 | override def applyFix(project: Project, descriptor: ProblemDescriptor): Unit = { 16 | action.invoke(project, editorFor(project).orNull, file) 17 | } 18 | 19 | private def editorFor(project: Project): Option[Editor] = { 20 | Option(FileEditorManager.getInstance(project).getSelectedTextEditor) 21 | } 22 | 23 | override def startInWriteAction(): Boolean = action.startInWriteAction() 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/QuickFix.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.openapi.editor.{Document, Editor} 4 | import com.intellij.openapi.fileEditor.FileEditorManager 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.psi.{PsiDocumentManager, PsiElement, PsiFile} 7 | import org.psliwa.idea.composerJson.json._ 8 | 9 | import scala.annotation.tailrec 10 | 11 | private object QuickFix { 12 | def getHeadOffset(e: PsiElement): Int = { 13 | @tailrec 14 | def loop(e: PsiElement, offset: Int): Int = { 15 | e match { 16 | case _: PsiFile => offset 17 | case _ => loop(e.getParent, e.getStartOffsetInParent + offset) 18 | } 19 | } 20 | 21 | loop(e, 0) 22 | } 23 | 24 | def documentFor(project: Project, file: PsiFile): Option[Document] = { 25 | Option(PsiDocumentManager.getInstance(project).getDocument(file)) 26 | } 27 | 28 | def editorFor(project: Project): Option[Editor] = { 29 | Option(FileEditorManager.getInstance(project).getSelectedTextEditor) 30 | } 31 | 32 | @tailrec 33 | def getEmptyValue(s: Schema): String = s match { 34 | case SObject(_, _) => "{}" 35 | case SArray(_) => "[]" 36 | case SString(_) | SStringChoice(_) => "\"\"" 37 | case SOr(h :: _) => getEmptyValue(h) 38 | case _ => "" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/QuickFixIntentionActionAdapter.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInsight.intention.IntentionAction 4 | import com.intellij.codeInspection.LocalQuickFixOnPsiElement 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.util.Comparing 8 | import com.intellij.psi.PsiFile 9 | 10 | private class QuickFixIntentionActionAdapter(quickFix: LocalQuickFixOnPsiElement) extends IntentionAction { 11 | override def getText: String = quickFix.getName 12 | override def getFamilyName: String = quickFix.getFamilyName 13 | override def invoke(project: Project, editor: Editor, file: PsiFile): Unit = quickFix.applyFix() 14 | override def startInWriteAction(): Boolean = true 15 | override def isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean = { 16 | quickFix.getStartElement != null && quickFix.isAvailable( 17 | project, 18 | file, 19 | quickFix.getStartElement, 20 | if (quickFix.getEndElement == null) quickFix.getStartElement else quickFix.getEndElement 21 | ) 22 | } 23 | } 24 | 25 | private class QuickFixIntentionActionAdapterWithPriority(quickFix: LocalQuickFixOnPsiElement, private val priority: Int) 26 | extends QuickFixIntentionActionAdapter(quickFix) 27 | with Comparable[IntentionAction] { 28 | override def compareTo(o: IntentionAction): Int = { 29 | o match { 30 | case p: QuickFixIntentionActionAdapterWithPriority => 31 | val diff = p.priority - priority 32 | if (diff == 0) { 33 | Comparing.compare(getText, o.getText) 34 | } else { 35 | p.priority - priority 36 | } 37 | case _ => 38 | Comparing.compare(getText, o.getText) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/QuoteInsertHandler.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInsight.completion.{InsertHandler, InsertionContext} 4 | import com.intellij.codeInsight.lookup.LookupElement 5 | import com.intellij.json.JsonElementTypes 6 | import com.intellij.psi.impl.source.tree.LeafPsiElement 7 | import com.intellij.psi.tree.IElementType 8 | 9 | private object QuoteInsertHandler extends InsertHandler[LookupElement] { 10 | override def handleInsert(context: InsertionContext, item: LookupElement): Unit = { 11 | item.getPsiElement match { 12 | case LeafPsiElement(JsonElementTypes.DOUBLE_QUOTED_STRING) => //there are already quotes 13 | case LeafPsiElement(_) => 14 | val document = context.getEditor.getDocument 15 | val editor = context.getEditor 16 | 17 | document.insertString(context.getStartOffset, "\"") 18 | document.insertString(context.getStartOffset + 1 + item.getLookupString.length, "\"") 19 | editor.getCaretModel.moveToOffset(context.getStartOffset + item.getLookupString.length + 1) 20 | case _ => 21 | } 22 | } 23 | 24 | private object LeafPsiElement { 25 | def unapply(x: LeafPsiElement): Option[IElementType] = Some(x.getElementType) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/References.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | private[codeAssist] object References { 4 | def getFixedReferenceName(s: String): String = 5 | s.replace(EmptyNamePlaceholder + " ", "").replace("\\\\", "\\").stripPrefix("\"").stripSuffix("\"") 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/RemoveJsonElementQuickFix.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInspection.LocalQuickFixOnPsiElement 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.{PsiElement, PsiFile} 6 | import org.psliwa.idea.composerJson.ComposerBundle 7 | import org.psliwa.idea.composerJson.intellij.PsiExtractors.JsonStringLiteral 8 | 9 | import scala.annotation.tailrec 10 | 11 | private class RemoveJsonElementQuickFix(element: PsiElement, text: String) extends LocalQuickFixOnPsiElement(element) { 12 | 13 | import org.psliwa.idea.composerJson.intellij.PsiExtractors.{JsonProperty, LeafPsiElement} 14 | 15 | override def invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement): Unit = { 16 | if (nextPropertyElementOf(startElement).isEmpty) { 17 | previousCommaElementOf(startElement).foreach(_.delete()) 18 | } 19 | 20 | nextCommaElementOf(startElement).foreach(_.delete()) 21 | 22 | startElement.delete() 23 | } 24 | 25 | private def nextCommaElementOf: PsiElement => Option[PsiElement] = 26 | findSibling(isCommaElement, x => Option(x.getNextSibling), isJsonProperty) 27 | private def previousCommaElementOf: PsiElement => Option[PsiElement] = 28 | findSibling(isCommaElement, x => Option(x.getPrevSibling), isJsonProperty) 29 | private def nextPropertyElementOf: PsiElement => Option[PsiElement] = 30 | findSibling(isJsonProperty, x => Option(x.getNextSibling)) 31 | 32 | private def isCommaElement(e: PsiElement): Boolean = e match { 33 | case LeafPsiElement(",") => true 34 | case _ => false 35 | } 36 | 37 | private def isJsonProperty(e: PsiElement): Boolean = e match { 38 | case JsonProperty(_, _) | JsonStringLiteral(_) => true 39 | case _ => false 40 | } 41 | 42 | private def findSibling( 43 | thatsIt: PsiElement => Boolean, 44 | nextSibling: PsiElement => Option[PsiElement], 45 | stop: PsiElement => Boolean = _ => false 46 | )(e: PsiElement): Option[PsiElement] = { 47 | @tailrec 48 | def loop(e: Option[PsiElement]): Option[PsiElement] = { 49 | e match { 50 | case Some(x) if thatsIt(x) => Some(x) 51 | case Some(x) if stop(x) => None 52 | case Some(x) => loop(nextSibling(x)) 53 | case None => None 54 | } 55 | } 56 | 57 | loop(nextSibling(e)) 58 | } 59 | 60 | override def getText: String = text 61 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/SetPropertyValueQuickFix.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInspection.LocalQuickFixOnPsiElement 4 | import com.intellij.json.psi.{JsonObject, JsonProperty} 5 | import com.intellij.openapi.editor.Document 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.psi.{PsiElement, PsiFile} 9 | import org.psliwa.idea.composerJson.ComposerBundle 10 | import org.psliwa.idea.composerJson.json.Schema 11 | import org.psliwa.idea.composerJson.intellij.PsiElements.findProperty 12 | import QuickFix._ 13 | 14 | private class SetPropertyValueQuickFix( 15 | element: JsonObject, 16 | propertyName: String, 17 | propertySchema: Schema, 18 | propertyValue: String 19 | ) extends LocalQuickFixOnPsiElement(element) { 20 | override def getText: String = 21 | ComposerBundle.message("inspection.quickfix.setPropertyValue", propertyName, propertyValue) 22 | 23 | override def invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement): Unit = { 24 | import org.psliwa.idea.composerJson.intellij.PsiExtractors.JsonProperty 25 | 26 | findProperty(element, propertyName) match { 27 | case Some(p @ JsonProperty(_, _)) => setPropertyValue(p) 28 | case None => createProperty() 29 | } 30 | } 31 | 32 | private def createProperty(): Unit = { 33 | new CreatePropertyQuickFix(element, propertyName, propertySchema).applyFix() 34 | 35 | for { 36 | document <- documentFor(element.getProject, element.getContainingFile) 37 | editor <- editorFor(element.getProject) 38 | } yield { 39 | val offset = editor.getCaretModel.getOffset 40 | 41 | document.insertString(editor.getCaretModel.getOffset, fixValue(propertyValue, document, offset)) 42 | } 43 | } 44 | 45 | private def fixValue(value: CharSequence, document: Document, offset: Int): String = { 46 | (if (document.getCharsSequence.charAt(offset - 1) == ':') " " else "") + value 47 | } 48 | 49 | private def setPropertyValue(property: JsonProperty): Unit = { 50 | for { 51 | document <- documentFor(element.getProject, element.getContainingFile) 52 | _ <- editorFor(element.getProject) 53 | range <- Option(property.getValue).map(_.getTextRange).orElse(Some(valueTextRangeFor(property))) 54 | } yield { 55 | val wrappedValue = fixValue(wrapValue(propertyValue), document, range.getStartOffset) 56 | document.replaceString(range.getStartOffset, range.getEndOffset, wrappedValue) 57 | } 58 | } 59 | 60 | private def valueTextRangeFor(property: JsonProperty) = { 61 | new TextRange(property.getTextRange.getEndOffset, property.getTextRange.getEndOffset) 62 | } 63 | 64 | private def wrapValue(s: String): CharSequence = { 65 | val wrapper = getEmptyValue(propertySchema) 66 | val (prefix, suffix) = wrapper.splitAt(wrapper.length / 2) 67 | 68 | prefix + s + suffix 69 | } 70 | 71 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/CustomRepositoriesEditorNotificationProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.fileEditor.FileEditor 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.util.Key 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import com.intellij.ui.{EditorNotificationPanel, EditorNotifications} 9 | import org.psliwa.idea.composerJson._ 10 | import org.psliwa.idea.composerJson.composer.model.repository.RepositoryProvider 11 | import org.psliwa.idea.composerJson.intellij.codeAssist.BaseLookupElement 12 | import org.psliwa.idea.composerJson.settings.ProjectSettings 13 | 14 | class CustomRepositoriesEditorNotificationProvider(notifications: EditorNotifications, project: Project) 15 | extends EditorNotifications.Provider[EditorNotificationPanel] { 16 | import CustomRepositoriesEditorNotificationProvider._ 17 | 18 | override def getKey: Key[EditorNotificationPanel] = key 19 | 20 | override def createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel = { 21 | if (file.getName == ComposerJson && isCustomRepositoriesSupportUnspecified(file)) { 22 | val panel = new EditorNotificationPanel() 23 | .text(ComposerBundle.message("editorNotifications.customRepositories")) 24 | 25 | panel.createActionLabel( 26 | ComposerBundle.message("editorNotifications.customRepositories.yes"), 27 | () => { 28 | getSettings().enable(file.getCanonicalPath) 29 | notifications.updateNotifications(file) 30 | } 31 | ) 32 | panel.createActionLabel( 33 | ComposerBundle.message("editorNotifications.customRepositories.no"), 34 | () => { 35 | getSettings().disable(file.getCanonicalPath) 36 | notifications.updateNotifications(file) 37 | } 38 | ) 39 | 40 | panel 41 | } else { 42 | null 43 | } 44 | } 45 | 46 | private def getSettings(): ProjectSettings.CustomRepositoriesSettings = { 47 | ProjectSettings.getInstance(project).getCustomRepositoriesSettings 48 | } 49 | 50 | private def isCustomRepositoriesSupportUnspecified(file: VirtualFile): Boolean = { 51 | !getRepositoryProvider.hasDefaultRepository(file.getCanonicalPath) && 52 | getSettings().isUnspecified(file.getCanonicalPath) 53 | } 54 | 55 | private def getRepositoryProvider: RepositoryProvider[_ <: BaseLookupElement] = { 56 | ApplicationManager.getApplication.getComponent(classOf[PackagesLoader]).repositoryProviderFor(project) 57 | } 58 | } 59 | 60 | private object CustomRepositoriesEditorNotificationProvider { 61 | val key: Key[EditorNotificationPanel] = Key.create("Custom repositories") 62 | val EnableAction = "enable" 63 | val DisableAction = "disable" 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/ExcludePatternAction.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.codeInsight.intention.IntentionAction 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.PsiFile 8 | import org.psliwa.idea.composerJson.ComposerBundle 9 | import org.psliwa.idea.composerJson.settings.{PatternItem, ProjectSettings} 10 | 11 | private class ExcludePatternAction(pattern: String) extends IntentionAction { 12 | override def getText: String = ComposerBundle.message("inspection.quickfix.excludePackagePattern", pattern) 13 | 14 | private def settings(project: Project) = { 15 | ProjectSettings(project) 16 | } 17 | 18 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 19 | 20 | override def isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean = true 21 | 22 | override def invoke(project: Project, editor: Editor, file: PsiFile): Unit = { 23 | settings(project).getUnboundedVersionInspectionSettings.addExcludedPattern(new PatternItem(pattern)) 24 | //force reanalyse file 25 | DaemonCodeAnalyzer.getInstance(project).restart(file) 26 | } 27 | 28 | override def startInWriteAction(): Boolean = true 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/InstallPackagesAction.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.codeInsight.intention.IntentionAction 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.fileEditor.FileDocumentManager 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.PsiFile 8 | import org.psliwa.idea.composerJson.ComposerBundle 9 | import org.psliwa.idea.composerJson.composer._ 10 | import org.psliwa.idea.composerJson.composer.command.DefaultPackagesInstaller 11 | import org.psliwa.idea.composerJson.composer.model.PackageName 12 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 13 | import org.psliwa.idea.composerJson.intellij.codeAssist.composer.NotInstalledPackages._ 14 | 15 | private object InstallPackagesAction extends IntentionAction { 16 | 17 | override def getText: String = ComposerBundle.message("inspection.quickfix.installNotInstalledPackages") 18 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 19 | 20 | override def invoke(project: Project, editor: Editor, file: PsiFile): Unit = { 21 | 22 | val documentManager = FileDocumentManager.getInstance() 23 | 24 | for { 25 | document <- Option(documentManager.getDocument(file.getVirtualFile)) 26 | } yield documentManager.saveDocument(document) 27 | 28 | val installedPackages = InstalledPackages.forFile(file.getVirtualFile) 29 | 30 | val packages = for { 31 | jsonFile <- ensureJsonFile(file).toList 32 | topValue <- Option(jsonFile.getTopLevelValue).toList 33 | packageName <- getNotInstalledPackageProperties(topValue, installedPackages).map( 34 | property => PackageName(property.getName) 35 | ) 36 | } yield packageName 37 | 38 | if (packages.nonEmpty) { 39 | new DefaultPackagesInstaller(project, file).install(packages) 40 | } 41 | } 42 | 43 | override def startInWriteAction(): Boolean = false 44 | 45 | override def isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean = true 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/NotInstalledPackageInspection.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.codeInspection.ProblemsHolder 4 | import com.intellij.json.psi.JsonProperty 5 | import com.intellij.psi.PsiElement 6 | import org.psliwa.idea.composerJson.ComposerBundle 7 | import org.psliwa.idea.composerJson.composer.InstalledPackages 8 | import org.psliwa.idea.composerJson.intellij.codeAssist.{AbstractInspection, IntentionActionQuickFixAdapter} 9 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem.ProblemDescriptor 10 | import org.psliwa.idea.composerJson.json.Schema 11 | 12 | class NotInstalledPackageInspection extends AbstractInspection { 13 | 14 | override protected def collectProblems(element: PsiElement, schema: Schema, problems: ProblemsHolder): Unit = { 15 | val installedPackages = InstalledPackages.forFile(element.getContainingFile.getVirtualFile) 16 | val notInstalledPackageProperties = 17 | NotInstalledPackages.getNotInstalledPackageProperties(element, installedPackages) 18 | 19 | notInstalledPackageProperties 20 | .map(createProblem) 21 | .foreach( 22 | problem => problems.registerProblem(problem.element, problem.message.getOrElse(""), problem.quickFixes: _*) 23 | ) 24 | } 25 | 26 | private def createProblem(property: JsonProperty): ProblemDescriptor[IntentionActionQuickFixAdapter] = { 27 | ProblemDescriptor( 28 | property, 29 | ComposerBundle.message("inspection.notInstalledPackage.packageIsNotInstalled", property.getName), 30 | Seq(new IntentionActionQuickFixAdapter(InstallPackagesAction, property.getContainingFile)) 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/NotInstalledPackages.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.json.psi.JsonProperty 4 | import com.intellij.psi.PsiElement 5 | import org.psliwa.idea.composerJson.composer.model.{PackageName, Packages} 6 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | private object NotInstalledPackages { 11 | def getNotInstalledPackageProperties(element: PsiElement, installedPackages: Packages): Seq[JsonProperty] = 12 | for { 13 | jsonObject <- ensureJsonObject(element).toList 14 | (propertyName, devPred) <- List("require" -> ((_: Boolean) == false), "require-dev" -> ((_: Boolean) => true)) 15 | property <- findProperty(jsonObject, propertyName).toList 16 | packagesObject <- Option(property.getValue).toList 17 | packagesObject <- ensureJsonObject(packagesObject).toList 18 | packageProperty <- packagesObject.getPropertyList.asScala 19 | if isNotInstalled(packageProperty, devPred, installedPackages) 20 | } yield packageProperty 21 | 22 | private def isNotInstalled(property: JsonProperty, 23 | devPredicate: Boolean => Boolean, 24 | installedPackages: Packages): Boolean = { 25 | property.getName.contains("/") && 26 | !getPackageVersion(property).isEmpty && 27 | !installedPackages.get(PackageName(property.getName)).map(_.isDev).exists(devPredicate) 28 | } 29 | 30 | def getPackageVersion(property: JsonProperty): String = { 31 | import scala.jdk.CollectionConverters._ 32 | 33 | val maybeVersion = for { 34 | value <- Option(property.getValue) 35 | stringLiteral <- ensureJsonStringLiteral(value) 36 | } yield stringLiteral.getTextFragments.asScala.foldLeft("")(_ + _.second) 37 | 38 | maybeVersion.getOrElse("") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/PackageDocumentationProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import java.util 4 | 5 | import com.intellij.lang.documentation.DocumentationProvider 6 | import com.intellij.psi.{PsiElement, PsiManager} 7 | import com.intellij.patterns.PlatformPatterns._ 8 | import com.intellij.patterns.PsiElementPattern 9 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 10 | import org.psliwa.idea.composerJson.composer.model.PackageDescriptor._ 11 | import org.psliwa.idea.composerJson.composer.model.PackageName 12 | 13 | class PackageDocumentationProvider extends DocumentationProvider { 14 | import PackageDocumentationProvider._ 15 | 16 | override def getQuickNavigateInfo(element: PsiElement, originalElement: PsiElement): String = null 17 | 18 | override def getDocumentationElementForLookupItem(psiManager: PsiManager, 19 | `object`: scala.Any, 20 | element: PsiElement): PsiElement = null 21 | 22 | override def getDocumentationElementForLink(psiManager: PsiManager, link: String, context: PsiElement): PsiElement = 23 | null 24 | 25 | override def getUrlFor(element: PsiElement, originalElement: PsiElement): util.List[String] = { 26 | if (packageNamePattern.accepts(originalElement)) { 27 | import scala.jdk.CollectionConverters._ 28 | documentationUrl(originalElement, PackageName(getStringValue(originalElement.getParent).getOrElse(""))).toList.asJava 29 | } else { 30 | null 31 | } 32 | } 33 | 34 | override def generateDoc(element: PsiElement, originalElement: PsiElement): String = null 35 | } 36 | 37 | private object PackageDocumentationProvider { 38 | val packageNamePattern: PsiElementPattern.Capture[PsiElement] = psiElement().withParent( 39 | packageElement.beforeLeaf(psiElement().withText(":")) 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/PackagesLoader.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.ApplicationComponent 5 | import com.intellij.openapi.fileEditor.{FileEditorManager, FileEditorManagerAdapter, FileEditorManagerListener} 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import org.psliwa.idea.composerJson._ 9 | import org.psliwa.idea.composerJson.composer.model.PackageName 10 | import org.psliwa.idea.composerJson.composer.model.repository._ 11 | import org.psliwa.idea.composerJson.composer.repository.{DefaultRepositoryProvider, Packagist} 12 | import org.psliwa.idea.composerJson.intellij.codeAssist.BaseLookupElement 13 | import org.psliwa.idea.composerJson.settings.ProjectSettings 14 | import org.psliwa.idea.composerJson.util.Funcs._ 15 | 16 | import scala.collection.mutable 17 | 18 | class PackagesLoader extends ApplicationComponent { 19 | private val repositoryProviders = mutable.Map[Project, RepositoryProvider[_ <: BaseLookupElement]]() 20 | 21 | private lazy val loadPackageLookupElements = loadPackages.map(new BaseLookupElement(_, Some(Icons.Packagist))) 22 | private lazy val loadPackages = { 23 | if (isUnitTestMode) Nil 24 | else Packagist.loadPackages(Packagist.defaultUrl).getOrElse(Nil) 25 | } 26 | private val versionsLoader: PackageName => Seq[String] = 27 | memorize(30)(Packagist.loadVersions(Packagist.defaultUrl)(_).getOrElse(List())) 28 | private lazy val packagistRepository = Repository.callback(loadPackageLookupElements, versionsLoader) 29 | 30 | private val defaultRepositoryProvider = new DefaultRepositoryProvider(packagistRepository, new BaseLookupElement(_)) 31 | 32 | override def initComponent(): Unit = { 33 | val app = ApplicationManager.getApplication 34 | val bus = app.getMessageBus.connect(app) 35 | 36 | //load packages first time, when composer.json file is opened 37 | bus.subscribe( 38 | FileEditorManagerListener.FILE_EDITOR_MANAGER, 39 | new FileEditorManagerAdapter { 40 | override def fileOpened(source: FileEditorManager, file: VirtualFile): Unit = file.getName match { 41 | case ComposerJson => 42 | app.executeOnPooledThread(new Runnable { 43 | override def run(): Unit = loadPackageLookupElements 44 | }) 45 | case _ => 46 | } 47 | } 48 | ) 49 | } 50 | 51 | override def disposeComponent(): Unit = { 52 | repositoryProviders.clear() 53 | } 54 | override def getComponentName: String = "composer.packagesLoader" 55 | 56 | def repositoryProviderFor(project: Project): RepositoryProvider[_ <: BaseLookupElement] = { 57 | repositoryProviders.getOrElseUpdate(project, createRepositoryProvider(project)) 58 | } 59 | 60 | private def createRepositoryProvider(project: Project): RepositoryProvider[_ <: BaseLookupElement] = { 61 | val settings = ProjectSettings.getInstance(project) 62 | if (isUnitTestMode) new TestingRepositoryProvider 63 | else { 64 | new RepositoryProviderWrapper( 65 | defaultRepositoryProvider, 66 | packagistRepository, 67 | file => !settings.getCustomRepositoriesSettings.isEnabled(file) 68 | ) 69 | } 70 | } 71 | 72 | private def isUnitTestMode = ApplicationManager.getApplication.isUnitTestMode 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfo.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | private case class PackageInfo(offset: Int, info: String) 4 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfoCaretListener.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory 4 | import com.intellij.openapi.editor.{Editor, LogicalPosition} 5 | import com.intellij.openapi.editor.colors.EditorFontType 6 | import com.intellij.openapi.editor.event.{CaretAdapter, CaretEvent} 7 | import com.intellij.openapi.editor.ex.EditorEx 8 | import com.intellij.openapi.util.text.StringUtil 9 | import org.psliwa.idea.composerJson.ComposerJson 10 | 11 | private class PackageInfoCaretListener(packagesInfoMap: Map[String, List[PackageInfo]]) extends CaretAdapter { 12 | 13 | override def caretPositionChanged(caretEvent: CaretEvent): Unit = { 14 | caretEvent.getEditor match { 15 | case editor: EditorEx if Option(editor.getVirtualFile).exists(_.getName == ComposerJson) => 16 | caretPositionChanged(editor, caretEvent.getNewPosition) 17 | 18 | case _ => 19 | } 20 | } 21 | 22 | private def caretPositionChanged(editor: EditorEx, position: LogicalPosition): Unit = { 23 | val color = editor.getColorsScheme.getAttributes(JsonSyntaxHighlighterFactory.JSON_BLOCK_COMMENT).getForegroundColor 24 | val font = editor.getColorsScheme.getFont(EditorFontType.CONSOLE_ITALIC) 25 | 26 | removeOverlays(editor) 27 | 28 | textToRender(editor, position) match { 29 | case Some((text, textPosition)) => 30 | val component = 31 | new PackageInfoOverlayView(editor, editor.logicalPositionToOffset(textPosition), text, color, font) 32 | editor.getContentComponent.add(component) 33 | val innerViewpoint = editor.getScrollPane.getViewport.getView 34 | component.setBounds(0, 0, innerViewpoint.getWidth, innerViewpoint.getHeight) 35 | 36 | case None => 37 | } 38 | } 39 | 40 | private def removeOverlays(editor: EditorEx): Unit = { 41 | editor.getContentComponent.getComponents.collect { 42 | case overlay: PackageInfoOverlayView => overlay 43 | } foreach editor.getContentComponent.remove 44 | } 45 | 46 | private def textToRender(editor: EditorEx, position: LogicalPosition): Option[(String, LogicalPosition)] = { 47 | (for { 48 | packageVersion <- packagesInfoMap.getOrElse(editor.getVirtualFile.getCanonicalPath, List.empty).view 49 | offset <- endLineOffset(editor, packageVersion.offset) 50 | packageVersionPosition = editor.offsetToLogicalPosition(offset) 51 | if position.line == packageVersionPosition.line 52 | } yield (packageVersion.info, packageVersionPosition)).headOption 53 | } 54 | 55 | private def endLineOffset(editor: Editor, offset: Int): Option[Int] = { 56 | lineNumber(editor, offset).map(editor.getDocument.getLineEndOffset) 57 | } 58 | 59 | private def lineNumber(editor: Editor, offset: Int): Option[Int] = { 60 | val lineNumber = StringUtil.offsetToLineNumber(editor.getDocument.getCharsSequence, offset) 61 | 62 | if (lineNumber >= 0) Option(lineNumber) 63 | else None 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfoInspection.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | import com.intellij.codeInspection.ProblemsHolder 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.psi.PsiElement 6 | import org.psliwa.idea.composerJson.composer.InstalledPackages 7 | import org.psliwa.idea.composerJson.intellij.PsiElements 8 | import org.psliwa.idea.composerJson.intellij.codeAssist.AbstractInspection 9 | import org.psliwa.idea.composerJson.json.Schema 10 | import PsiElements._ 11 | import org.psliwa.idea.composerJson.composer.model.{PackageDescriptor, PackageName} 12 | 13 | import scala.jdk.CollectionConverters._ 14 | 15 | class PackageInfoInspection extends AbstractInspection { 16 | override protected def collectProblems(element: PsiElement, schema: Schema, problems: ProblemsHolder): Unit = { 17 | val installedPackages = InstalledPackages.forFile(element.getContainingFile.getVirtualFile) 18 | 19 | def packageInfo(pkg: PackageDescriptor): String = { 20 | pkg.replacedBy.map(p => s"replaced by: ${p.name.presentation} (${p.version})").getOrElse(pkg.version) 21 | } 22 | 23 | val packagesInfo = for { 24 | jsonObject <- ensureJsonObject(element).toList 25 | propertyName <- List("require", "require-dev") 26 | property <- findProperty(jsonObject, propertyName).toList 27 | packagesObject <- Option(property.getValue).toList 28 | packagesObject <- ensureJsonObject(packagesObject).toList 29 | packageProperty <- packagesObject.getPropertyList.asScala 30 | pkg <- installedPackages.get(PackageName(packageProperty.getName)).toList 31 | } yield { 32 | PackageInfo(packageProperty.getTextOffset, packageInfo(pkg)) 33 | } 34 | 35 | Option(ApplicationManager.getApplication.getComponent(classOf[PackageInfoOverlay])) 36 | .foreach(_.setPackagesInfo(element.getContainingFile.getVirtualFile.getCanonicalPath, packagesInfo)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfoOverlay.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | import com.intellij.openapi.components.ApplicationComponent 4 | import com.intellij.openapi.editor.EditorFactory 5 | import com.intellij.openapi.editor.event.{CaretAdapter, CaretEvent} 6 | 7 | import scala.collection.mutable 8 | 9 | class PackageInfoOverlay(editorFactory: EditorFactory) extends ApplicationComponent { 10 | 11 | private val packagesInfoMap = mutable.Map[String, List[PackageInfo]]() 12 | private val caretListener = new CaretListener() 13 | 14 | override def initComponent(): Unit = { 15 | editorFactory.getEventMulticaster.addCaretListener(caretListener) 16 | } 17 | 18 | override def disposeComponent(): Unit = { 19 | editorFactory.getEventMulticaster.removeCaretListener(caretListener) 20 | } 21 | 22 | def setPackagesInfo(filePath: String, packagesInfo: List[PackageInfo]): Unit = { 23 | packagesInfoMap(filePath) = packagesInfo 24 | caretListener.refresh() 25 | } 26 | 27 | private[infoRenderer] def getPackagesInfo(filePath: String): List[PackageInfo] = { 28 | packagesInfoMap.getOrElse(filePath, List()) 29 | } 30 | 31 | private[infoRenderer] def clearPackagesInfo(): Unit = { 32 | packagesInfoMap.clear() 33 | caretListener.refresh() 34 | } 35 | 36 | override def getComponentName: String = "composer.editorOverlay" 37 | 38 | private class CaretListener extends CaretAdapter { 39 | var listener: PackageInfoCaretListener = newCaretListener 40 | 41 | override def caretPositionChanged(e: CaretEvent): Unit = { 42 | listener.caretPositionChanged(e) 43 | } 44 | 45 | private def newCaretListener = new PackageInfoCaretListener(packagesInfoMap.toMap) 46 | 47 | def refresh(): Unit = { 48 | listener = newCaretListener 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfoOverlayView.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | import java.awt.{Color, Font, Graphics} 4 | import javax.swing.JComponent 5 | 6 | import com.intellij.openapi.editor.Editor 7 | 8 | private class PackageInfoOverlayView(editor: Editor, offset: Int, text: String, color: Color, font: Font) 9 | extends JComponent { 10 | private val horizontalMargin = 40 11 | 12 | override def paintComponent(g: Graphics): Unit = { 13 | g.setColor(color) 14 | g.setFont(font) 15 | 16 | val verticalAlignment = editor.getLineHeight - editor.getColorsScheme.getEditorFontSize 17 | val point = editor.visualPositionToXY(editor.offsetToVisualPosition(offset)) 18 | g.drawString(text, point.x + horizontalMargin, point.y + editor.getLineHeight - verticalAlignment) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/package.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.json.JsonLanguage 4 | import com.intellij.json.psi.{JsonFile, JsonObject, JsonProperty, JsonStringLiteral} 5 | import com.intellij.patterns.PlatformPatterns._ 6 | import com.intellij.patterns.PsiElementPattern 7 | import com.intellij.patterns.StandardPatterns._ 8 | import org.psliwa.idea.composerJson._ 9 | 10 | package object composer { 11 | private[composer] def packageElement: PsiElementPattern.Capture[JsonStringLiteral] = { 12 | psiElement(classOf[JsonStringLiteral]) 13 | .inFile(psiFile(classOf[JsonFile]).withName(ComposerJson)) 14 | .withLanguage(JsonLanguage.INSTANCE) 15 | .withParent( 16 | psiElement(classOf[JsonProperty]).withParent( 17 | psiElement(classOf[JsonObject]).withParent( 18 | or( 19 | psiElement(classOf[JsonProperty]).withName("require"), 20 | psiElement(classOf[JsonProperty]).withName("require-dev") 21 | ) 22 | ) 23 | ) 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/FilePathReferenceContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.json.psi._ 4 | import com.intellij.patterns.PlatformPatterns._ 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.AbstractReferenceContributor 6 | import org.psliwa.idea.composerJson.json._ 7 | import org.psliwa.idea.composerJson.intellij.codeAssist.Capture 8 | 9 | class FilePathReferenceContributor extends AbstractReferenceContributor { 10 | 11 | override protected def schemaToPatterns(schema: Schema, parent: Capture): List[ReferenceMatcher] = schema match { 12 | case SFilePath(_) => 13 | List(new ReferenceMatcher(psiElement(classOf[JsonStringLiteral]).withParent(parent), FilePathReferenceProvider)) 14 | case SFilePaths(_) => 15 | val root = psiElement(classOf[JsonProperty]).withParent(psiElement(classOf[JsonObject]).withParent(parent)) 16 | List( 17 | new ReferenceMatcher(psiElement(classOf[JsonStringLiteral]).withParent(root).afterLeaf(":"), 18 | FilePathReferenceProvider), 19 | new ReferenceMatcher( 20 | psiElement(classOf[JsonStringLiteral]).withParent(psiElement(classOf[JsonArray]).withParent(root)), 21 | FilePathReferenceProvider 22 | ) 23 | ) 24 | case SPackages => 25 | val property = psiElement(classOf[JsonProperty]).withParent(psiElement(classOf[JsonObject]).withParent(parent)) 26 | List( 27 | new ReferenceMatcher(psiElement().beforeLeaf(":").withParent(property), PackageReferenceProvider), 28 | new ReferenceMatcher(psiElement().afterLeaf(":").withParent(property), PackageVersionReferenceProvider) 29 | ) 30 | case _ => Nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/FilePathReferenceProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet 4 | import com.intellij.psi.{PsiElement, PsiReference, PsiReferenceProvider} 5 | import com.intellij.util.ProcessingContext 6 | 7 | private object FilePathReferenceProvider extends PsiReferenceProvider { 8 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 9 | new FileReferenceSet(element) { 10 | override def isEndingSlashNotAllowed: Boolean = false 11 | }.getAllReferences.toArray[PsiReference] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/PackageReferenceProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.json.psi.JsonStringLiteral 4 | import com.intellij.openapi.util.TextRange 5 | import com.intellij.psi.impl.source.resolve.reference.impl.providers.{FileReference, FileReferenceSet} 6 | import com.intellij.psi.{ElementManipulators, PsiElement, PsiReference, PsiReferenceProvider} 7 | import com.intellij.util.ProcessingContext 8 | import org.psliwa.idea.composerJson 9 | import org.psliwa.idea.composerJson.composer.model.PackageName 10 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 11 | 12 | private object PackageReferenceProvider extends PsiReferenceProvider { 13 | private val EmptyReferences: Array[PsiReference] = Array() 14 | 15 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 16 | val maybeReferences = for { 17 | name <- ensureJsonStringLiteral(element) 18 | references <- nameToReferences(name) 19 | } yield references 20 | 21 | maybeReferences.getOrElse(EmptyReferences) 22 | } 23 | 24 | private def nameToReferences(nameElement: JsonStringLiteral): Option[Array[PsiReference]] = { 25 | val range = ElementManipulators.getValueTextRange(nameElement) 26 | val packageName = PackageName(range.substring(nameElement.getText)) 27 | 28 | packageName.`vendor/project` 29 | .map { 30 | case (vendor, project) if !project.contains(composerJson.EmptyPsiElementNamePlaceholder) => 31 | val set = new FileReferenceSet(s"vendor/$vendor/$project", nameElement, range.getStartOffset, this, true) 32 | Array[PsiReference]( 33 | new FileReference(set, new TextRange(1, vendor.length + 1), 0, s"vendor/$vendor"), 34 | new FileReference(set, new TextRange(1, vendor.length + project.length + 2), 0, s"vendor/$vendor/$project") 35 | ) 36 | case _ => Array() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/PackageVersionReferenceProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.psi.{PsiElement, PsiReference, PsiReferenceProvider} 4 | import com.intellij.util.ProcessingContext 5 | import org.psliwa.idea.composerJson.composer.model.PackageDescriptor._ 6 | import org.psliwa.idea.composerJson.composer.model.PackageName 7 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 8 | import org.psliwa.idea.composerJson.util.ImplicitConversions._ 9 | 10 | private object PackageVersionReferenceProvider extends PsiReferenceProvider { 11 | private val EmptyReferences: Array[PsiReference] = Array() 12 | 13 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 14 | val maybeReferences = for { 15 | propertyValue <- ensureJsonStringLiteral(element) 16 | property <- ensureJsonProperty(element.getParent) 17 | propertyName <- ensureJsonStringLiteral(property.getNameElement) 18 | packageName = PackageName(propertyName.getValue.stripQuotes) 19 | } yield Array[PsiReference](new UrlPsiReference(propertyValue) { 20 | override protected def url: Option[String] = documentationUrl(element, packageName) 21 | }) 22 | 23 | maybeReferences.getOrElse(EmptyReferences) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/UrlPsiReference.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.navigation.ItemPresentation 5 | import com.intellij.psi.{NavigatablePsiElement, PsiElement, PsiReferenceBase} 6 | import org.psliwa.idea.composerJson.intellij.PsiElementWrapper 7 | 8 | private class UrlPsiReference(element: PsiElement) extends PsiReferenceBase[PsiElement](element) { 9 | 10 | protected def url: Option[String] = Some(getValue) 11 | 12 | override def resolve: PsiElement = { 13 | new PsiElementWrapper(element) with NavigatablePsiElement { 14 | override def getParent: PsiElement = element.getParent 15 | override def navigate(requestFocus: Boolean): Unit = url.foreach(BrowserUtil.browse) 16 | override def canNavigate: Boolean = true 17 | override def canNavigateToSource: Boolean = true 18 | override def getNavigationElement: PsiElement = this 19 | override def getName: String = url.getOrElse("") 20 | override def getPresentation: ItemPresentation = null 21 | } 22 | } 23 | override def getVariants: Array[AnyRef] = UrlPsiReference.EmptyArray 24 | override def isSoft: Boolean = true 25 | } 26 | 27 | private object UrlPsiReference { 28 | val EmptyArray: Array[AnyRef] = Array[AnyRef]() 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/UrlReferenceContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.json.psi.JsonStringLiteral 4 | import com.intellij.patterns.PlatformPatterns._ 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.AbstractReferenceContributor 6 | import org.psliwa.idea.composerJson.json._ 7 | import org.psliwa.idea.composerJson.intellij.codeAssist._ 8 | 9 | class UrlReferenceContributor extends AbstractReferenceContributor { 10 | override protected def schemaToPatterns(schema: Schema, parent: Capture): List[ReferenceMatcher] = schema match { 11 | case SString(UriFormat | EmailFormat) => 12 | List(new ReferenceMatcher(psiElement(classOf[JsonStringLiteral]).withParent(parent), UrlReferenceProvider)) 13 | case _ => Nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/UrlReferenceProvider.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import com.intellij.psi.{PsiElement, PsiReference, PsiReferenceProvider} 4 | import com.intellij.util.ProcessingContext 5 | import org.psliwa.idea.composerJson.json.{EmailFormat, UriFormat} 6 | 7 | private object UrlReferenceProvider extends PsiReferenceProvider { 8 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 9 | val text = element.getText.substring(1, element.getText.length - 1) 10 | 11 | if (EmailFormat.isValid(text)) { 12 | Array(new UrlPsiReference(element) { 13 | override protected def url: Option[String] = Some("mailto:" + super.url) 14 | }) 15 | } else if (UriFormat.isValid(text)) { 16 | Array(new UrlPsiReference(element)) 17 | } else { 18 | Array() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/package.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij 2 | 3 | import com.intellij.codeInsight.completion.{CompletionParameters, InsertHandler, InsertionContext} 4 | import com.intellij.codeInsight.lookup.LookupElement 5 | import com.intellij.patterns.PsiElementPattern 6 | import com.intellij.psi.PsiElement 7 | import com.intellij.util.ProcessingContext 8 | import org.psliwa.idea.composerJson.util.CharOffsetFinder._ 9 | import org.psliwa.idea.composerJson.util.OffsetFinder.ImplicitConversions._ 10 | 11 | package object codeAssist { 12 | private[codeAssist] val EmptyNamePlaceholder = org.psliwa.idea.composerJson.EmptyPsiElementNamePlaceholder 13 | 14 | type Capture = PsiElementPattern.Capture[_ <: PsiElement] 15 | private[codeAssist] type InsertHandlerFinder = BaseLookupElement => Option[InsertHandler[LookupElement]] 16 | private[codeAssist] type LookupElements = CompletionParameters => Iterable[BaseLookupElement] 17 | 18 | private val autoPopupCondition = (context: InsertionContext) => { 19 | val text = context.getEditor.getDocument.getCharsSequence 20 | ensure('"' || ' ')(context.getEditor.getCaretModel.getOffset - 1)(text).isDefined 21 | } 22 | 23 | private[codeAssist] val StringPropertyValueInsertHandler = 24 | new AutoPopupInsertHandler(Some(new PropertyValueInsertHandler("\"\"")), autoPopupCondition) 25 | private[codeAssist] val ObjectPropertyValueInsertHandler = 26 | new AutoPopupInsertHandler(Some(new PropertyValueInsertHandler("{}")), autoPopupCondition) 27 | private[codeAssist] val ArrayPropertyValueInsertHandler = 28 | new AutoPopupInsertHandler(Some(new PropertyValueInsertHandler("[]")), autoPopupCondition) 29 | private[codeAssist] val EmptyPropertyValueInsertHandler = 30 | new AutoPopupInsertHandler(Some(new PropertyValueInsertHandler("")), autoPopupCondition) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/php/PhpClassInsertHandler.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.php 2 | 3 | import com.intellij.codeInsight.completion.{InsertHandler, InsertionContext} 4 | import com.intellij.codeInsight.lookup.LookupElement 5 | import com.jetbrains.php.lang.psi.elements.PhpClass 6 | import PhpUtils._ 7 | 8 | private object PhpClassInsertHandler extends InsertHandler[LookupElement] { 9 | override def handleInsert(context: InsertionContext, item: LookupElement): Unit = { 10 | for { 11 | phpClass <- ensurePhpClass(item.getObject) 12 | } yield { 13 | val document = context.getDocument 14 | document.insertString(context.getTailOffset, "::") 15 | context.getEditor.getCaretModel.moveToOffset(context.getTailOffset) 16 | document.insertString(context.getStartOffset, getFixedFQNamespace(phpClass)) 17 | } 18 | } 19 | 20 | private def ensurePhpClass(o: AnyRef): Option[PhpClass] = o match { 21 | case x: PhpClass => Option(x) 22 | case _ => None 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/php/PhpNamespaceReference.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.php 2 | 3 | import com.intellij.codeInsight.completion.impl.CamelHumpMatcher 4 | import com.intellij.codeInsight.lookup.LookupElementPresentation 5 | import com.intellij.json.psi.JsonStringLiteral 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.stubs.StubIndexKey 8 | import com.intellij.psi.{PsiElementResolveResult, PsiPolyVariantReferenceBase, ResolveResult} 9 | import com.jetbrains.php.completion.PhpLookupElement 10 | import com.jetbrains.php.{PhpIcons, PhpIndex} 11 | import org.psliwa.idea.composerJson.intellij.codeAssist.php.PhpNamespaceReference._ 12 | import org.psliwa.idea.composerJson.intellij.codeAssist.php.PhpUtils._ 13 | 14 | import scala.jdk.CollectionConverters._ 15 | 16 | private class PhpNamespaceReference(element: JsonStringLiteral) 17 | extends PsiPolyVariantReferenceBase[JsonStringLiteral](element) { 18 | private val namespaceName = getFixedReferenceName(element.getText) 19 | 20 | override def multiResolve(incompleteCode: Boolean): Array[ResolveResult] = { 21 | val phpIndex = PhpIndex.getInstance(element.getProject) 22 | 23 | phpIndex 24 | .getNamespacesByName("\\" + namespaceName.stripSuffix("\\")) 25 | .asScala 26 | .map(new PsiElementResolveResult(_)) 27 | .toArray 28 | } 29 | 30 | override def getVariants: Array[AnyRef] = { 31 | import org.psliwa.idea.composerJson.util.CharOffsetFinder._ 32 | import org.psliwa.idea.composerJson.util.OffsetFinder.ImplicitConversions._ 33 | 34 | val o = for { 35 | lastSlashOffset <- findOffsetReverse('\\')(namespaceName.length - 1)(namespaceName) 36 | } yield (namespaceName.substring(0, lastSlashOffset), namespaceName.substring(lastSlashOffset + 1)) 37 | 38 | val (parentNamespace, currentNamespace) = o match { 39 | case Some(x) => x 40 | case None => "" -> "" 41 | } 42 | 43 | val phpIndex = PhpIndex.getInstance(element.getProject) 44 | 45 | val methodMatcher = new CamelHumpMatcher(currentNamespace) 46 | 47 | phpIndex 48 | .getChildNamespacesByParentName(ensureLandingSlash(parentNamespace + "\\")) 49 | .asScala 50 | .filter(methodMatcher.prefixMatches) 51 | .map( 52 | namespace => 53 | new PhpNamespaceLookupElement(element.getProject, 54 | (parentNamespace + "\\" + namespace + "\\").stripPrefix("\\")) 55 | ) 56 | .toArray 57 | } 58 | } 59 | 60 | private object PhpNamespaceReference { 61 | lazy val NamespaceStubIndexKey: StubIndexKey[Nothing, Nothing] = 62 | StubIndexKey.createIndexKey("org.psliwa.idea.composerJson.phpNamespace") 63 | 64 | private class PhpNamespaceLookupElement(project: Project, namespace: String) 65 | extends PhpLookupElement( 66 | escapeSlashes(namespace), 67 | NamespaceStubIndexKey, 68 | PhpIcons.NAMESPACE, 69 | null, 70 | project, 71 | null 72 | ) { 73 | override def renderElement(presentation: LookupElementPresentation): Unit = { 74 | super.renderElement(presentation) 75 | presentation.setItemText(namespace) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/php/PhpReferenceContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.php 2 | 3 | import com.intellij.json.psi.{JsonProperty, JsonStringLiteral} 4 | import com.intellij.patterns.PlatformPatterns._ 5 | import com.intellij.patterns.StandardPatterns._ 6 | import com.intellij.psi._ 7 | import com.intellij.util.ProcessingContext 8 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 9 | import org.psliwa.idea.composerJson.intellij.codeAssist.scripts.ScriptsPsiElementPattern 10 | 11 | class PhpReferenceContributor extends PsiReferenceContributor { 12 | 13 | override def registerReferenceProviders(registrar: PsiReferenceRegistrar): Unit = { 14 | registerCallbackProvider(registrar) 15 | registerNamespaceProvider(registrar) 16 | } 17 | 18 | private def registerCallbackProvider(registrar: PsiReferenceRegistrar) { 19 | registrar.registerReferenceProvider( 20 | ScriptsPsiElementPattern.Pattern, 21 | PhpCallbackReferenceProvider 22 | ) 23 | } 24 | 25 | private def registerNamespaceProvider(registrar: PsiReferenceRegistrar): Unit = { 26 | val rootElement = psiElement(classOf[JsonProperty]) 27 | .withName("autoload", "autoload-dev") 28 | .withSuperParent(2, rootPsiElementPattern) 29 | 30 | registrar.registerReferenceProvider( 31 | psiElement(classOf[JsonStringLiteral]) 32 | .and( 33 | or( 34 | psiElement().beforeLeaf(psiElement().withText(":")), 35 | //1 element is property name, second PsiErrorElement 36 | psiElement().withParent(psiElement().withChildren(collection().last(psiElement(classOf[PsiErrorElement])))) 37 | ) 38 | ) 39 | .withParent(classOf[JsonProperty]) 40 | .withSuperParent( 41 | 3, 42 | psiElement(classOf[JsonProperty]) 43 | .withName("psr-0", "psr-4") 44 | .withSuperParent(2, rootElement) 45 | ), 46 | PhpNamespaceReferenceProvider 47 | ) 48 | } 49 | } 50 | 51 | private object PhpCallbackReferenceProvider extends PsiReferenceProvider { 52 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 53 | val maybeReferences = for { 54 | stringElement <- ensureJsonStringLiteral(element) 55 | } yield { 56 | Array[PsiReference](new PhpCallbackReference(stringElement)) 57 | } 58 | 59 | maybeReferences.getOrElse(Array()) 60 | } 61 | } 62 | 63 | private object PhpNamespaceReferenceProvider extends PsiReferenceProvider { 64 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 65 | val maybeReferences = for { 66 | property <- ensureJsonStringLiteral(element) 67 | } yield { 68 | Array[PsiReference](new PhpNamespaceReference(property)) 69 | } 70 | 71 | maybeReferences.getOrElse(Array()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/php/PhpUtils.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.php 2 | 3 | import com.jetbrains.php.lang.psi.elements.PhpClass 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.References 5 | 6 | private object PhpUtils { 7 | def getFixedFQNamespace(phpClass: PhpClass): String = escapeSlashes(phpClass.getNamespaceName.stripPrefix("\\")) 8 | 9 | def getFixedFQN(phpClass: PhpClass): String = escapeSlashes(phpClass.getFQN.stripPrefix("\\")) 10 | 11 | def getFixedReferenceName(s: String): String = References.getFixedReferenceName(s) 12 | 13 | def ensureLandingSlash(s: String): String = if (s.isEmpty || s.charAt(0) != '\\') "\\" + s else s 14 | 15 | def escapeSlashes(s: String): String = s.replace("\\", "\\\\") 16 | 17 | def getCallableInfo(s: String): (String, String) = { 18 | s.replace("::", "").splitAt(positive(s.indexOf("::"), s.length)) 19 | } 20 | 21 | private def positive(i: Int, default: => Int): Int = { 22 | if (i >= 0) i 23 | else default 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/CheckResult.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem 2 | 3 | private[codeAssist] case class CheckResult(value: Boolean, properties: Set[PropertyPath]) { 4 | def not = CheckResult(!value, properties) 5 | 6 | def &&(result: CheckResult) = CheckResult(value && result.value, properties ++ result.properties) 7 | 8 | def ||(result: CheckResult) = CheckResult(value || result.value, properties ++ result.properties) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/Condition.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import com.intellij.psi.PsiElement 5 | import org.psliwa.idea.composerJson.intellij.PsiExtractors 6 | import PropertyPath._ 7 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 8 | import scala.jdk.CollectionConverters._ 9 | 10 | import scala.util.matching.Regex 11 | 12 | sealed private[codeAssist] trait Condition { 13 | import Condition._ 14 | 15 | def check(jsonObject: JsonObject, propertyPath: PropertyPath): CheckResult = { 16 | val result = for { 17 | property <- findPropertiesInPath(jsonObject, propertyPath) 18 | value <- getValue(property.getValue) 19 | } yield CheckResult( 20 | this match { 21 | case ConditionIs(expected) => value == expected 22 | case ConditionIsNot(expected) => value != expected 23 | case ConditionNot(condition) => condition.check(jsonObject, propertyPath).not.value 24 | case ConditionMatch(pattern) => pattern.findFirstIn(value.toString).isDefined 25 | case ConditionDuplicateIn(dependencyPropertyPath) => 26 | (for { 27 | dependencyProperty <- findPropertiesInPath(jsonObject, dependencyPropertyPath) 28 | dependencyObject <- Option(dependencyProperty.getValue).flatMap(ensureJsonObject).toList 29 | _ <- findPropertiesInPath(dependencyObject, PropertyPath(propertyPath.lastProperty, List.empty)) 30 | } yield true).headOption.getOrElse(false) 31 | case ConditionExists => true 32 | }, 33 | Set(siblingPropertyPath(propertyPath, property.getName)) 34 | ) 35 | 36 | result.filter(_.value).foldLeft(CheckResult(value = false, Set.empty))(_ || _) 37 | } 38 | } 39 | 40 | private[codeAssist] case class ConditionIs(value: Any) extends Condition 41 | private[codeAssist] case class ConditionMatch(regex: Regex) extends Condition 42 | private[codeAssist] object ConditionExists extends Condition 43 | private[codeAssist] case class ConditionIsNot(value: Any) extends Condition 44 | private[codeAssist] case class ConditionNot(condition: Condition) extends Condition 45 | private[codeAssist] case class ConditionDuplicateIn(dependencyPropertyPath: PropertyPath) extends Condition 46 | 47 | private[codeAssist] object Condition { 48 | import PsiExtractors._ 49 | def getValue(element: PsiElement): Option[Any] = { 50 | element match { 51 | case JsonStringLiteral(value) => Some(value) 52 | case JsonBooleanLiteral(value) => Some(value) 53 | case JsonArray(value) => Some(value.asScala.toList) 54 | case _ => None 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/ProblemChecker.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem 2 | 3 | import com.intellij.codeInspection.{LocalQuickFixOnPsiElement, ProblemHighlightType} 4 | import com.intellij.json.psi.JsonObject 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem.checker.Checker 6 | 7 | import scala.language.implicitConversions 8 | 9 | private[codeAssist] case class ProblemChecker( 10 | checker: Checker, 11 | problem: String, 12 | createQuickFixes: (JsonObject, PropertyPath) => List[LocalQuickFixOnPsiElement] = (_, _) => List.empty, 13 | highlightType: ProblemHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING, 14 | elements: JsonObject => List[JsonObject] = List(_: JsonObject) 15 | ) extends Checker { 16 | override def check(jsonObject: JsonObject): CheckResult = checker.check(jsonObject) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/ProblemDescriptor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem 2 | 3 | import com.intellij.codeInspection.ProblemHighlightType 4 | import com.intellij.openapi.util.TextRange 5 | import com.intellij.psi.PsiElement 6 | 7 | private[codeAssist] case class ProblemDescriptor[QuickFix]( 8 | element: PsiElement, 9 | message: Option[String], 10 | quickFixes: Seq[QuickFix] = Seq(), 11 | private val maybeRange: Option[TextRange] = None, 12 | highlightType: ProblemHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING 13 | ) { 14 | lazy val range: TextRange = maybeRange.getOrElse(element.getTextRange) 15 | } 16 | 17 | private[codeAssist] object ProblemDescriptor { 18 | def apply[QuickFix](element: PsiElement, message: String, quickFixes: Seq[QuickFix]): ProblemDescriptor[QuickFix] = { 19 | ProblemDescriptor(element, Some(message), quickFixes) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/PropertyPath.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem 2 | 3 | import com.intellij.json.psi.{JsonObject, JsonProperty} 4 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 5 | import scala.jdk.CollectionConverters._ 6 | 7 | import scala.annotation.tailrec 8 | 9 | /*** 10 | * "*" char can be used as wildcard 11 | */ 12 | private[codeAssist] case class PropertyPath(headProperty: String, tailProperties: List[String]) { 13 | def /(property: String): PropertyPath = copy(tailProperties = tailProperties ++ List(property)) 14 | 15 | lazy val lastProperty: String = (headProperty :: tailProperties).last 16 | } 17 | 18 | private[codeAssist] object PropertyPath { 19 | def findPropertiesInPath(jsonObject: JsonObject, propertyPath: PropertyPath): List[JsonProperty] = { 20 | @tailrec 21 | def loop(jsonObjects: List[JsonObject], 22 | propertyPath: PropertyPath, 23 | foundProperties: List[JsonProperty]): List[JsonProperty] = { 24 | (jsonObjects.flatMap(findProperties(_, propertyPath.headProperty)), propertyPath) match { 25 | case (properties, PropertyPath(_, Nil)) => properties 26 | case (properties, PropertyPath(_, head :: tail)) => 27 | val newJsonObjects: List[JsonObject] = 28 | properties.flatMap(property => Option(property.getValue)).flatMap(ensureJsonObject) 29 | loop(newJsonObjects, PropertyPath(head, tail), properties) 30 | case _ => foundProperties 31 | } 32 | } 33 | 34 | def findProperties(jsonObject: JsonObject, propertyName: String): List[JsonProperty] = propertyName match { 35 | case "*" => jsonObject.getPropertyList.asScala.toList 36 | case name => Option(jsonObject.findProperty(name)).toList 37 | } 38 | 39 | loop(List(jsonObject), propertyPath, List.empty) 40 | } 41 | 42 | def siblingPropertyPath(propertyPath: PropertyPath, siblingPropertyName: String): PropertyPath = propertyPath match { 43 | case PropertyPath(_, Nil) => PropertyPath(siblingPropertyName, List.empty) 44 | case PropertyPath(rootProperty, tailProperties) => 45 | PropertyPath(rootProperty, tailProperties.dropRight(1) ++ List(siblingPropertyName)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/checker/Checker.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem.checker 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem.{CheckResult, PropertyPath} 5 | 6 | import scala.language.implicitConversions 7 | 8 | private[codeAssist] trait Checker { 9 | def check(jsonObject: JsonObject): CheckResult 10 | 11 | def &&(checker: Checker): Checker = AndChecker(this, checker) 12 | def ||(checker: Checker): Checker = OrChecker(this, checker) 13 | } 14 | 15 | private[codeAssist] object Checker { 16 | def not(checker: Checker): Checker = new Checker { 17 | override def check(jsonObject: JsonObject): CheckResult = checker.check(jsonObject).not 18 | } 19 | } 20 | 21 | private[codeAssist] object ImplicitConversions { 22 | implicit def stringToProblemChecker(property: String): PropertyChecker = PropertyChecker(property) 23 | implicit def propertyPathToProblemChecker(propertyPath: PropertyPath): PropertyChecker = PropertyChecker(propertyPath) 24 | implicit def stringToPropertyPath(property: String): PropertyPath = PropertyPath(property, List.empty) 25 | } 26 | 27 | private[codeAssist] case class AndChecker(checker1: Checker, checker2: Checker) extends Checker { 28 | override def check(jsonObject: JsonObject): CheckResult = checker1.check(jsonObject) && checker2.check(jsonObject) 29 | } 30 | 31 | private[codeAssist] case class OrChecker(checker1: Checker, checker2: Checker) extends Checker { 32 | override def check(jsonObject: JsonObject): CheckResult = checker1.check(jsonObject) || checker2.check(jsonObject) 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/checker/MultiplePropertiesChecker.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem.checker 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem.PropertyPath._ 6 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem.{CheckResult, Condition, PropertyPath} 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | private[codeAssist] case class MultiplePropertiesChecker(propertyPath: PropertyPath, condition: Condition) 11 | extends Checker { 12 | override def check(jsonObject: JsonObject): CheckResult = { 13 | val propertyPaths = (for { 14 | property <- findPropertiesInPath(jsonObject, propertyPath) 15 | propertyValue <- Option(property.getValue).toList 16 | propertyObject <- ensureJsonObject(propertyValue).toList 17 | propertyName <- propertyObject.getPropertyList.asScala.map(_.getName) 18 | } yield propertyPath / propertyName).toSet 19 | 20 | propertyPaths.map(condition.check(jsonObject, _)).foldLeft(CheckResult(value = false, Set.empty))(_ || _) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/problem/checker/PropertyChecker.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.problem.checker 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.problem._ 5 | import scala.util.matching.Regex 6 | import PropertyPath._ 7 | 8 | private[codeAssist] case class PropertyChecker(propertyPath: PropertyPath, condition: Condition = ConditionExists) 9 | extends Checker { 10 | def is(value: Any): PropertyChecker = copy(condition = ConditionIs(value)) 11 | def isNot(value: Any): PropertyChecker = copy(condition = ConditionIsNot(value)) 12 | def matches(regex: Regex): PropertyChecker = copy(condition = ConditionMatch(regex)) 13 | def duplicatesSibling(siblingPropertyName: String): MultiplePropertiesChecker = { 14 | MultiplePropertiesChecker(propertyPath, 15 | ConditionDuplicateIn(siblingPropertyPath(propertyPath, siblingPropertyName))) 16 | } 17 | 18 | override def check(jsonObject: JsonObject): CheckResult = condition.check(jsonObject, propertyPath) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/schema/CompletionContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.schema 2 | 3 | import com.intellij.codeInsight.completion._ 4 | import com.intellij.codeInsight.lookup.LookupElement 5 | import com.intellij.patterns.PlatformPatterns._ 6 | import org.psliwa.idea.composerJson.intellij.codeAssist._ 7 | import org.psliwa.idea.composerJson.intellij.codeAssist.{AbstractCompletionContributor, BaseLookupElement} 8 | import org.psliwa.idea.composerJson.json._ 9 | 10 | import scala.annotation.tailrec 11 | 12 | class CompletionContributor extends AbstractCompletionContributor { 13 | 14 | import AbstractCompletionContributor._ 15 | 16 | override protected def getCompletionProvidersForSchema( 17 | s: Schema, 18 | parent: Capture 19 | ): List[(Capture, CompletionProvider[CompletionParameters])] = s match { 20 | case SStringChoice(m) => 21 | List( 22 | (psiElement().withSuperParent(2, parent), 23 | new LookupElementsCompletionProvider(_ => m.map(new BaseLookupElement(_)))) 24 | ) 25 | case _ => List() 26 | } 27 | 28 | override protected def propertyCompletionProvider( 29 | parent: Capture, 30 | properties: Map[String, Property] 31 | ): List[(Capture, CompletionProvider[CompletionParameters])] = { 32 | propertyCompletionProvider( 33 | parent, 34 | (_: CompletionParameters) => properties.map(x => new BaseLookupElement(x._1, description = x._2.description)), 35 | k => insertHandlerFor(properties(k.name).schema) 36 | ) 37 | } 38 | 39 | @tailrec 40 | final override protected def insertHandlerFor(schema: Schema): Option[InsertHandler[LookupElement]] = schema match { 41 | case SString(_) | SStringChoice(_) | SFilePath(_) => Some(StringPropertyValueInsertHandler) 42 | case SObject(_, _) | SPackages | SFilePaths(_) => Some(ObjectPropertyValueInsertHandler) 43 | case SArray(_) => Some(ArrayPropertyValueInsertHandler) 44 | case SBoolean | SNumber => Some(EmptyPropertyValueInsertHandler) 45 | case SOr(h :: _) => insertHandlerFor(h) 46 | case _ => None 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/schema/RemoveQuotesQuickFix.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.schema 2 | 3 | import com.intellij.codeInspection.LocalQuickFixOnPsiElement 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.{PsiElement, PsiFile} 6 | import org.psliwa.idea.composerJson.ComposerBundle 7 | import org.psliwa.idea.composerJson.intellij.PsiElements 8 | import org.psliwa.idea.composerJson.intellij.codeAssist.QuickFix 9 | import QuickFix._ 10 | import org.psliwa.idea.composerJson.intellij.codeAssist.QuickFix 11 | import PsiElements._ 12 | 13 | private class RemoveQuotesQuickFix(element: PsiElement) extends LocalQuickFixOnPsiElement(element) { 14 | 15 | override def invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement): Unit = { 16 | for { 17 | stringLiteral <- ensureJsonStringLiteral(element) 18 | document <- documentFor(project, file) 19 | } yield { 20 | val headOffset = getHeadOffset(stringLiteral) 21 | val trailingOffset = headOffset + stringLiteral.getText.length - 2 22 | 23 | document.replaceString(headOffset, headOffset + 1, "") 24 | document.replaceString(trailingOffset, trailingOffset + 1, "") 25 | } 26 | } 27 | 28 | override def getText: String = ComposerBundle.message("inspection.quickfix.removeQuotes") 29 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/schema/ShowValidValuesQuickFix.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.schema 2 | 3 | import com.intellij.codeInsight.AutoPopupController 4 | import com.intellij.codeInspection.LocalQuickFixOnPsiElement 5 | import com.intellij.json.psi.JsonStringLiteral 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.{PsiElement, PsiFile} 8 | import org.psliwa.idea.composerJson.ComposerBundle 9 | import org.psliwa.idea.composerJson.intellij.codeAssist.QuickFix 10 | import QuickFix._ 11 | import org.psliwa.idea.composerJson.intellij.codeAssist.QuickFix 12 | 13 | private class ShowValidValuesQuickFix(element: JsonStringLiteral) extends LocalQuickFixOnPsiElement(element) { 14 | override def getText: String = ComposerBundle.message("inspection.quickfix.chooseValidValue") 15 | 16 | override def invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement): Unit = { 17 | for { 18 | _ <- documentFor(project, file) 19 | editor <- editorFor(project) 20 | } yield { 21 | val range = element.getTextRange 22 | editor.getCaretModel.getPrimaryCaret.setSelection(range.getStartOffset + 1, range.getEndOffset - 1) 23 | AutoPopupController.getInstance(project).scheduleAutoPopup(editor) 24 | } 25 | } 26 | 27 | override def getFamilyName: String = ComposerBundle.message("inspection.group") 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptAliasReference.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import com.intellij.codeInsight.lookup.LookupElement 4 | import com.intellij.json.psi.{JsonElement, JsonObject, JsonProperty, JsonStringLiteral} 5 | import com.intellij.psi.{PsiElementResolveResult, PsiPolyVariantReferenceBase, ResolveResult} 6 | import org.psliwa.idea.composerJson.intellij.PsiElements 7 | import org.psliwa.idea.composerJson.intellij.codeAssist.scripts.ScriptAliasReference.ScriptAliasLookupElement 8 | import org.psliwa.idea.composerJson.util.ImplicitConversions._ 9 | 10 | import scala.jdk.CollectionConverters._ 11 | 12 | class ScriptAliasReference(findScriptsHolder: JsonObject => Option[JsonObject], element: JsonStringLiteral) 13 | extends PsiPolyVariantReferenceBase[JsonStringLiteral](element) { 14 | override def multiResolve(b: Boolean): Array[ResolveResult] = { 15 | val currentElementScript = element.getText.stripQuotes.stripPrefix("@") 16 | getScriptProperties() 17 | .map(_.getNameElement) 18 | .filter(_.getText.stripQuotes == currentElementScript) 19 | .map(new PsiElementResolveResult(_)) 20 | .toArray 21 | } 22 | 23 | override def getVariants: Array[AnyRef] = { 24 | val currentScriptName = PsiElements.findParentProperty(element).map(_.getName) 25 | 26 | getScriptProperties() 27 | .filterNot(property => currentScriptName.contains(property.getName)) 28 | .flatMap(property => Option(property.getNameElement)) 29 | .map(new ScriptAliasLookupElement(_)) 30 | .toArray ++ Array("@composer", "@php") 31 | } 32 | 33 | private def getScriptProperties(): List[JsonProperty] = { 34 | for { 35 | root <- element.getContainingFile.getChildren.flatMap(PsiElements.ensureJsonObject).headOption.toList 36 | scriptsHolder <- findScriptsHolder(root).toList 37 | property <- scriptsHolder.getPropertyList.asScala.toList 38 | } yield property 39 | } 40 | } 41 | 42 | private object ScriptAliasReference { 43 | class ScriptAliasLookupElement(element: JsonElement) extends LookupElement { 44 | override def getLookupString: String = "@" + element.getText.stripQuotes 45 | override def getObject: AnyRef = element 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptsPsiElementPattern.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import com.intellij.json.psi.{JsonArray, JsonProperty, JsonStringLiteral} 4 | import com.intellij.patterns.PlatformPatterns.psiElement 5 | import com.intellij.patterns.StandardPatterns.or 6 | import org.psliwa.idea.composerJson.intellij.PsiElements.rootPsiElementPattern 7 | 8 | object ScriptsPsiElementPattern { 9 | private val RootElement = psiElement(classOf[JsonProperty]) 10 | .withName("scripts") 11 | .withSuperParent(2, rootPsiElementPattern) 12 | 13 | val Pattern = or( 14 | psiElement(classOf[JsonStringLiteral]) 15 | .withParent(classOf[JsonArray]) 16 | .withSuperParent(4, RootElement), 17 | psiElement() 18 | .afterLeaf(":") 19 | .withSuperParent(3, RootElement) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptsReference.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import java.util.Collections 4 | 5 | import com.intellij.codeInsight.completion.FilePathCompletionContributor.FilePathLookupItem 6 | import com.intellij.json.psi.JsonStringLiteral 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.psi.{PsiDirectory, PsiElementResolveResult, PsiPolyVariantReferenceBase, ResolveResult} 9 | import org.psliwa.idea.composerJson.intellij.codeAssist.References 10 | 11 | private class ScriptsReference(element: JsonStringLiteral) 12 | extends PsiPolyVariantReferenceBase[JsonStringLiteral](element) { 13 | private val referenceName: String = 14 | References.getFixedReferenceName(element.getText).split(' ').headOption.getOrElse("") 15 | 16 | override def multiResolve(incompleteCode: Boolean): Array[ResolveResult] = { 17 | val maybeCommandFile = for { 18 | binDir <- maybeBinDir 19 | commandFile <- Option(binDir.findFile(referenceName)) 20 | } yield commandFile 21 | 22 | maybeCommandFile.map(new PsiElementResolveResult(_)).toArray 23 | } 24 | 25 | private def maybeBinDir: Option[PsiDirectory] = { 26 | for { 27 | rootDir <- Option(element.getContainingFile.getOriginalFile.getContainingDirectory) 28 | vendorDir <- Option(rootDir.findSubdirectory("vendor")) 29 | binDir <- Option(vendorDir.findSubdirectory("bin")) 30 | } yield binDir 31 | } 32 | 33 | override def getRangeInElement: TextRange = { 34 | val textRange = super.getRangeInElement 35 | new TextRange(textRange.getStartOffset, textRange.getStartOffset + referenceName.length) 36 | } 37 | 38 | override def getVariants: Array[AnyRef] = { 39 | maybeBinDir match { 40 | case Some(binDir) => 41 | binDir.getFiles.map(new FilePathLookupItem(_, Collections.emptyList())) 42 | case None => 43 | Array() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptsReferenceContributor.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import com.intellij.json.psi.JsonObject 4 | import com.intellij.psi._ 5 | import com.intellij.util.ProcessingContext 6 | import org.psliwa.idea.composerJson.intellij.PsiElements._ 7 | 8 | class ScriptsReferenceContributor extends PsiReferenceContributor { 9 | override def registerReferenceProviders(registrar: PsiReferenceRegistrar): Unit = { 10 | registrar.registerReferenceProvider( 11 | ScriptsPsiElementPattern.Pattern, 12 | ScriptsReferenceProvider 13 | ) 14 | } 15 | } 16 | 17 | private object ScriptsReferenceProvider extends PsiReferenceProvider { 18 | override def getReferencesByElement(element: PsiElement, context: ProcessingContext): Array[PsiReference] = { 19 | def findScriptsHolder(root: JsonObject): Option[JsonObject] = { 20 | Option(root.findProperty("scripts")).flatMap(a => Option(a.getValue)).flatMap(ensureJsonObject) 21 | } 22 | 23 | val maybeReferences = for { 24 | stringElement <- ensureJsonStringLiteral(element) 25 | } yield { 26 | Array[PsiReference]( 27 | new ScriptsReference(stringElement), 28 | new ScriptAliasReference(findScriptsHolder, stringElement) 29 | ) 30 | } 31 | 32 | maybeReferences.getOrElse(Array()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/filetype/ComposerJsonFileType.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.filetype 2 | 3 | import javax.swing.Icon 4 | 5 | import com.intellij.json.JsonLanguage 6 | import com.intellij.openapi.fileTypes.LanguageFileType 7 | import org.psliwa.idea.composerJson.Icons 8 | 9 | class ComposerJsonFileType extends LanguageFileType(JsonLanguage.INSTANCE) { 10 | override def getName: String = "composer.json" 11 | 12 | override def getDescription: String = "Composer configuration file" 13 | 14 | override def getIcon: Icon = Icons.Composer 15 | 16 | override def getDefaultExtension: String = "json" 17 | } 18 | 19 | object ComposerJsonFileType { 20 | val INSTANCE = new ComposerJsonFileType 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/intellij/filetype/ComposerJsonFileTypeFactory.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.filetype 2 | 3 | import com.intellij.openapi.fileTypes.{ExactFileNameMatcher, FileTypeConsumer, FileTypeFactory} 4 | import org.psliwa.idea.composerJson.ComposerJson 5 | import org.psliwa.idea.composerJson.ComposerLock 6 | 7 | class ComposerJsonFileTypeFactory extends FileTypeFactory { 8 | override def createFileTypes(consumer: FileTypeConsumer): Unit = { 9 | consumer.consume(ComposerJsonFileType.INSTANCE, new ExactFileNameMatcher(ComposerJson)) 10 | consumer.consume(ComposerJsonFileType.INSTANCE, new ExactFileNameMatcher(ComposerLock)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/json/Format.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.json 2 | 3 | import java.net.{MalformedURLException, URL} 4 | 5 | import scala.util.matching.Regex 6 | 7 | trait Format { 8 | def isValid(s: String): Boolean 9 | } 10 | 11 | class PatternFormat(private val pattern: Regex) extends Format { 12 | override def isValid(s: String): Boolean = pattern.findFirstMatchIn(s).isDefined 13 | } 14 | 15 | object PatternFormat { 16 | def unapply(format: PatternFormat): Option[Regex] = Some(format.pattern) 17 | } 18 | 19 | object EmailFormat extends PatternFormat("^(?i)[\\p{L}0-9._%+-]+@[\\p{L}0-9.-]+\\.[\\p{L}0-9]{2,}$".r) 20 | 21 | object UriFormat extends Format { 22 | override def isValid(s: String): Boolean = { 23 | try { 24 | new URL(s) 25 | true 26 | } catch { 27 | case _: MalformedURLException => false 28 | } 29 | } 30 | } 31 | 32 | object AnyFormat extends Format { 33 | override def isValid(s: String) = true 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/json/SchemaLoader.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.json 2 | 3 | import scala.io.Source 4 | import scala.util.Try 5 | 6 | object SchemaLoader { 7 | def load(path: String): Option[Schema] = { 8 | Option(this.getClass.getResource(path)) 9 | .map(Source.fromURL) 10 | .flatMap(consumeSource) 11 | .flatMap(Schema.parse) 12 | } 13 | 14 | private def consumeSource(s: Source): Option[String] = { 15 | Try { 16 | try { 17 | s.mkString 18 | } finally { 19 | s.close() 20 | } 21 | }.toOption 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/package.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea 2 | 3 | import org.psliwa.idea.composerJson.json.{Schema, SchemaLoader} 4 | 5 | package object composerJson { 6 | val ComposerJson = "composer.json" 7 | val ComposerLock = "composer.lock" 8 | val ComposerSchemaFilepath = "/org/psliwa/idea/composerJson/composer-schema.json" 9 | val EmptyPsiElementNamePlaceholder = "IntellijIdeaRulezzz" 10 | lazy val ComposerSchema: Option[Schema] = SchemaLoader.load(ComposerSchemaFilepath) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/settings/AppSettings.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings 2 | 3 | import java.time.{LocalDate, Month} 4 | 5 | import com.intellij.openapi.components.{PersistentStateComponent, ServiceManager, State, Storage} 6 | import org.jdom.Element 7 | 8 | @State(name = "ComposerJsonPluginAppSettings", storages = Array(new Storage("$APP_CONFIG$/composerJson.xml"))) 9 | class AppSettings extends PersistentStateComponent[Element] { 10 | private var charityNotificationShown: Boolean = false 11 | private var charitySummaryNotificationShown: Boolean = false 12 | 13 | override def loadState(name: Element): Unit = { 14 | charityNotificationShown = loadBoolean(name, "charityNotificationShown") 15 | charitySummaryNotificationShown = loadBoolean(name, "charitySummaryNotificationShown") 16 | } 17 | 18 | private def loadBoolean(element: Element, name: String) = { 19 | Option(element.getChild(name)) 20 | .exists( 21 | child => 22 | child.getValue match { 23 | case "true" => true 24 | case _ => false 25 | } 26 | ) 27 | } 28 | 29 | override def getState: Element = { 30 | val element = new Element("ComposerJsonPluginAppSettings") 31 | element.addContent(new Element("charityNotificationShown").addContent(charityNotificationShown.toString)) 32 | element.addContent( 33 | new Element("charitySummaryNotificationShown").addContent(charitySummaryNotificationShown.toString) 34 | ) 35 | } 36 | 37 | def wasCharityNotificationShown: Boolean = charityNotificationShown 38 | def isCharityNotificationStillValid: Boolean = 39 | LocalDate.now().isBefore(LocalDate.of(2018, Month.JANUARY.getValue, 17)) 40 | 41 | def wasCharitySummaryNotificationShown: Boolean = charitySummaryNotificationShown 42 | def charitySummaryNotificationWasShown(): Unit = charitySummaryNotificationShown = true 43 | } 44 | 45 | object AppSettings { 46 | def getInstance: AppSettings = ServiceManager.getService(classOf[AppSettings]) 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/settings/TabularSettings.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings 2 | 3 | trait TabularSettings[A] { 4 | def getValues(): java.util.List[A] 5 | def setValues(values: java.util.List[A]) 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/CharOffsetFinder.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import scala.language.implicitConversions 4 | 5 | object CharOffsetFinder extends OffsetFinder[CharSequence, Char] { 6 | 7 | type CharMatcher = Matcher[Char] 8 | 9 | protected def objectAt(haystack: CharSequence, offset: Int): Char = { 10 | haystack.subSequence(offset, offset + 1).charAt(0) 11 | } 12 | 13 | protected def stop(haystack: CharSequence)(offset: Int): Boolean = offset >= haystack.length() 14 | protected def reverseStop(haystack: CharSequence)(offset: Int): Boolean = offset < 0 15 | 16 | val Whitespace: Matcher[Char] = Matcher(_.isWhitespace) 17 | val Alphnum: Matcher[Char] = Matcher(_.isLetterOrDigit) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/Files.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import com.intellij.psi.{PsiDirectory, PsiFile, PsiFileSystemItem} 4 | 5 | import scala.annotation.tailrec 6 | 7 | object Files { 8 | def findDir(rootDir: PsiDirectory, path: String): Option[PsiDirectory] = findPath(rootDir, path) match { 9 | case Some(x: PsiDirectory) => Some(x) 10 | case _ => None 11 | } 12 | 13 | def findPath(rootDir: PsiDirectory, path: String): Option[PsiFileSystemItem] = { 14 | @tailrec 15 | def loop(rootDir: PsiDirectory, paths: List[String]): Option[PsiFileSystemItem] = { 16 | paths match { 17 | case Nil => Some(rootDir) 18 | case ".." :: t => 19 | Option(rootDir.getParent) match { 20 | case Some(parent) => loop(parent, t) 21 | case None => None 22 | } 23 | case "." :: t => loop(rootDir, t) 24 | case h :: t => 25 | val subPath = Option(rootDir.findSubdirectory(h)) 26 | .orElse(Option(rootDir.findFile(h))) 27 | 28 | subPath match { 29 | case Some(x: PsiDirectory) => loop(x, t) 30 | case Some(x: PsiFile) if t.isEmpty => Some(x) 31 | case _ => None 32 | } 33 | } 34 | } 35 | 36 | loop(rootDir, path.split("/").toList.filter(!_.isEmpty)) 37 | } 38 | 39 | def findFile(rootDir: PsiDirectory, path: String): Option[PsiFile] = { 40 | findPath(rootDir, path) match { 41 | case Some(x: PsiFile) => Some(x) 42 | case _ => None 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/Funcs.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import java.util 4 | import java.util.Collections 5 | import java.util.Map.Entry 6 | 7 | object Funcs { 8 | def memorize[A, B](maxSize: Int)(f: A => B): A => B = { 9 | val cache = Collections.synchronizedMap(new util.LinkedHashMap[A, B]() { 10 | override def removeEldestEntry(eldest: Entry[A, B]): Boolean = this.size() > maxSize 11 | }) 12 | 13 | a: A => { 14 | val cachedValue = cache.get(a) 15 | if (cachedValue == null) { 16 | val newValue = f(a) 17 | cache.put(a, newValue) 18 | newValue 19 | } else { 20 | cachedValue 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/IO.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import java.net.{HttpURLConnection, URL} 4 | 5 | import scala.io.{Codec, Source} 6 | import scala.util.Try 7 | 8 | object IO { 9 | def loadUrl(uri: String): Try[String] = { 10 | Try { 11 | val connection = new URL(uri).openConnection() match { 12 | case c: HttpURLConnection => 13 | c.setConnectTimeout(5000) 14 | c.setReadTimeout(15000) // packages list might be very heavy, take it enough time to complete 15 | c.setRequestProperty("User-Agent", "idea-composer-plugin") 16 | c 17 | case c => c 18 | } 19 | 20 | val in = Source.fromInputStream(connection.getInputStream, Codec.UTF8.charSet.name()) 21 | try { 22 | in.getLines().mkString 23 | } finally { 24 | in.close() 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/ImplicitConversions.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import scala.language.implicitConversions 4 | 5 | object ImplicitConversions { 6 | implicit def wrapString(s: String): StringOps = new StringOps(s) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/OffsetFinder.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import scala.annotation.tailrec 4 | import scala.language.implicitConversions 5 | 6 | case class Matcher[A](is: A => Boolean) { 7 | def &&(matcher: Matcher[A]): Matcher[A] = Matcher[A](t => is(t) && matcher.is(t)) 8 | def ||(matcher: Matcher[A]): Matcher[A] = Matcher[A](t => is(t) || matcher.is(t)) 9 | } 10 | 11 | trait OffsetFinder[Haystack, A] { 12 | 13 | protected def stop(haystack: Haystack)(offset: Int): Boolean 14 | protected def reverseStop(haystack: Haystack)(offset: Int): Boolean 15 | protected def objectAt(haystack: Haystack, offset: Int): A 16 | 17 | def not(matcher: Matcher[A]): Matcher[A] = Matcher[A](!matcher.is(_)) 18 | 19 | def findOffset(matchers: Matcher[A]*)(offset: Int)(implicit haystack: Haystack): Option[Int] = { 20 | findOffset(Matcher[A](c => matchers.exists(_ is c)))(offset) 21 | } 22 | 23 | def findOffset(expectedMatcher: Matcher[A])(offset: Int)(implicit haystack: Haystack): Option[Int] = { 24 | findOffset(stop(haystack) _, 1)(expectedMatcher)(offset) 25 | } 26 | 27 | private def findOffset(stop: Int => Boolean, delta: Int)( 28 | expectedMatcher: Matcher[A] 29 | )(offset: Int)(implicit haystack: Haystack): Option[Int] = { 30 | @tailrec 31 | def loop(offset: Int): Option[Int] = { 32 | if (stop(offset)) { 33 | None 34 | } else { 35 | val obj = objectAt(haystack, offset) 36 | 37 | if (expectedMatcher is obj) Some(offset) 38 | else loop(offset + delta) 39 | } 40 | } 41 | 42 | loop(offset) 43 | } 44 | 45 | def findOffsetReverse(expectedMatcher: Matcher[A])(offset: Int)(implicit haystack: Haystack): Option[Int] = { 46 | findOffset(reverseStop(haystack) _, -1)(expectedMatcher)(offset) 47 | } 48 | 49 | def ensure(s: Matcher[A]*)(offset: Int)(implicit haystack: Haystack): Option[A] = { 50 | val obj = objectAt(haystack, offset) 51 | 52 | if (s.exists(_ is obj)) Some(obj) 53 | else None 54 | } 55 | } 56 | 57 | object OffsetFinder { 58 | object ImplicitConversions { 59 | implicit def objectToMatcher[A](o: A): Matcher[A] = Matcher(_ == o) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/StringOps.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | class StringOps(s: String) { 4 | def stripQuotes: String = if (s.headOption.contains('"')) stripMargins("\"") else stripMargins("'") 5 | 6 | private def stripMargins(margin: String) = s.stripPrefix(margin).stripSuffix(margin) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/TryMonoid.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | import scala.util.{Failure, Try} 4 | import scalaz._ 5 | 6 | class TryMonoid[A](ex: Throwable) extends Monoid[Try[A]] { 7 | override def zero: Try[A] = Failure(ex) 8 | override def append(f1: Try[A], f2: => Try[A]): Try[A] = f1.recoverWith { case _ => f2 } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/Implicits.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | import scala.language.implicitConversions 4 | import scala.util.matching.Regex 5 | import scalaz.Monad 6 | 7 | object Implicits { 8 | implicit def string(s: String): Parser[String] = Parsers.string(s) 9 | implicit def regex(r: Regex): Parser[String] = Parsers.regex(r) 10 | implicit def asStringParser[A](a: A)(implicit f: A => Parser[String]): ParserOps[String] = new ParserOps(f(a)) 11 | implicit def ToParserOps[A](p: Parser[A]): ParserOps[A] = new ParserOps(p) 12 | implicit val monad: Monad[Parser] = ParserMonad 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/JSON.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | import spray.json.{JsValue, JsonParser} 4 | 5 | import scala.util.Try 6 | 7 | object JSON { 8 | def parse(data: String): Option[JsValue] = Try { JsonParser(data) }.toOption 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/Location.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | case class Location(wholeInput: String, offset: Int = 0) { 4 | lazy val input: String = wholeInput.substring(offset) 5 | 6 | def advancedBy(n: Int): Location = copy(offset = offset + n) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/ParserMonad.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | import scalaz.Monad 4 | 5 | object ParserMonad extends Monad[Parser] { 6 | 7 | override def bind[A, B](p: Parser[A])(f: A => Parser[B]): Parser[B] = loc => { 8 | p(loc) match { 9 | case Success(a, n) => f(a)(loc.advancedBy(n)).advanceSuccess(n) 10 | case Failure => Failure 11 | } 12 | } 13 | 14 | override def point[A](a: => A): Parser[A] = _ => Success(a, 0) 15 | 16 | def fail[A](): Parser[A] = _ => Failure 17 | 18 | def succeed[A](a: A): Parser[A] = point(a) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/ParserOps.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | import org.psliwa.idea.composerJson.util.parsers.Implicits._ 4 | 5 | import scala.language.implicitConversions 6 | import scalaz.Monad 7 | 8 | class ParserOps[A](p: Parser[A]) { 9 | 10 | def run(input: String): Option[A] = p(Location(input)) match { 11 | case Success(a, _) => Some(a) 12 | case Failure => None 13 | } 14 | 15 | def or[B >: A](p2: Parser[B]): Parser[B] = input => { 16 | p(input) match { 17 | case Failure => p2(input) 18 | case x => x 19 | } 20 | } 21 | 22 | def |[B >: A](p2: Parser[B]): Parser[B] = or(p2) 23 | 24 | def many: Parser[List[A]] = Monad[Parser].apply2(p, p.many)(_ :: _) or Monad[Parser].point(List[A]()) 25 | 26 | def map[B](f: A => B): Parser[B] = Monad[Parser].map(p)(f) 27 | 28 | def flatMap[B](f: A => Parser[B]): Parser[B] = Monad[Parser].bind(p)(f) 29 | 30 | } 31 | 32 | trait ToParserOps { 33 | implicit def ToParserOps[A](p: Parser[A]): ParserOps[A] = new ParserOps(p) 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/Parsers.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | import scala.language.implicitConversions 4 | import scala.util.matching.Regex 5 | import Implicits._ 6 | 7 | object Parsers { self => 8 | 9 | def string(s: String): Parser[String] = loc => { 10 | if (loc.input.startsWith(s)) Success(s, s.length) 11 | else Failure 12 | } 13 | 14 | def whole(): Parser[String] = loc => Success(loc.input, loc.input.length) 15 | 16 | def regex(r: Regex): Parser[String] = loc => { 17 | r.findFirstMatchIn(loc.input) 18 | .map(m => Success(m.toString(), m.end)) 19 | .getOrElse(Failure) 20 | } 21 | 22 | def char(c: Char): Parser[Char] = string(c.toString).map(_.charAt(0)) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/Result.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util.parsers 2 | 3 | sealed trait Result[+A] { 4 | def advanceSuccess(n: Int): Result[A] = this match { 5 | case Success(a, c) => Success(a, c + n) 6 | case _ => this 7 | } 8 | } 9 | case object Failure extends Result[Nothing] 10 | case class Success[+A](get: A, charsConsumed: Int) extends Result[A] 11 | -------------------------------------------------------------------------------- /src/main/scala/org/psliwa/idea/composerJson/util/parsers/package.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.util 2 | 3 | /** 4 | * Generic parser library based on https://github.com/fpinscala/fpinscala/blob/master/answers/src/main/scala/fpinscala/parsing/Parsers.scala 5 | */ 6 | package object parsers { 7 | type Parser[A] = Location => Result[A] 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine.php -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/orm", 3 | "type": "library", 4 | "description": "Object-Relational-Mapper for PHP", 5 | "keywords": [ 6 | "orm", 7 | "database" 8 | ], 9 | "homepage": "http://www.doctrine-project.org", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Guilherme Blanco", 14 | "email": "guilhermeblanco@gmail.com" 15 | }, 16 | { 17 | "name": "Roman Borschel", 18 | "email": "roman@code-factory.org" 19 | }, 20 | { 21 | "name": "Benjamin Eberlei", 22 | "email": "kontakt@beberlei.de" 23 | }, 24 | { 25 | "name": "Jonathan Wage", 26 | "email": "jonwage@gmail.com" 27 | } 28 | ], 29 | "minimum-stability": "dev", 30 | "require": { 31 | "php": ">=5.3.2", 32 | "ext-pdo": "*", 33 | "doctrine/collections": "~1.2", 34 | "doctrine/dbal": ">=2.5-dev,<2.6-dev", 35 | "doctrine/instantiator": "~1.0.1", 36 | "symfony/console": "~2.5" 37 | }, 38 | "require-dev": { 39 | "symfony/yaml": "~2.1", 40 | "phpunit/phpunit": "~4.0", 41 | "satooshi/php-coveralls": "dev-master" 42 | }, 43 | "suggest": { 44 | "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" 45 | }, 46 | "autoload": { 47 | "psr-0": { 48 | "Doctrine\\ORM\\": "lib/" 49 | } 50 | }, 51 | "bin": [ 52 | "bin/doctrine", 53 | "bin/doctrine.php" 54 | ], 55 | "extra": { 56 | "branch-alias": { 57 | "dev-master": "2.5.x-dev" 58 | } 59 | }, 60 | "archive": { 61 | "exclude": [ 62 | "!vendor", 63 | "tests", 64 | "*phpunit.xml", 65 | ".travis.yml", 66 | "build.xml", 67 | "build.properties", 68 | "composer.phar", 69 | "vendor/satooshi", 70 | "lib/vendor", 71 | "*.swp", 72 | "*coveralls.yml" 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/lib/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/commands/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/controllers/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/migrations/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/seeds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/seeds/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/models/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/tests/TestCase.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/tests/TestCase.php -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "description": "The Laravel Framework.", 4 | "keywords": ["framework", "laravel"], 5 | "license": "MIT", 6 | "type": "project", 7 | "require": { 8 | "laravel/framework": "4.2.*" 9 | }, 10 | "autoload": { 11 | "classmap": [ 12 | "app/commands", 13 | "app/controllers", 14 | "app/models", 15 | "app/database/migrations", 16 | "app/database/seeds", 17 | "app/tests/TestCase.php" 18 | ] 19 | }, 20 | "scripts": { 21 | "post-install-cmd": [ 22 | "php artisan clear-compiled", 23 | "php artisan optimize" 24 | ], 25 | "post-update-cmd": [ 26 | "php artisan clear-compiled", 27 | "php artisan optimize" 28 | ], 29 | "post-create-project-cmd": [ 30 | "php artisan key:generate" 31 | ] 32 | }, 33 | "config": { 34 | "preferred-install": "dist" 35 | }, 36 | "minimum-stability": "stable" 37 | } -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/HttpFoundation/Resources/stubs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/HttpFoundation/Resources/stubs/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/Intl/Resources/stubs/functions.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/Intl/Resources/stubs/functions.php -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/app/config/parameters.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/d39ab2b4a9d2eb6661ccd645bcebaa14e77d31bc/src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/app/config/parameters.yml -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/framework-standard-edition", 3 | "license": "MIT", 4 | "type": "project", 5 | "description": "The \"Symfony Standard Edition\" distribution", 6 | "autoload": { 7 | "psr-0": { 8 | "": "src/", 9 | "SymfonyStandard": "app/" 10 | } 11 | }, 12 | "require": { 13 | "php": ">=5.3.3", 14 | "symfony/symfony": "2.7.x-dev", 15 | "doctrine/orm": "~2.2,>=2.2.3", 16 | "doctrine/doctrine-bundle": "~1.2", 17 | "twig/extensions": "~1.0", 18 | "symfony/assetic-bundle": "~2.3", 19 | "symfony/swiftmailer-bundle": "~2.3", 20 | "symfony/monolog-bundle": "~2.4", 21 | "sensio/distribution-bundle": "~3.0.12", 22 | "sensio/framework-extra-bundle": "~3.0", 23 | "incenteev/composer-parameter-handler": "~2.0" 24 | }, 25 | "require-dev": { 26 | "sensio/generator-bundle": "~2.3" 27 | }, 28 | "scripts": { 29 | "post-root-package-install": [ 30 | "SymfonyStandard\\Composer::hookRootPackageInstall" 31 | ], 32 | "post-install-cmd": [ 33 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 34 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 35 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 36 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 37 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 38 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles" 39 | ], 40 | "post-update-cmd": [ 41 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 42 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 43 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 44 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 45 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 46 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles" 47 | ] 48 | }, 49 | "config": { 50 | "bin-dir": "bin" 51 | }, 52 | "extra": { 53 | "symfony-app-dir": "app", 54 | "symfony-web-dir": "web", 55 | "symfony-assets-install": "relative", 56 | "incenteev-parameters": { 57 | "file": "app/config/parameters.yml" 58 | }, 59 | "branch-alias": { 60 | "dev-master": "2.7-dev" 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/src/classes.php: -------------------------------------------------------------------------------- 1 | Prop, params: PropertyCheckConfigParam*): Unit = { 9 | super.property(testName)(check(testFun, params: _*)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/repository/ComposedRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.psliwa.idea.composerJson.BasePropSpec 4 | import org.psliwa.idea.composerJson.composer.model.PackageName 5 | import org.scalacheck.Prop 6 | import org.scalacheck.Prop.{forAll, AnyOperators} 7 | 8 | class ComposedRepositoryTest extends BasePropSpec { 9 | import RepositoryGenerators._ 10 | 11 | property("contains packages from all repositories") { 12 | forAll(repositoryWithPackageNamesGen) { 13 | case (repository: Repository[String], packages: Seq[PackageName]) => 14 | repository.getPackages ?= packages.map(_.presentation) 15 | } 16 | } 17 | 18 | property("contains versions from all repositories") { 19 | forAll(repositoryWithVersionsGen) { 20 | case (repository: Repository[String], pkgsVersions: Map[PackageName, Seq[String]]) => 21 | Prop.all(pkgsVersions.map { 22 | case (pkg, versions) => (repository.getPackageVersions(pkg).toList ?= versions.toList) :| s"$versions" 23 | }.toSeq: _*) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/repository/RepositoryGenerators.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.scalacheck.Gen 4 | import scalaz._ 5 | import Scalaz._ 6 | import org.psliwa.idea.composerJson.composer.model.PackageName 7 | 8 | object RepositoryGenerators { 9 | 10 | implicit private val seqStringSemigrup: Semigroup[Seq[String]] = seqSemigroup[String] 11 | 12 | private def pkgGen: Gen[PackageName] = Gen.listOfN(5, Gen.alphaLowerChar).map(_.mkString("")).map(PackageName) 13 | private def versionGen: Gen[String] = 14 | for { 15 | size <- Gen.choose(1, 2) 16 | chars <- Gen.listOfN(size, Gen.alphaChar) 17 | } yield chars.mkString("") 18 | private def versionsGen: Gen[List[String]] = 19 | for { 20 | size <- Gen.choose(0, 50) 21 | versions <- Gen.listOfN(size, versionGen) 22 | } yield versions 23 | def repositoryWithPackageNamesGen: Gen[(Repository[String], Seq[PackageName])] = 24 | for { 25 | n <- Gen.choose(0, 20) 26 | packagesNames <- Gen.listOfN(n, Gen.listOf(pkgGen)) 27 | repos = packagesNames.map(packageNames => InMemoryRepository(packageNames.map(_.presentation))) 28 | } yield (new ComposedRepository(repos), packagesNames.flatten) 29 | 30 | private def pkgsVersionsGen(packageNames: Seq[PackageName]): Gen[Map[PackageName, Seq[String]]] = 31 | for { 32 | packagesAndVersions <- Gen.sequence[Seq[(PackageName, Seq[String])], (PackageName, Seq[String])]( 33 | packageNames.map(packageName => versionsGen.map(packageName -> _)) 34 | ) 35 | } yield packagesAndVersions.toMap 36 | 37 | def repositoryWithVersionsGen: Gen[(Repository[String], Map[PackageName, Seq[String]])] = 38 | for { 39 | pkgsCount <- Gen.choose(2, 4) 40 | packageNames <- Gen.listOfN(pkgsCount, pkgGen) 41 | reposCount <- Gen.choose(1, 4) 42 | versions <- Gen.listOfN(reposCount, pkgsVersionsGen(packageNames)) 43 | repos = versions.map(InMemoryRepository(packageNames.map(_.presentation), _)) 44 | } yield (new ComposedRepository(repos), versions.reduce[Map[PackageName, Seq[String]]](_ |+| _)) 45 | 46 | private def seqSemigroup[A]: Semigroup[Seq[A]] = new Semigroup[Seq[A]] { 47 | override def append(f1: Seq[A], f2: => Seq[A]): Seq[A] = f1 ++ f2 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/repository/RepositoryProviderWrapperTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.repository 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | 6 | class RepositoryProviderWrapperTest { 7 | val innerRepository: Repository[String] = Repository.inMemory[String](List("package1")) 8 | val innerRepositoryProvider: RepositoryProvider[String] = new RepositoryProvider[String] { 9 | override def repositoryFor(file: String): Repository[String] = innerRepository 10 | override def updateRepository(file: String, info: RepositoryInfo) = false 11 | override def hasDefaultRepository(file: String): Boolean = false 12 | } 13 | 14 | val defaultRepository: Repository[String] = Repository.inMemory[String](List("package2")) 15 | 16 | @Test 17 | def defaultRepositoryShouldBeReturnedWhenPredicateIsTrue(): Unit = { 18 | //given 19 | 20 | val defaultFile = "defaultFile" 21 | val predicate = (file: String) => file == defaultFile 22 | 23 | val repositoryProvider = 24 | new RepositoryProviderWrapper[String](innerRepositoryProvider, defaultRepository, predicate) 25 | 26 | //when & then 27 | 28 | assertEquals(defaultRepository, repositoryProvider.repositoryFor(defaultFile)) 29 | assertEquals(innerRepository, repositoryProvider.repositoryFor("different-file")) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/version/ConstraintTest/ParsePresentationTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.version.ConstraintTest 2 | 3 | import org.psliwa.idea.composerJson.BasePropSpec 4 | import org.psliwa.idea.composerJson.composer.model.version.{VersionGenerators => gen, _} 5 | import org.scalacheck.Prop.{forAll, propBoolean} 6 | 7 | class ParsePresentationTest extends BasePropSpec { 8 | 9 | property("presentation should be able to be parsed")( 10 | { 11 | forAll(gen.version) { version: Constraint => 12 | val parsed = Parser.parse(version.presentation) 13 | 14 | parsed.contains(version) :| s"$parsed contains $version, presentation ${version.presentation}" 15 | } 16 | }, 17 | MinSuccessful(500) 18 | ) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/version/ConstraintTest/PresentationStringTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.version.ConstraintTest 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | import org.psliwa.idea.composerJson.composer.model.version._ 6 | 7 | class PresentationStringTest { 8 | 9 | val semVer120 = SemanticConstraint(new SemanticVersion(1, 2, 0)) 10 | val semVer121 = SemanticConstraint(new SemanticVersion(1, 2, 1)) 11 | 12 | @Test 13 | def testPresentationString(): Unit = { 14 | List( 15 | (semVer120, "1.2.0"), 16 | (DevConstraint("master"), "dev-master"), 17 | (WildcardConstraint(Some(semVer120)), "1.2.0.*"), 18 | (WildcardConstraint(None), "*"), 19 | (WrappedConstraint(semVer120, Some("prefix-"), Some("-suffix")), "prefix-1.2.0-suffix"), 20 | (OperatorConstraint(ConstraintOperator.<=, semVer120), "<=1.2.0"), 21 | (OperatorConstraint(ConstraintOperator.<=, semVer120, " "), "<= 1.2.0"), 22 | (HashConstraint("afafaf"), "afafaf"), 23 | (DateConstraint("20101010"), "20101010"), 24 | (HyphenRangeConstraint(semVer120, semVer120, " - "), "1.2.0 - 1.2.0"), 25 | (HyphenRangeConstraint(semVer120, semVer120, "-"), "1.2.0-1.2.0"), 26 | (AliasedConstraint(semVer120, semVer121, " as "), "1.2.0 as 1.2.1"), 27 | (AliasedConstraint(semVer120, semVer121, " AS "), "1.2.0 AS 1.2.1"), 28 | (LogicalConstraint(List(semVer120, semVer121), LogicalOperator.AND, ", "), "1.2.0, 1.2.1"), 29 | (LogicalConstraint(List(semVer120, semVer121), LogicalOperator.AND, ","), "1.2.0,1.2.1"), 30 | (LogicalConstraint(List(semVer120, semVer121), LogicalOperator.OR, " || "), "1.2.0 || 1.2.1"), 31 | (LogicalConstraint(List(semVer120, semVer121), LogicalOperator.OR, "|"), "1.2.0|1.2.1") 32 | ).foreach { 33 | case (constraint, expected) => assertEquals(expected, constraint.presentation) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/version/VersionComparatorTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.version 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | import org.psliwa.idea.composerJson.composer.model.version.VersionSuggestions.SimplifiedVersionConstraint.{ 6 | NonSemantic, 7 | PrefixedSemantic, 8 | PureSemantic, 9 | Wildcard 10 | } 11 | import org.psliwa.idea.composerJson.composer.model.version.VersionSuggestions._ 12 | 13 | class VersionComparatorTest { 14 | 15 | @Test 16 | def sortVersions(): Unit = { 17 | val sortedVersions = sorted( 18 | List( 19 | PureSemantic(new SemanticVersion(1, 0, 0)), 20 | NonSemantic("dev-master"), 21 | PureSemantic(new SemanticVersion(1, 2)), 22 | PureSemantic(new SemanticVersion(1, 0, 100)), 23 | Wildcard(PureSemantic(new SemanticVersion(1, 0))), 24 | NonSemantic("1.1.x-dev"), 25 | PureSemantic(new SemanticVersion(1, 1)), 26 | Wildcard(PureSemantic(new SemanticVersion(1))), 27 | PureSemantic(new SemanticVersion(1, 1, 0)), 28 | PrefixedSemantic(PureSemantic(new SemanticVersion(1, 1, 0))) 29 | ) 30 | ) 31 | 32 | assertEquals(List("1.1.0", "1.0.*", "1.0.100", "1.0.0", "1.*", "1.2", "1.1", "v1.1.0", "dev-master", "1.1.x-dev"), 33 | sortedVersions) 34 | } 35 | 36 | @Test 37 | def sortSemanticVersionsNumerically(): Unit = { 38 | val versions = sorted(List("3.9.0", "3.33.0").flatMap(VersionSuggestions.parseSemantic)) 39 | 40 | assertEquals(List("3.33.0", "3.9.0"), versions) 41 | } 42 | 43 | private def sorted(versions: List[SimplifiedVersionConstraint]): List[String] = { 44 | versions.sortWith(VersionSuggestions.isGreater).map(_.presentation) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/model/version/VersionEquivalentsTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.version 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | 6 | //those tests depends on version.ParserTest 7 | class VersionEquivalentsTest { 8 | 9 | @Test 10 | def simpleSemanticVersionShouldNotHasEquivalents(): Unit = { 11 | assertEquals(Nil, equivalents("1.2.3")) 12 | } 13 | 14 | @Test 15 | def nsrTildeOperatorShouldHasRangeEquivalent(): Unit = { 16 | assertEquals(List(">=1.2.3 <1.3.0"), equivalents("~1.2.3")) 17 | assertEquals(List(">=1.2 <2.0.0"), equivalents("~1.2")) 18 | assertEquals(List(">=1.0 <2.0.0"), equivalents("~1")) 19 | } 20 | 21 | @Test 22 | def nsrDashOperatorShouldHasRangeEquivalent(): Unit = { 23 | assertEquals(List(">=1.2.3 <2.0.0"), equivalents("^1.2.3")) 24 | assertEquals(List(">=1.2 <2.0.0"), equivalents("^1.2")) 25 | } 26 | 27 | @Test 28 | def givenPreReleaseSemanticVersion_nsrDashOperatorShouldHasRangeEquivalent(): Unit = { 29 | assertEquals(List(">=0.2.3 <0.3.0"), equivalents("^0.2.3")) 30 | assertEquals(List(">=0.2 <0.3.0"), equivalents("^0.2")) 31 | } 32 | 33 | @Test 34 | def nsrRangeShouldHasNsrDashOperatorEquivalent(): Unit = { 35 | assertEquals(List("^1.2.1"), equivalents(">=1.2.1,<2.0.0")) 36 | } 37 | 38 | private def equivalents(s: String): List[String] = { 39 | val constraint = Parser.parse(s).get 40 | VersionEquivalents.equivalentsFor(constraint).map(_.presentation).toList 41 | } 42 | 43 | @Test 44 | def nsrRangeShouldHasNsrOperatorEquivalent(): Unit = { 45 | assertEquals(List("~1.2.3"), equivalents(">=1.2.3,<1.3.0")) 46 | assertEquals(List("~1.2.0"), equivalents(">=1.2,<1.3")) 47 | assertEquals(List("~1.2"), equivalents(">=1.2,<2.0.0")) 48 | assertEquals(List("~1.2"), equivalents(">=1.2.0,<2.0.0")) 49 | } 50 | 51 | @Test 52 | def nonNsrRangeShouldNotHasEquivalents(): Unit = { 53 | List(">=1.2.3,<1.3.3", ">=1.2.3,<1.6.0", ">1.2.3,<1.3.3").foreach(version => { 54 | assertEquals(Nil, equivalents(version)) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/repository/DefaultRepositoryFactoryTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.repository 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | import org.psliwa.idea.composerJson.composer.model.PackageName 6 | import org.psliwa.idea.composerJson.composer.model.repository.{Repository, RepositoryInfo} 7 | import org.psliwa.idea.composerJson.composer.repository.DefaultRepositoryProvider._ 8 | 9 | class DefaultRepositoryFactoryTest { 10 | 11 | val packagistRepository: Repository[String] = Repository.inMemory(List("packagist")) 12 | val factory = new DefaultRepositoryFactory(url => Repository.inMemory(List(url)), packagistRepository, pkg => pkg) 13 | 14 | @Test 15 | def givenFewUrls_createRepositoryFromFewUrls(): Unit = { 16 | 17 | //when 18 | 19 | val repository = factory.repositoryFor(RepositoryInfo(List("url1", "url2"), packagist = false)) 20 | 21 | //then 22 | 23 | assertEquals(List("url1", "url2"), repository.getPackages) 24 | } 25 | 26 | @Test 27 | def givenPackagistRepository_createdRepositoryShouldContainsAlsoPackagistRepo(): Unit = { 28 | 29 | //when 30 | 31 | val repository = factory.repositoryFor(RepositoryInfo(List(), true)) 32 | 33 | //then 34 | 35 | assertEquals(packagistRepository.getPackages, repository.getPackages) 36 | } 37 | 38 | @Test 39 | def givenRepository_createdRepositoryShouldContainsGivenOne(): Unit = { 40 | //given 41 | 42 | val packageName = "vendor/pkg" 43 | val packages = List(packageName) 44 | val versions = Map(packageName -> List("1.0.0")) 45 | 46 | //when 47 | 48 | val repository = 49 | factory.repositoryFor(RepositoryInfo(List(), false, Some(Repository.inMemory[String](packages, versions)))) 50 | 51 | //then 52 | 53 | assertEquals(packages, repository.getPackages) 54 | assertEquals(versions.getOrElse(packageName, List()), repository.getPackageVersions(PackageName(packageName))) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/repository/LoadPackagesTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.repository 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | 6 | class LoadPackagesTest { 7 | 8 | @Test 9 | def loadJsonFromPackagist_shouldBeLoaded(): Unit = { 10 | val result = Packagist.loadPackagesFromPackagist(Packagist.defaultUrl) 11 | 12 | assertFalse(result.isFailure) 13 | assertTrue(result.toOption.map(_.contains("packageNames")).get) 14 | } 15 | 16 | @Test 17 | def loadJsonFromPackagist_givenInvalidIri_expectedError(): Unit = { 18 | val result = Packagist.loadUri(Packagist.defaultUrl)("some/invalid/uri.json") 19 | 20 | assertTrue(result.isFailure) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/composer/repository/LoadVersionsTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.repository 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | import org.psliwa.idea.composerJson.composer.model.PackageName 6 | 7 | class LoadVersionsTest { 8 | 9 | @Test 10 | def loadFromPackagist_givenValidPackage_expectSomeVersions(): Unit = { 11 | val result = Packagist.loadVersions(Packagist.defaultUrl)(PackageName("symfony/symfony")) 12 | 13 | assertTrue(result.isSuccess) 14 | assertTrue(result.get.nonEmpty) 15 | } 16 | 17 | @Test 18 | def loadFromPackagist_givenInvalidPackage_expectError(): Unit = { 19 | val result = 20 | Packagist.loadVersions(Packagist.defaultUrl)(PackageName("some-unexisting-vendor/some-unexisting-package-123")) 21 | 22 | assertTrue(result.isFailure) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/fixtures/ComposerFixtures.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.fixtures 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.util.Computable 5 | import com.intellij.openapi.vfs.{VfsUtil, VirtualFile} 6 | import com.intellij.testFramework.fixtures.CodeInsightTestFixture 7 | import org.psliwa.idea.composerJson 8 | import org.psliwa.idea.composerJson.composer._ 9 | import org.psliwa.idea.composerJson.composer.model.PackageDescriptor 10 | 11 | object ComposerFixtures { 12 | def writeAction[A](f: () => A): A = { 13 | ApplicationManager.getApplication.runWriteAction(new Computable[A] { 14 | override def compute: A = f() 15 | }) 16 | } 17 | 18 | def saveText(file: VirtualFile, text: String): Unit = { 19 | writeAction(() => VfsUtil.saveText(file, text)) 20 | } 21 | 22 | private def makePackagesJson(pkgs: Iterable[ComposerPackageWithReplaces]): String = { 23 | def makeReplacesJson(pkg: ComposerPackageWithReplaces): String = { 24 | if (pkg.replaces.isEmpty) { 25 | "" 26 | } else { 27 | def x(pkg: String): String = s""""$pkg":""""" 28 | s""" 29 | |,"replace": { 30 | |${pkg.replaces.map(x).mkString(",")} 31 | |} 32 | |""".stripMargin 33 | } 34 | } 35 | 36 | pkgs.map(pkg => s"""{ 37 | | "name": "${pkg.pkg.name.presentation}", 38 | | ${pkg.pkg.homepage.map(homepage => s""""homepage":"$homepage",""").getOrElse("")} 39 | | "version": "${pkg.pkg.version}" 40 | | ${makeReplacesJson(pkg)} 41 | |} 42 | """.stripMargin).mkString(",\n") 43 | } 44 | 45 | def createComposerLock(fixture: CodeInsightTestFixture, 46 | packages: List[ComposerPackageWithReplaces], 47 | dir: String = "."): VirtualFile = { 48 | 49 | val (devPackages, prodPackages) = packages.partition(_.pkg.isDev) 50 | 51 | val devPackagesJson = makePackagesJson(devPackages) 52 | val prodPackagesJson = makePackagesJson(prodPackages) 53 | 54 | val file = writeAction( 55 | () => fixture.getTempDirFixture.findOrCreateDir(dir).createChildData(this, composerJson.ComposerLock) 56 | ) 57 | saveText(file, s""" 58 | |{ 59 | | "packages": [ $prodPackagesJson ], 60 | | "packages-dev": [ $devPackagesJson ] 61 | |} 62 | """.stripMargin) 63 | 64 | file 65 | } 66 | 67 | def createComposerJson(fixture: CodeInsightTestFixture, dir: String = "."): VirtualFile = { 68 | val file = writeAction( 69 | () => fixture.getTempDirFixture.findOrCreateDir(dir).createChildData(this, composerJson.ComposerJson) 70 | ) 71 | saveText(file, "{}") 72 | 73 | file 74 | } 75 | 76 | case class ComposerPackageWithReplaces(pkg: PackageDescriptor, replaces: Set[String] = Set.empty) 77 | } 78 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/CompletionTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.codeInsight.lookup.Lookup 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.util.Computable 6 | import com.intellij.testFramework.UsefulTestCase 7 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 8 | import org.junit.Assert._ 9 | import org.psliwa.idea.composerJson.ComposerJson 10 | 11 | abstract class CompletionTest extends BasePlatformTestCase { 12 | 13 | override def isWriteActionRequired: Boolean = false 14 | 15 | protected def suggestions( 16 | contents: String, 17 | expectedSuggestions: Array[String], 18 | unexpectedSuggestions: Array[String] = Array() 19 | ): Unit = 20 | suggestions(UsefulTestCase.assertContainsElements(_, _: _*))(contents, expectedSuggestions, unexpectedSuggestions) 21 | 22 | protected def orderedSuggestions( 23 | contents: String, 24 | expectedSuggestions: Array[String], 25 | unexpectedSuggestions: Array[String] = Array() 26 | ): Unit = 27 | suggestions(UsefulTestCase.assertContainsOrdered(_, _: _*))(contents, expectedSuggestions, unexpectedSuggestions) 28 | 29 | protected def suggestions( 30 | containsElements: (java.util.List[String], Array[String]) => Unit 31 | )( 32 | contents: String, 33 | expectedSuggestions: Array[String], 34 | unexpectedSuggestions: Array[String] 35 | ): Unit = { 36 | myFixture.configureByText(ComposerJson, contents) 37 | myFixture.completeBasic() 38 | 39 | val lookupElements = myFixture.getLookupElementStrings 40 | 41 | assertNotNull(lookupElements) 42 | containsElements(lookupElements, expectedSuggestions) 43 | UsefulTestCase.assertDoesntContain(lookupElements, unexpectedSuggestions: _*) 44 | } 45 | 46 | protected def completion(contents: String, expected: String): Unit = { 47 | myFixture.configureByText(ComposerJson, contents) 48 | val elements = myFixture.completeBasic() 49 | 50 | if (elements != null && elements.length == 1) { 51 | //finish completion if there is only one item 52 | myFixture.finishLookup(Lookup.NORMAL_SELECT_CHAR) 53 | } 54 | 55 | myFixture.checkResult(expected.replace("\r", "")) 56 | } 57 | 58 | def writeAction(f: () => Unit): Unit = { 59 | ApplicationManager.getApplication.runWriteAction(new Computable[Unit] { 60 | override def compute: Unit = f() 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/DocumentationTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.lang.documentation.DocumentationProvider 4 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 5 | import org.junit.Assert._ 6 | import org.psliwa.idea.composerJson._ 7 | 8 | abstract class DocumentationTest extends BasePlatformTestCase { 9 | 10 | override def isWriteActionRequired: Boolean = true 11 | 12 | protected def checkDocumentation(s: String, 13 | externalUrls: List[String], 14 | maybeExpectedDoc: Option[String] = None, 15 | filename: String = ComposerJson): Unit = { 16 | def externalUrlsAssertion(urls: List[String]): Unit = 17 | externalUrls.foreach(url => assertTrue(urls.exists(_.contains(url)))) 18 | def docAssertion(doc: String): Unit = maybeExpectedDoc.foreach(assertEquals(_, doc)) 19 | 20 | checkDocumentation(s, externalUrlsAssertion _, docAssertion _, filename) 21 | } 22 | 23 | protected def checkDocumentation(s: String, 24 | externalUrlsAssertion: List[String] => Unit, 25 | docAssertion: String => Unit, 26 | filename: String): Unit = { 27 | import scala.jdk.CollectionConverters._ 28 | 29 | myFixture.configureByText(filename, s.replace("\r", "")) 30 | 31 | val element = myFixture.getElementAtCaret.getFirstChild.getFirstChild 32 | 33 | val urls = Option(documentationProvider.getUrlFor(element, element)).map(_.asScala).getOrElse(List()) 34 | val doc = documentationProvider.generateDoc(element, element.getOriginalElement) 35 | 36 | externalUrlsAssertion(urls.toList) 37 | docAssertion(doc) 38 | } 39 | 40 | protected def checkDocumentation(s: String, expectedDoc: String): Unit = 41 | checkDocumentation(s, List(), Option(expectedDoc)) 42 | 43 | protected def documentationProvider: DocumentationProvider 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/FilePathReferences.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.psi.{PsiElement, PsiFileSystemItem} 4 | import org.junit.Assert.assertEquals 5 | import org.psliwa.idea.composerJson.ComposerJson 6 | 7 | abstract class FilePathReferences extends CompletionTest { 8 | 9 | def checkFileReference(file: String, s: String): Unit = { 10 | myFixture.getTempDirFixture.createFile(file) 11 | assertEquals(1, getResolvedFileReferences(endsWith(file), s).length) 12 | } 13 | 14 | def checkEmptyFileReferences(file: String, s: String): Unit = { 15 | myFixture.getTempDirFixture.createFile(file) 16 | 17 | assertEquals(0, getResolvedFileReferences(endsWith(file), s).length) 18 | } 19 | 20 | private def endsWith(suffix: String)(s: String): Boolean = s.endsWith(suffix) 21 | 22 | def getResolvedFileReferences(fileComparator: String => Boolean, 23 | s: String, 24 | mapElement: PsiElement => PsiElement = _.getParent): Array[String] = { 25 | myFixture.configureByText(ComposerJson, s) 26 | 27 | val element = mapElement(myFixture.getFile.findElementAt(myFixture.getCaretOffset)) 28 | 29 | element.getReferences 30 | .map(_.resolve()) 31 | .filter(_.isInstanceOf[PsiFileSystemItem]) 32 | .map(_.asInstanceOf[PsiFileSystemItem]) 33 | .map(_.getVirtualFile.getCanonicalPath) 34 | .filter(fileComparator) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/InspectionTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.util.Computable 5 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 6 | import org.junit.Assert._ 7 | import org.psliwa.idea.composerJson._ 8 | 9 | abstract class InspectionTest extends BasePlatformTestCase { 10 | override def isWriteActionRequired: Boolean = false 11 | 12 | def checkInspection(s: String, filePath: String = ComposerJson): Unit = { 13 | filePath.split("/").toList match { 14 | case dir :: file :: Nil => 15 | val composerJson = myFixture.configureByText(file, s.replace("\r", "")) 16 | writeAction(() => composerJson.getVirtualFile.move(this, findOrCreateDir(dir))) 17 | myFixture.testHighlighting(filePath) 18 | case file :: Nil => 19 | myFixture.configureByText(file, s.replace("\r", "")) 20 | myFixture.checkHighlighting() 21 | case _ => fail(s"only file name or file name with one parent dir are supported as filePath, $filePath given") 22 | } 23 | } 24 | 25 | protected def findOrCreateDir(dir: String) = myFixture.getTempDirFixture.findOrCreateDir(dir) 26 | 27 | def checkQuickFix(quickFix: String, expectedQuickFixCount: Int = 1)(actual: String, expected: String): Unit = { 28 | checkQuickFix(quickFix, Range(Some(expectedQuickFixCount), Some(expectedQuickFixCount)))(actual, expected) 29 | } 30 | 31 | def checkQuickFix(quickFix: String, expectedQuickFixCount: Range)(actual: String, expected: String): Unit = { 32 | runQuickFix(quickFix, expectedQuickFixCount)(actual) 33 | 34 | myFixture.checkResult(expected.replace("\r", "")) 35 | } 36 | 37 | def runQuickFix(quickFix: String, expectedQuickFixCount: Int = 1)(actual: String): Unit = { 38 | runQuickFix(quickFix, Range(Some(expectedQuickFixCount), Some(expectedQuickFixCount)))(actual) 39 | } 40 | 41 | def runQuickFix(quickFix: String, expectedQuickFixCount: Range)(actual: String): Unit = { 42 | import scala.jdk.CollectionConverters._ 43 | 44 | myFixture.configureByText(ComposerJson, actual.replace("\r", "")) 45 | 46 | val caretOffset = myFixture.getEditor.getCaretModel.getOffset 47 | 48 | //side effect of getAllQuickFixes - caret is moved to "0" offset 49 | val quickFixes = myFixture 50 | .getAllQuickFixes(ComposerJson) 51 | .asScala 52 | .filter(qf => qf.getFamilyName.contains(quickFix) || qf.getText.contains(quickFix)) 53 | val quickFixesCount = quickFixes.length 54 | 55 | val msg = s"Expected $expectedQuickFixCount '$quickFix' quick fix, $quickFixesCount found" 56 | expectedQuickFixCount.from.foreach(expected => assertTrue(msg, expected <= quickFixesCount)) 57 | expectedQuickFixCount.to.foreach(expected => assertTrue(msg, expected >= quickFixesCount)) 58 | 59 | myFixture.getEditor.getCaretModel.moveToOffset(caretOffset) 60 | quickFixes.take(1).foreach(myFixture.launchAction) 61 | } 62 | 63 | def writeAction(f: () => Unit): Unit = { 64 | ApplicationManager.getApplication.runWriteAction(new Computable[Unit] { 65 | override def compute: Unit = f() 66 | }) 67 | } 68 | 69 | case class Range(from: Option[Int], to: Option[Int]) 70 | } 71 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/ValidComposerJsonFilesInspectionTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist 2 | 3 | import java.io.File 4 | 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.composer.MisconfigurationInspection 6 | import org.psliwa.idea.composerJson.intellij.codeAssist.file.FilePathInspection 7 | import org.psliwa.idea.composerJson.intellij.codeAssist.schema.SchemaInspection 8 | 9 | /** 10 | * Tests for inspections on few real-live composer.json files 11 | */ 12 | class ValidComposerJsonFilesInspectionTest extends InspectionTest { 13 | 14 | override def setUp(): Unit = { 15 | super.setUp() 16 | 17 | myFixture.enableInspections(classOf[SchemaInspection]) 18 | myFixture.enableInspections(classOf[FilePathInspection]) 19 | myFixture.enableInspections(classOf[MisconfigurationInspection]) 20 | } 21 | 22 | def testSymfonyComposerJson(): Unit = { 23 | checkComposerJson("symfony") 24 | } 25 | 26 | def testSymfonyStandardComposerJson(): Unit = { 27 | checkComposerJson("symfony_standard") 28 | } 29 | 30 | def testLaravelComposerJson(): Unit = { 31 | checkComposerJson("laravel") 32 | } 33 | 34 | def testDoctrineComposerJson(): Unit = { 35 | checkComposerJson("doctrine") 36 | } 37 | 38 | override def getTestDataPath: String = { 39 | new File(s"${sys.env("PWD")}/src/test/resources/org/psliwa/idea/composerJson/inspection/").getAbsolutePath 40 | } 41 | 42 | private def checkComposerJson(pkg: String): Unit = { 43 | myFixture.copyDirectoryToProject(pkg, "/") 44 | myFixture.testHighlighting(true, false, true, "composer.json") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/AbstractPackagesTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.codeInsight 4 | import com.intellij.json.JsonLanguage 5 | import org.psliwa.idea.composerJson.composer.model.PackageName 6 | import org.psliwa.idea.composerJson.intellij.codeAssist.{BaseLookupElement, CompletionTest} 7 | 8 | abstract class AbstractPackagesTest extends CompletionTest { 9 | 10 | def getCompletionContributor = { 11 | getCompletionContributors.head 12 | } 13 | 14 | def getCompletionContributors = { 15 | import scala.jdk.CollectionConverters._ 16 | 17 | codeInsight.completion.CompletionContributor 18 | .forLanguage(JsonLanguage.INSTANCE) 19 | .asScala 20 | .filter(_.isInstanceOf[CompletionContributor]) 21 | .map(_.asInstanceOf[CompletionContributor]) 22 | } 23 | 24 | def setCompletionPackageLoader(f: () => Seq[BaseLookupElement]): Unit = { 25 | getCompletionContributors.foreach(_.setPackagesLoader(f)) 26 | } 27 | 28 | def setCompletionVersionsLoader(f: String => Seq[String]): Unit = { 29 | getCompletionContributors.foreach(_.setVersionsLoader(f.compose[PackageName](_.presentation))) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/PackageDocumentationProviderTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import com.intellij.lang.documentation.DocumentationProvider 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import org.psliwa.idea.composerJson.composer.model.PackageDescriptor 6 | import org.psliwa.idea.composerJson.fixtures.ComposerFixtures 7 | import org.psliwa.idea.composerJson.fixtures.ComposerFixtures._ 8 | import org.psliwa.idea.composerJson.intellij.codeAssist.DocumentationTest 9 | 10 | class PackageDocumentationProviderTest extends DocumentationTest { 11 | override protected def documentationProvider: DocumentationProvider = new PackageDocumentationProvider 12 | 13 | def testGivenPackage_thereShouldBeUrlToPackagistAsExternalDocumentation(): Unit = { 14 | checkDocumentation( 15 | """ 16 | |{ 17 | | "require": { 18 | | "vendor/pkg": "1.0.0" 19 | | } 20 | |} 21 | """.stripMargin, 22 | List("packagist.org/packages/vendor/pkg") 23 | ) 24 | } 25 | 26 | def testGivenPackage_homepageExistsInComposerLock_theUrlShouldBeTheSameAsHomepage(): Unit = { 27 | createComposerLock(List(PackageDescriptor("vendor/pkg", "1.0.0", homepage = Some("some/url")))) 28 | 29 | checkDocumentation( 30 | """ 31 | |{ 32 | | "require": { 33 | | "vendor/pkg": "1.0.0" 34 | | } 35 | |} 36 | """.stripMargin, 37 | List("some/url") 38 | ) 39 | } 40 | 41 | private def createComposerLock(packages: List[PackageDescriptor]): VirtualFile = { 42 | ComposerFixtures.createComposerLock(myFixture, packages.map(ComposerPackageWithReplaces(_, Set.empty))) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/PackageVersionInspectionTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer 2 | 3 | import org.psliwa.idea.composerJson.ComposerBundle 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.InspectionTest 5 | import org.psliwa.idea.composerJson.settings.{PatternItem, ProjectSettings} 6 | 7 | class PackageVersionInspectionTest extends InspectionTest { 8 | 9 | val UnboundedVersionConstraintWarning = ComposerBundle.message("inspection.version.unboundVersion") 10 | val WildcardAndComparisonWarning = ComposerBundle.message("inspection.version.wildcardAndComparison") 11 | 12 | override def setUp(): Unit = { 13 | super.setUp() 14 | 15 | ProjectSettings(myFixture.getProject).getUnboundedVersionInspectionSettings.clear() 16 | } 17 | 18 | def testGivenUnboundVersion_thatShouldBeReported(): Unit = { 19 | checkInspection(s""" 20 | |{ 21 | | "require": { 22 | | "vendor/pkg": ">=2.1.0" 23 | | } 24 | |} 25 | """.stripMargin) 26 | } 27 | 28 | def testGivenBoundVersion_thatIsOk(): Unit = { 29 | checkInspection(""" 30 | |{ 31 | | "require": { 32 | | "vendor/pkg": "2.1.0" 33 | | } 34 | |} 35 | """.stripMargin) 36 | } 37 | 38 | def testGivenSemVerBoundedVersion_thatIsOk(): Unit = { 39 | checkInspection(""" 40 | |{ 41 | | "require": { 42 | | "vendor/pkg": "~1.4" 43 | | } 44 | |} 45 | """.stripMargin) 46 | } 47 | 48 | def testGivenUnboundVersion_givenPackageIsExcluded_thatIsOk(): Unit = { 49 | val pkg = "vendor/pkg" 50 | 51 | ProjectSettings(myFixture.getProject).getUnboundedVersionInspectionSettings.addExcludedPattern(new PatternItem(pkg)) 52 | 53 | checkInspection(s""" 54 | |{ 55 | | "require": { 56 | | "$pkg": ">=2.1.0" 57 | | } 58 | |} 59 | """.stripMargin) 60 | } 61 | 62 | def testGivenComparisonWildcardedVersion_thatShouldBeReported(): Unit = { 63 | checkInspection(s""" 64 | |{ 65 | | "require": { 66 | | "vendor/pkg": "<2.1.*" 67 | | } 68 | |} 69 | """.stripMargin) 70 | } 71 | 72 | def testGivenComparisonAndWrappedWildcardComboVersion_thatShouldBeReported(): Unit = { 73 | checkInspection(s""" 74 | |{ 75 | | "require": { 76 | | "vendor/pkg": "<2.1.*@dev" 77 | | } 78 | |} 79 | """.stripMargin) 80 | } 81 | 82 | def testGivenComparisonAnWildcardComboInLogicalConstraint_thatShouldBeReported(): Unit = { 83 | checkInspection(s""" 84 | |{ 85 | | "require": { 86 | | "vendor/pkg": ">=2.1.*, <2.2" 87 | | } 88 | |} 89 | """.stripMargin) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/composer/infoRenderer/PackageInfoInspectionTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.composer.infoRenderer 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import org.junit.Assert._ 6 | import org.psliwa.idea.composerJson.composer.model.PackageDescriptor 7 | import org.psliwa.idea.composerJson.fixtures.ComposerFixtures 8 | import org.psliwa.idea.composerJson.fixtures.ComposerFixtures._ 9 | import org.psliwa.idea.composerJson.intellij.codeAssist.InspectionTest 10 | 11 | class PackageInfoInspectionTest extends InspectionTest { 12 | 13 | override def setUp(): Unit = { 14 | super.setUp() 15 | 16 | myFixture.enableInspections(classOf[PackageInfoInspection]) 17 | 18 | val overlay: PackageInfoOverlay = getOverlay 19 | overlay.clearPackagesInfo() 20 | } 21 | 22 | private def getOverlay: PackageInfoOverlay = { 23 | val app = ApplicationManager.getApplication 24 | val overlay = app.getComponent(classOf[PackageInfoOverlay]) 25 | overlay 26 | } 27 | 28 | def testGivenInstalledPackage_itsVersionShouldBeCollected(): Unit = { 29 | createComposerLock(List(PackageDescriptor("some/pkg", "1.0.1"))) 30 | 31 | checkInspection( 32 | s""" 33 | |{ 34 | | "require": { 35 | | "some/pkg": "1.0.0" 36 | | } 37 | |} 38 | """.stripMargin 39 | ) 40 | 41 | assertPackageVersions(List(PackageInfo(myFixture.getCaretOffset, "1.0.1"))) 42 | } 43 | 44 | private def createComposerLock(packages: List[PackageDescriptor], dir: String = "."): VirtualFile = { 45 | ComposerFixtures.createComposerLock(myFixture, packages.map(ComposerPackageWithReplaces(_, Set.empty)), dir) 46 | } 47 | 48 | private def assertPackageVersions(expected: List[PackageInfo]): Unit = { 49 | assertEquals( 50 | expected, 51 | getOverlay.getPackagesInfo(myFixture.getFile.getVirtualFile.getCanonicalPath) 52 | ) 53 | } 54 | 55 | override def isWriteActionRequired: Boolean = false 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/FilePathReferenceTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import org.junit.Assert._ 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.FilePathReferences 5 | 6 | class FilePathReferenceTest extends FilePathReferences { 7 | 8 | def testGivenFileInArrayOfFilePaths_referenceShouldBeCreated(): Unit = { 9 | val file = "file.txt" 10 | 11 | checkFileReference(file, s""" 12 | |{ 13 | | "bin": [ "$file" ] 14 | |} 15 | """.stripMargin) 16 | } 17 | 18 | def testGivenFileInFilePathsObject_referenceShouldBeCreated(): Unit = { 19 | val file = "file.txt" 20 | 21 | checkFileReference( 22 | file, 23 | s""" 24 | |{ 25 | | "autoload": { 26 | | "psr-0": { 27 | | "": "$file" 28 | | } 29 | | } 30 | |} 31 | """.stripMargin 32 | ) 33 | } 34 | 35 | def testGivenFileInArrayInFilePathsObject_referenceShouldBeCreated(): Unit = { 36 | val file = "file.txt" 37 | 38 | checkFileReference( 39 | file, 40 | s""" 41 | |{ 42 | | "autoload": { 43 | | "psr-0": { 44 | | "": ["$file"] 45 | | } 46 | | } 47 | |} 48 | """.stripMargin 49 | ) 50 | } 51 | 52 | def testGivenNonFilePathProperty_referenceShouldNotBeCreated(): Unit = { 53 | val file = "file.txt" 54 | 55 | checkEmptyFileReferences(file, s""" 56 | |{ 57 | | "name": "$file" 58 | |} 59 | """.stripMargin) 60 | } 61 | 62 | def testGivenRequireProperty_referenceToVendorDirShouldBeCreated(): Unit = { 63 | writeAction(() => { 64 | myFixture.getTempDirFixture 65 | .findOrCreateDir("vendor") 66 | .createChildDirectory(this, "some-vendor") 67 | .createChildDirectory(this, "some-pkg") 68 | }) 69 | 70 | val references = getResolvedFileReferences( 71 | _.contains("vendor"), 72 | """ 73 | |{ 74 | | "require": { 75 | | "some-vendor/some-pkg": "" 76 | | } 77 | |} 78 | """.stripMargin 79 | ) 80 | 81 | assertEquals(2, references.length) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/file/UrlReferenceTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.file 2 | 3 | import org.junit.Assert._ 4 | import org.psliwa.idea.composerJson.ComposerJson 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.CompletionTest 6 | 7 | class UrlReferenceTest extends CompletionTest { 8 | 9 | def testGivenUrlProperty_givenUrlValue_valueShouldBeUrlReference(): Unit = { 10 | checkUrlReference( 11 | """ 12 | |{ 13 | | "homepage": "http://psliwa.org" 14 | |} 15 | """.stripMargin 16 | ) 17 | } 18 | 19 | def testGivenUrlProperty_givenInvalidUrlValue_valueShouldNotBeUrlReference(): Unit = { 20 | checkUrlReference( 21 | """ 22 | |{ 23 | | "homepage": "invalid" 24 | |} 25 | """.stripMargin, 26 | 0 27 | ) 28 | } 29 | 30 | def testGivenEmailProperty_givenEmailValue_valueShouldBeUrlReference(): Unit = { 31 | checkUrlReference( 32 | """ 33 | |{ 34 | | "support": { 35 | | "email": "me@psliwa.org" 36 | | } 37 | |} 38 | """.stripMargin 39 | ) 40 | } 41 | 42 | def testGivenEmailProperty_givenInvalidEmailValue_valueShouldNotBeUrlReference(): Unit = { 43 | checkUrlReference( 44 | """ 45 | |{ 46 | | "support": { 47 | | "email": "invalid" 48 | | } 49 | |} 50 | """.stripMargin, 51 | 0 52 | ) 53 | } 54 | 55 | def testGivenUrlProperty_givenUrlPropertyIsInFactOrProperty_givenValidUrl_valueShouldBeUrlReference(): Unit = { 56 | checkUrlReference( 57 | """ 58 | |{ 59 | | "repositories": [ 60 | | { 61 | | "url": "http://psliwa.org" 62 | | } 63 | | ] 64 | |} 65 | """.stripMargin 66 | ) 67 | } 68 | 69 | def testGivenPackageVersionProperty_valueShouldBeUrlReference(): Unit = { 70 | checkUrlReference( 71 | """ 72 | |{ 73 | | "require": { 74 | | "some/pkg": "1.0.0" 75 | | } 76 | |} 77 | """.stripMargin 78 | ) 79 | } 80 | 81 | private def checkUrlReference(s: String, expectedCount: Int = 1): Unit = { 82 | myFixture.configureByText(ComposerJson, s) 83 | 84 | val element = myFixture.getFile.findElementAt(myFixture.getCaretOffset).getParent 85 | 86 | val references = element.getReferences 87 | .filter(_.isInstanceOf[UrlPsiReference]) 88 | 89 | assertEquals(expectedCount, references.length) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/schema/CharContainsMatcherTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.schema 2 | 3 | import org.junit.Test 4 | import org.junit.Assert._ 5 | import org.psliwa.idea.composerJson.intellij.CharContainsMatcher 6 | 7 | class CharContainsMatcherTest { 8 | 9 | @Test 10 | def givenExactPrefix_expectMatch(): Unit = { 11 | assertTrue(matches("some text", "some text")) 12 | } 13 | 14 | @Test 15 | def givenPartialPrefix_expectMatch(): Unit = { 16 | assertTrue(matches("some", "some text")) 17 | } 18 | 19 | @Test 20 | def givenEverySecondCharPrefix_expectMatch(): Unit = { 21 | assertTrue(matches("smtx", "some text")) 22 | } 23 | 24 | @Test 25 | def givenEveryCharsButInDifferentOrderAsPrefix_expectNotMatch(): Unit = { 26 | assertFalse(matches("emos txet", "some text")) 27 | } 28 | 29 | @Test 30 | def givenThreeTimesTheSameChar_inGivenValueTheCharOccursOnlyTwice_expectNotMatch(): Unit = { 31 | assertFalse(matches("222", "2.2.8")) 32 | } 33 | 34 | @Test 35 | def givenPrefixIsLongerThanValue_expectNotMatch(): Unit = { 36 | assertFalse(matches("some text and more", "some text")) 37 | } 38 | 39 | @Test 40 | def givenAnagram_expectNotMatch(): Unit = { 41 | assertFalse(matches("smeo", "some")) 42 | } 43 | 44 | @Test 45 | def givenAnagramPlusExtraChars_expectNotMatch(): Unit = { 46 | assertFalse(matches("smeoe", "some")) 47 | } 48 | 49 | private def matches(prefix: String, value: String): Boolean = { 50 | new CharContainsMatcher(prefix).prefixMatches(value) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/schema/SchemaDocumentationProviderTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.schema 2 | 3 | import com.intellij.lang.documentation.DocumentationProvider 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.DocumentationTest 5 | import org.junit.Assert._ 6 | import org.psliwa.idea.composerJson._ 7 | 8 | class SchemaDocumentationProviderTest extends DocumentationTest { 9 | override protected def documentationProvider: DocumentationProvider = new SchemaDocumentationProvider 10 | 11 | def testGivenPropertyOnFirstLevel_docShouldBeFromSchemaDesc(): Unit = { 12 | checkDocumentation( 13 | """ 14 | |{ 15 | | "name": "" 16 | |} 17 | """.stripMargin, 18 | "Package name, including 'vendor-name/' prefix." 19 | ) 20 | } 21 | 22 | def testGivenNestedProperty_docShouldBeFromSchemaDesc(): Unit = { 23 | checkDocumentation( 24 | """ 25 | |{ 26 | | "support": { 27 | | "email": "" 28 | | } 29 | |} 30 | """.stripMargin, 31 | "Email address for support." 32 | ) 33 | } 34 | 35 | def testGivenPropertyInArray_docShouldBeFromSchemaDesc(): Unit = { 36 | checkDocumentation( 37 | """ 38 | |{ 39 | | "authors": [ 40 | | { 41 | | "name": "" 42 | | } 43 | | ] 44 | |} 45 | """.stripMargin, 46 | "Full name of the author." 47 | ) 48 | } 49 | 50 | def testGivenPropertyInTopLevel_externalDocUrlShouldExist(): Unit = { 51 | checkDocumentation( 52 | """ 53 | |{ 54 | | "name": "" 55 | |} 56 | """.stripMargin, 57 | List("getcomposer.org/doc/04-schema.md#name") 58 | ) 59 | } 60 | 61 | def testGivenNotComposerJsonFile_docsShouldNotBeFound(): Unit = { 62 | val unexpectedUrl = "getcomposer.org/doc/04-schema.md#name" 63 | checkDocumentation( 64 | """ 65 | |{ 66 | | "name": "" 67 | |} 68 | """.stripMargin, 69 | urls => urls.foreach(url => assertFalse(url.contains(unexpectedUrl))), 70 | _ => (), 71 | "some.json" 72 | ) 73 | } 74 | 75 | def testGivenFileWithNewLine_thereShouldNotBeNullPointerEx(): Unit = { 76 | val s = """ 77 | | 78 | | 79 | """.stripMargin 80 | myFixture.configureByText(ComposerJson, s.replace("\r", "")) 81 | 82 | try { 83 | val element = myFixture.getElementAtCaret 84 | documentationProvider.getUrlFor(element, element) 85 | } catch { 86 | case ex: AssertionError if ex.getMessage.startsWith("element not found") => 87 | // ignore - in this case from 2018.1 element at caret is not found, so exception is thrown 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptAliasReferenceTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import org.junit.Assert 4 | import org.psliwa.idea.composerJson.ComposerJson 5 | import org.psliwa.idea.composerJson.intellij.codeAssist.CompletionTest 6 | import org.psliwa.idea.composerJson.util.ImplicitConversions._ 7 | 8 | class ScriptAliasReferenceTest extends CompletionTest { 9 | 10 | def testScriptAliasesSuggestions_thereAreFewScripts_suggestThem(): Unit = { 11 | suggestions( 12 | """ 13 | |{ 14 | | "scripts": { 15 | | "script1": "", 16 | | "script2": "", 17 | | "script3": "" 18 | | } 19 | |} 20 | |""".stripMargin, 21 | Array("@script1", "@script2") 22 | ) 23 | } 24 | 25 | def testScriptAliasesSuggestions_suggestComposerAndPhpSpecialScripts(): Unit = { 26 | suggestions( 27 | """ 28 | |{ 29 | | "scripts": { 30 | | "script1": "" 31 | | } 32 | |} 33 | |""".stripMargin, 34 | Array("@composer", "@php") 35 | ) 36 | } 37 | 38 | def testScriptAliasesSuggestions_skipScriptWhereIsCaret(): Unit = { 39 | suggestions( 40 | """ 41 | |{ 42 | | "scripts": { 43 | | "script1": "", 44 | | "script2": "", 45 | | "script3": "" 46 | | } 47 | |} 48 | |""".stripMargin, 49 | Array(), 50 | Array("@script3") 51 | ) 52 | } 53 | 54 | def testScriptAliasesReferences_referenceExists(): Unit = { 55 | val references = getResolvedFileReferences( 56 | """ 57 | |{ 58 | | "scripts": { 59 | | "script1": "", 60 | | "script3": "@script1" 61 | | } 62 | |} 63 | |""".stripMargin 64 | ) 65 | 66 | Assert.assertEquals(List("script1"), references) 67 | } 68 | 69 | private def getResolvedFileReferences(content: String): List[String] = { 70 | myFixture.configureByText(ComposerJson, content) 71 | 72 | val element = myFixture.getFile.findElementAt(myFixture.getCaretOffset).getParent 73 | 74 | element.getReferences 75 | .collect { case reference: ScriptAliasReference => reference } 76 | .flatMap(_.multiResolve(false)) 77 | .map(_.getElement.getText) 78 | .map(_.stripQuotes) 79 | .toList 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/intellij/codeAssist/scripts/ScriptsReferenceTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.intellij.codeAssist.scripts 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.psliwa.idea.composerJson.intellij.codeAssist.FilePathReferences 5 | 6 | class ScriptsReferenceTest extends FilePathReferences { 7 | 8 | def testScriptReference_givenExistingScript_referenceShouldExist(): Unit = { 9 | createScriptFile("superScript") 10 | 11 | val references = getResolvedFileReferences( 12 | _.contains("vendor"), 13 | """ 14 | |{ 15 | | "scripts": { 16 | | "custom": "superScript" 17 | | } 18 | |} 19 | """.stripMargin 20 | ) 21 | 22 | assertEquals(1, references.length) 23 | } 24 | 25 | def testScriptReference_givenExistingScriptWithParameters_referenceShouldExist(): Unit = { 26 | createScriptFile("superScript") 27 | 28 | val references = getResolvedFileReferences( 29 | _.contains("vendor"), 30 | """ 31 | |{ 32 | | "scripts": { 33 | | "custom": "superScript --some-param" 34 | | } 35 | |} 36 | """.stripMargin 37 | ) 38 | 39 | assertEquals(1, references.length) 40 | } 41 | 42 | def testScriptReference_givenUnexistingScript_referenceShouldNotExist(): Unit = { 43 | createScriptFile("superScript") 44 | 45 | val references = getResolvedFileReferences( 46 | _.contains("vendor"), 47 | """ 48 | |{ 49 | | "scripts": { 50 | | "custom": "anotherScript" 51 | | } 52 | |} 53 | """.stripMargin 54 | ) 55 | 56 | assertEquals(0, references.length) 57 | } 58 | 59 | def testScriptReference_givenExistingScript_commandShouldBeCompleted(): Unit = { 60 | createScriptFile("superScript") 61 | 62 | completion( 63 | """ 64 | |{ 65 | | "scripts": { 66 | | "custom": "sup" 67 | | } 68 | |} 69 | """.stripMargin, 70 | """ 71 | |{ 72 | | "scripts": { 73 | | "custom": "superScript" 74 | | } 75 | |} 76 | """.stripMargin 77 | ) 78 | } 79 | 80 | private def createScriptFile(scriptName: String): Unit = { 81 | writeAction(() => { 82 | myFixture.getTempDirFixture 83 | .findOrCreateDir("vendor") 84 | .createChildDirectory(this, "bin") 85 | .findOrCreateChildData(this, scriptName) 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/json/FormatTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.json 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | 6 | class FormatTest { 7 | @Test 8 | def givenValidUrl_itShouldBeValidUri(): Unit = { 9 | assertTrue(UriFormat.isValid("http://somevalid.url.com/some?query=123")) 10 | } 11 | 12 | @Test 13 | def givenInvalidUrl_itShouldBeInvalidUri(): Unit = { 14 | assertFalse(UriFormat.isValid("invalid url")) 15 | } 16 | 17 | @Test 18 | def givenEmails_checkTheyValidity(): Unit = { 19 | val emails = List( 20 | "me@psliwa.org" -> true, 21 | "some+123@gmail.com" -> true, 22 | "some.value@xyz.xyz.com" -> true, 23 | "some value@xyz.xyz.com" -> false, 24 | "some.value@xyz" -> false, 25 | "abc" -> false 26 | ) 27 | 28 | assertEquals(emails.map(_._2), emails.map { case (email, _) => EmailFormat.isValid(email) }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/json/SchemaConversions.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.json 2 | 3 | import scala.language.implicitConversions 4 | 5 | object SchemaConversions { 6 | implicit def stringProp(s: String): (String, Property) = (s, Property(SString(AnyFormat), required = false, "")) 7 | implicit def schemaToProperty(s: Schema): Property = Property(s, required = false, "") 8 | } 9 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/json/SchemaLoadingTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.json 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | import org.psliwa.idea.composerJson.ComposerSchemaFilepath 6 | 7 | class SchemaLoadingTest { 8 | 9 | @Test 10 | def loadComposerCompletionSchema(): Unit = { 11 | assertNotEquals(None, SchemaLoader.load(ComposerSchemaFilepath)) 12 | } 13 | 14 | @Test 15 | def loadComposerSchema_givenPathIsInvalid_returnNone(): Unit = { 16 | assertEquals(None, SchemaLoader.load("invalid-file")) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/scala/org/psliwa/idea/composerJson/settings/PatternItemTest.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.settings 2 | 3 | import org.junit.Assert._ 4 | import org.junit.Test 5 | 6 | class PatternItemTest { 7 | 8 | @Test 9 | def givenExactPattern_givenTheSameText_itShouldMatch(): Unit = { 10 | assertPatternMatch("vendor/pkg", "vendor/pkg") 11 | } 12 | 13 | @Test 14 | def givenExactPattern_givenDifferentText_itShouldNotMatch(): Unit = { 15 | assertPatternNotMatch("vendor/pkg", "vendor2/pkg") 16 | } 17 | 18 | @Test 19 | def givenExactPattern_givenText_patternIsTextPrefix_itShouldNotMatch(): Unit = { 20 | assertPatternNotMatch("vendor/pkg", "vendor/pkg2") 21 | } 22 | 23 | @Test 24 | def givenWildcardPattern_givenMatchingText_itShouldMatch(): Unit = { 25 | assertPatternMatch("vendor/*", "vendor/pkg") 26 | } 27 | 28 | @Test 29 | def givenWildcardPattern_givenWildcardIsInTheMiddle_givenMatchingText_itShouldMatch(): Unit = { 30 | assertPatternMatch("vend*kg", "vendor/pkg") 31 | } 32 | 33 | @Test 34 | def givenInvalidPattern_isShouldNotMatch(): Unit = { 35 | assertPatternNotMatch("[ ? abc .-", "vendor/pkg") 36 | } 37 | 38 | @Test 39 | def givenEmptyPattern_isShouldNotMatch(): Unit = { 40 | assertPatternNotMatch("", "vendor/pkg") 41 | } 42 | 43 | private def assertPatternMatch(pattern: String, text: String, expectedMatch: Boolean = true): Unit = { 44 | assertEquals(expectedMatch, new PatternItem(pattern).matches(text)) 45 | } 46 | 47 | private def assertPatternNotMatch(pattern: String, text: String): Unit = { 48 | assertPatternMatch(pattern, text, expectedMatch = false) 49 | } 50 | } 51 | --------------------------------------------------------------------------------