├── project ├── build.properties ├── Versions.scala └── plugins.sbt ├── .github └── FUNDING.yml ├── src ├── test │ ├── resources │ │ └── org │ │ │ └── psliwa │ │ │ └── idea │ │ │ └── composerJson │ │ │ └── inspection │ │ │ ├── doctrine │ │ │ ├── bin │ │ │ │ ├── doctrine │ │ │ │ └── doctrine.php │ │ │ ├── lib │ │ │ │ └── .gitkeep │ │ │ └── composer.json │ │ │ ├── laravel │ │ │ ├── app │ │ │ │ ├── commands │ │ │ │ │ └── .gitkeep │ │ │ │ ├── models │ │ │ │ │ └── .gitkeep │ │ │ │ ├── controllers │ │ │ │ │ └── .gitkeep │ │ │ │ ├── tests │ │ │ │ │ └── TestCase.php │ │ │ │ └── database │ │ │ │ │ ├── seeds │ │ │ │ │ └── .gitkeep │ │ │ │ │ └── migrations │ │ │ │ │ └── .gitkeep │ │ │ └── composer.json │ │ │ ├── symfony_standard │ │ │ ├── web │ │ │ │ └── .gitkeep │ │ │ ├── app │ │ │ │ └── config │ │ │ │ │ └── parameters.yml │ │ │ ├── src │ │ │ │ └── classes.php │ │ │ └── composer.json │ │ │ └── symfony │ │ │ └── src │ │ │ └── Symfony │ │ │ └── Component │ │ │ ├── Intl │ │ │ └── Resources │ │ │ │ └── stubs │ │ │ │ └── functions.php │ │ │ └── HttpFoundation │ │ │ └── Resources │ │ │ └── stubs │ │ │ └── .gitkeep │ └── scala │ │ └── org │ │ └── psliwa │ │ └── idea │ │ └── composerJson │ │ ├── json │ │ ├── SchemaConversions.scala │ │ ├── SchemaLoadingTest.scala │ │ └── FormatTest.scala │ │ ├── BasePropSpec.scala │ │ ├── composer │ │ ├── repository │ │ │ ├── LoadPackagesTest.scala │ │ │ ├── LoadVersionsTest.scala │ │ │ └── DefaultRepositoryFactoryTest.scala │ │ └── model │ │ │ ├── version │ │ │ ├── ConstraintTest │ │ │ │ ├── ParsePresentationTest.scala │ │ │ │ └── PresentationStringTest.scala │ │ │ ├── VersionComparatorTest.scala │ │ │ └── VersionEquivalentsTest.scala │ │ │ └── repository │ │ │ ├── ComposedRepositoryTest.scala │ │ │ ├── RepositoryProviderWrapperTest.scala │ │ │ └── RepositoryGenerators.scala │ │ ├── intellij │ │ └── codeAssist │ │ │ ├── composer │ │ │ ├── AbstractPackagesTest.scala │ │ │ ├── PackageDocumentationProviderTest.scala │ │ │ ├── infoRenderer │ │ │ │ └── PackageInfoInspectionTest.scala │ │ │ └── PackageVersionInspectionTest.scala │ │ │ ├── FilePathReferences.scala │ │ │ ├── schema │ │ │ ├── CharContainsMatcherTest.scala │ │ │ └── SchemaDocumentationProviderTest.scala │ │ │ ├── ValidComposerJsonFilesInspectionTest.scala │ │ │ ├── DocumentationTest.scala │ │ │ ├── file │ │ │ ├── FilePathReferenceTest.scala │ │ │ └── UrlReferenceTest.scala │ │ │ ├── scripts │ │ │ ├── ScriptAliasReferenceTest.scala │ │ │ └── ScriptsReferenceTest.scala │ │ │ ├── CompletionTest.scala │ │ │ └── InspectionTest.scala │ │ ├── settings │ │ └── PatternItemTest.scala │ │ └── fixtures │ │ └── ComposerFixtures.scala └── main │ ├── resources │ └── org │ │ └── psliwa │ │ └── idea │ │ └── composerJson │ │ └── icons │ │ ├── composer.png │ │ ├── packagist.png │ │ ├── composer@x2.png │ │ └── packagist@2x.png │ ├── scala │ └── org │ │ └── psliwa │ │ └── idea │ │ └── composerJson │ │ ├── composer │ │ ├── parsers │ │ │ └── RepositoryPackages.scala │ │ ├── model │ │ │ ├── repository │ │ │ │ ├── RepositoryInfo.scala │ │ │ │ ├── CallbackRepository.scala │ │ │ │ ├── InMemoryRepository.scala │ │ │ │ ├── ComposedRepository.scala │ │ │ │ ├── TestingRepositoryProvider.scala │ │ │ │ ├── RepositoryProvider.scala │ │ │ │ ├── RepositoryProviderWrapper.scala │ │ │ │ ├── SkipBuiltInPackagesVersionRepository.scala │ │ │ │ └── Repository.scala │ │ │ ├── PackageName.scala │ │ │ ├── Packages.scala │ │ │ ├── PackageDescriptor.scala │ │ │ └── version │ │ │ │ └── VersionEquivalents.scala │ │ └── repository │ │ │ └── Packagist.scala │ │ ├── intellij │ │ ├── codeAssist │ │ │ ├── composer │ │ │ │ ├── infoRenderer │ │ │ │ │ ├── PackageInfo.scala │ │ │ │ │ ├── PackageInfoOverlayView.scala │ │ │ │ │ ├── PackageInfoOverlay.scala │ │ │ │ │ ├── PackageInfoInspection.scala │ │ │ │ │ └── PackageInfoCaretListener.scala │ │ │ │ ├── package.scala │ │ │ │ ├── ExcludePatternAction.scala │ │ │ │ ├── NotInstalledPackageInspection.scala │ │ │ │ ├── NotInstalledPackages.scala │ │ │ │ ├── PackageDocumentationProvider.scala │ │ │ │ ├── InstallPackagesAction.scala │ │ │ │ ├── CustomRepositoriesEditorNotificationProvider.scala │ │ │ │ └── PackagesLoader.scala │ │ │ ├── References.scala │ │ │ ├── ComposerJsonSchemaExclusion.scala │ │ │ ├── problem │ │ │ │ ├── CheckResult.scala │ │ │ │ ├── ProblemChecker.scala │ │ │ │ ├── ProblemDescriptor.scala │ │ │ │ ├── checker │ │ │ │ │ ├── PropertyChecker.scala │ │ │ │ │ ├── MultiplePropertiesChecker.scala │ │ │ │ │ └── Checker.scala │ │ │ │ ├── PropertyPath.scala │ │ │ │ └── Condition.scala │ │ │ ├── file │ │ │ │ ├── FilePathReferenceProvider.scala │ │ │ │ ├── UrlReferenceContributor.scala │ │ │ │ ├── UrlReferenceProvider.scala │ │ │ │ ├── UrlPsiReference.scala │ │ │ │ ├── PackageVersionReferenceProvider.scala │ │ │ │ ├── FilePathReferenceContributor.scala │ │ │ │ └── PackageReferenceProvider.scala │ │ │ ├── scripts │ │ │ │ ├── ScriptsPsiElementPattern.scala │ │ │ │ ├── ScriptsReferenceContributor.scala │ │ │ │ ├── ScriptsReference.scala │ │ │ │ └── ScriptAliasReference.scala │ │ │ ├── AutoPopupInsertHandler.scala │ │ │ ├── php │ │ │ │ ├── PhpClassInsertHandler.scala │ │ │ │ ├── PhpUtils.scala │ │ │ │ ├── PhpReferenceContributor.scala │ │ │ │ └── PhpNamespaceReference.scala │ │ │ ├── IntentionActionQuickFixAdapter.scala │ │ │ ├── QuoteInsertHandler.scala │ │ │ ├── QuickFix.scala │ │ │ ├── schema │ │ │ │ ├── ShowValidValuesQuickFix.scala │ │ │ │ ├── RemoveQuotesQuickFix.scala │ │ │ │ └── CompletionContributor.scala │ │ │ ├── AbstractInspection.scala │ │ │ ├── QuickFixIntentionActionAdapter.scala │ │ │ ├── package.scala │ │ │ ├── AbstractReferenceContributor.scala │ │ │ ├── BaseLookupElement.scala │ │ │ ├── RemoveJsonElementQuickFix.scala │ │ │ └── SetPropertyValueQuickFix.scala │ │ ├── filetype │ │ │ ├── ComposerJsonFileTypeFactory.scala │ │ │ └── ComposerJsonFileType.scala │ │ ├── Patterns.scala │ │ ├── CharContainsMatcher.scala │ │ ├── PsiElementOffsetFinder.scala │ │ ├── PsiExtractors.scala │ │ ├── Notifications.scala │ │ ├── NotificationsHandler.scala │ │ └── PsiElements.scala │ │ ├── settings │ │ ├── TabularSettings.scala │ │ └── AppSettings.scala │ │ ├── util │ │ ├── ImplicitConversions.scala │ │ ├── parsers │ │ │ ├── JSON.scala │ │ │ ├── Location.scala │ │ │ ├── package.scala │ │ │ ├── Result.scala │ │ │ ├── ParserMonad.scala │ │ │ ├── Implicits.scala │ │ │ ├── Parsers.scala │ │ │ └── ParserOps.scala │ │ ├── StringOps.scala │ │ ├── TryMonoid.scala │ │ ├── Funcs.scala │ │ ├── CharOffsetFinder.scala │ │ ├── IO.scala │ │ ├── Files.scala │ │ └── OffsetFinder.scala │ │ ├── Icons.scala │ │ ├── package.scala │ │ ├── json │ │ ├── SchemaLoader.scala │ │ └── Format.scala │ │ └── ComposerBundle.scala │ └── java │ └── org │ └── psliwa │ └── idea │ └── composerJson │ ├── settings │ ├── EnabledItem.java │ ├── TextItem.java │ └── PatternItem.java │ └── ui │ └── ChooserDialog.form ├── .jvmopts ├── .scalafmt.conf ├── .gitignore ├── proguard.pro ├── .travis.yml ├── CONTRIBUTING.md ├── benchmarks └── src │ └── main │ └── scala │ └── org │ └── psliwa │ └── idea │ └── composerJson │ └── benchmarks │ └── VersionSuggestionsBenchmark.scala ├── LICENSE └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.3 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [psliwa] 2 | custom: https://www.paypal.me/psliwa 3 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/lib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/doctrine/bin/doctrine.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/commands/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/web/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/seeds/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/laravel/app/database/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/app/config/parameters.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/Intl/Resources/stubs/functions.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/org/psliwa/idea/composerJson/inspection/symfony/src/Symfony/Component/HttpFoundation/Resources/stubs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -XX:MaxMetaspaceSize=1g 2 | -Xss1m 3 | -Xms1536m 4 | -Xmx2G 5 | -XX:ReservedCodeCacheSize=128m 6 | -XX:+CMSClassUnloadingEnabled 7 | -Dfile.encoding=UTF8 8 | -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/composer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/HEAD/src/main/resources/org/psliwa/idea/composerJson/icons/composer.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/packagist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/HEAD/src/main/resources/org/psliwa/idea/composerJson/icons/packagist.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/composer@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/HEAD/src/main/resources/org/psliwa/idea/composerJson/icons/composer@x2.png -------------------------------------------------------------------------------- /src/main/resources/org/psliwa/idea/composerJson/icons/packagist@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psliwa/idea-composer-plugin/HEAD/src/main/resources/org/psliwa/idea/composerJson/icons/packagist@2x.png -------------------------------------------------------------------------------- /.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] -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/test/scala/org/psliwa/idea/composerJson/BasePropSpec.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson 2 | 3 | import org.scalacheck.Prop 4 | import org.scalatest.PropSpec 5 | import org.scalatestplus.scalacheck.Checkers 6 | 7 | abstract class BasePropSpec extends PropSpec with Checkers { 8 | protected def property(testName: String)(testFun: => Prop, params: PropertyCheckConfigParam*): Unit = { 9 | super.property(testName)(check(testFun, params: _*)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/test/resources/org/psliwa/idea/composerJson/inspection/symfony_standard/src/classes.php: -------------------------------------------------------------------------------- 1 | Option(x) 22 | case _ => None 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/composer/model/version/VersionEquivalents.scala: -------------------------------------------------------------------------------- 1 | package org.psliwa.idea.composerJson.composer.model.version 2 | 3 | import scala.language.postfixOps 4 | 5 | object VersionEquivalents { 6 | def equivalentsFor(version: Constraint): Seq[Constraint] = { 7 | nsrEquivalent(version).toList 8 | } 9 | 10 | private def nsrEquivalent(version: Constraint): Option[Constraint] = { 11 | def incrementVersion(version: SemanticVersion): Option[SemanticVersion] = { 12 | version.dropLast 13 | .map(_.incrementLast) 14 | .flatMap(_.append(0)) 15 | } 16 | 17 | Option(version.replace { 18 | //~ support 19 | case OperatorConstraint(ConstraintOperator.~, SemanticConstraint(versionFrom), _) => 20 | incrementVersion(versionFrom.ensureParts(2)) 21 | .map(versionTo => versionRange(versionFrom.ensureParts(2), versionTo.fillZero)) 22 | //example: >=1.2 <3.0.0 to ~1.2 23 | case VersionRange(versionFrom, versionTo) 24 | if versionFrom.dropZeros.partsNumber < 3 && incrementVersion(versionFrom.ensureExactlyParts(2)) 25 | .exists(_.fillZero == versionTo.fillZero) => 26 | Some(OperatorConstraint(ConstraintOperator.~, SemanticConstraint(versionFrom.dropZeros.ensureParts(2)), "")) 27 | //example: >=1.2.1 <1.3.0 to ~1.2.1 28 | case VersionRange(versionFrom, versionTo) 29 | if incrementVersion(versionFrom.fillZero).exists(_.fillZero == versionTo.fillZero) => 30 | Some(OperatorConstraint(ConstraintOperator.~, SemanticConstraint(versionFrom.ensureParts(3)), "")) 31 | //^ support for pre-release: ^0.3.1 to >=0.3.1 <0.4.0 32 | case OperatorConstraint(ConstraintOperator.^, SemanticConstraint(versionFrom), _) 33 | if versionFrom.partsNumber > 1 && versionFrom.major == 0 => 34 | incrementVersion(versionFrom.ensureParts(3)) 35 | .map(versionTo => versionRange(versionFrom, versionTo.fillZero)) 36 | //^ support 37 | case OperatorConstraint(ConstraintOperator.^, SemanticConstraint(versionFrom), _) => 38 | incrementVersion(versionFrom.ensureExactlyParts(2)) 39 | .map(versionTo => versionRange(versionFrom, versionTo.fillZero)) 40 | //example: >=1.2.1 <2.0.0 to ^1.2.1 41 | case VersionRange(versionFrom, versionTo) 42 | if versionFrom.partsNumber == 3 && incrementVersion(versionFrom.ensureExactlyParts(2)) 43 | .exists(_.fillZero == versionTo.fillZero) => 44 | Some(OperatorConstraint(ConstraintOperator.^, SemanticConstraint(versionFrom))) 45 | case _ => None 46 | }).filter(_ != version) 47 | } 48 | 49 | private def versionRange(versionFrom: SemanticVersion, versionTo: SemanticVersion): Constraint = { 50 | LogicalConstraint( 51 | List( 52 | OperatorConstraint(ConstraintOperator.>=, SemanticConstraint(versionFrom)), 53 | OperatorConstraint(ConstraintOperator.<, SemanticConstraint(versionTo)) 54 | ), 55 | LogicalOperator.AND, 56 | " " 57 | ) 58 | } 59 | 60 | private object VersionRange { 61 | def unapply(x: Constraint): Option[(SemanticVersion, SemanticVersion)] = x match { 62 | case LogicalConstraint( 63 | List(OperatorConstraint(ConstraintOperator.>=, SemanticConstraint(versionFrom), _), 64 | OperatorConstraint(ConstraintOperator.<, SemanticConstraint(versionTo), _)), 65 | LogicalOperator.AND, 66 | _ 67 | ) => 68 | Some((versionFrom, versionTo)) 69 | case _ => None 70 | } 71 | } 72 | } 73 | --------------------------------------------------------------------------------