diff --git a/build.sbt b/build.sbt index 4bd89d36885..4575e195186 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,7 @@ import java.nio.file.Paths // Global build settings -(ThisBuild / intellijPluginName) := "Scala" +(ThisBuild / intellijPluginName) := "Scala (AVSystem)" (ThisBuild / intellijBuild) := Versions.intellijVersion diff --git a/pluginXml/resources/META-INF/plugin.xml b/pluginXml/resources/META-INF/plugin.xml index 2e7a50dd2f0..f2fd5688d46 100644 --- a/pluginXml/resources/META-INF/plugin.xml +++ b/pluginXml/resources/META-INF/plugin.xml @@ -16,10 +16,11 @@ - org.intellij.scala - Scala + com.avsystem.scala + Scala (AVSystem) Temporary customized version by AVSystem Adds support for the Scala language. The following features are available for free with IntelliJ IDEA Community Edition:
+ AVSystem experimental extensions: +
Support for Play Framework, Akka and Scala.js is enabled in IntelliJ IDEA Ultimate. ]]>
- VERSION + 2022.3.99-sbtcaching.1 JetBrains diff --git a/sbt/sbt-api/resources/META-INF/sbt-api.xml b/sbt/sbt-api/resources/META-INF/sbt-api.xml index f27bb6194ce..6baa70db89e 100644 --- a/sbt/sbt-api/resources/META-INF/sbt-api.xml +++ b/sbt/sbt-api/resources/META-INF/sbt-api.xml @@ -1,6 +1,7 @@ + diff --git a/sbt/sbt-api/src/org/jetbrains/sbt/project/SbtStructureCache.scala b/sbt/sbt-api/src/org/jetbrains/sbt/project/SbtStructureCache.scala new file mode 100644 index 00000000000..add3617bd94 --- /dev/null +++ b/sbt/sbt-api/src/org/jetbrains/sbt/project/SbtStructureCache.scala @@ -0,0 +1,15 @@ +package org.jetbrains.sbt.project + +import org.jetbrains.sbt.project.SbtStructureCache.BuildDefinitionHash + +trait SbtStructureCache { + def getCachedStructureXml(key: BuildDefinitionHash): Option[String] + + def cacheStructureXml(key: BuildDefinitionHash, data: String): Unit +} +object SbtStructureCache { + final case class BuildDefinitionHash(hash: String) { + override def toString: String = hash + } +} + diff --git a/sbt/sbt-api/src/org/jetbrains/sbt/settings/SbtSettings.scala b/sbt/sbt-api/src/org/jetbrains/sbt/settings/SbtSettings.scala index 9e750e2efad..34c03f06517 100644 --- a/sbt/sbt-api/src/org/jetbrains/sbt/settings/SbtSettings.scala +++ b/sbt/sbt-api/src/org/jetbrains/sbt/settings/SbtSettings.scala @@ -33,6 +33,10 @@ final class SbtSettings(project: Project) @BeanProperty var customVMEnabled: Boolean = false @BeanProperty var customVMPath: String = "" @BeanProperty var customSbtStructurePath: String = "" + @BeanProperty var localStructureCachePath: String = "" + @BeanProperty var remoteStructureCacheUri: String = "" + @BeanProperty var remoteStructureCacheCredentialsPath: String = "" + @BeanProperty var allowManualDependencyDownload: Boolean = false override def getState: SbtSettings.State = { val state = new SbtSettings.State @@ -45,7 +49,10 @@ final class SbtSettings(project: Project) state.customVMEnabled = customVMEnabled state.customVMPath = customVMPath state.customSbtStructurePath = customSbtStructurePath - + state.localStructureCache = localStructureCachePath + state.remoteStructureCacheUri = remoteStructureCacheUri + state.remoteStructureCacheCredentialsPath = remoteStructureCacheCredentialsPath + state.allowManualDependencyDownload = allowManualDependencyDownload state } @@ -59,6 +66,10 @@ final class SbtSettings(project: Project) customVMEnabled = state.customVMEnabled customVMPath = state.customVMPath customSbtStructurePath = state.customSbtStructurePath + localStructureCachePath = state.localStructureCache + remoteStructureCacheUri = state.remoteStructureCacheUri + remoteStructureCacheCredentialsPath = state.remoteStructureCacheCredentialsPath + allowManualDependencyDownload = state.allowManualDependencyDownload } override def subscribe(listener: ExternalSystemSettingsListener[SbtProjectSettings]): Unit = { @@ -79,6 +90,10 @@ final class SbtSettings(project: Project) customVMEnabled = settings.customVMEnabled customVMPath = settings.customVMPath customSbtStructurePath = settings.customSbtStructurePath + localStructureCachePath = settings.localStructureCachePath + remoteStructureCacheUri = settings.remoteStructureCacheUri + remoteStructureCacheCredentialsPath = settings.remoteStructureCacheCredentialsPath + allowManualDependencyDownload = settings.allowManualDependencyDownload } def getLinkedProjectSettings(module: Module): Option[SbtProjectSettings] = @@ -124,6 +139,18 @@ object SbtSettings { @BeanProperty var customSbtStructurePath: String = "" + @BeanProperty + var localStructureCache: String = "" + + @BeanProperty + var remoteStructureCacheUri: String = "" + + @BeanProperty + var remoteStructureCacheCredentialsPath: String = "" + + @BeanProperty + var allowManualDependencyDownload: Boolean = false + private val linkedProjectSettings: util.TreeSet[SbtProjectSettings] = new util.TreeSet[SbtProjectSettings] @XCollection(style = XCollection.Style.v1, elementTypes = Array(classOf[SbtProjectSettings])) diff --git a/sbt/sbt-impl/resources/messages/SbtBundle.properties b/sbt/sbt-impl/resources/messages/SbtBundle.properties index 5fe14826c6c..67fc5bded7e 100644 --- a/sbt/sbt-impl/resources/messages/SbtBundle.properties +++ b/sbt/sbt-impl/resources/messages/SbtBundle.properties @@ -169,6 +169,10 @@ sbt.settings.bundled=&Bundled ### org/jetbrains/sbt/settings/SbtSettingsPane.java sbt.settings.choose.custom.launcher=Choose a Custom Launcher sbt.settings.choose.sbt.launch.jar=Choose sbt-launch.jar +sbt.settings.choose.cache.directory=Choose Cache Directory +sbt.settings.choose.local.structure.cache.directory=Choose local sbt structure cache directory +sbt.settings.choose.remote.cache.credentials=Choose Remote Cache Credentials +sbt.settings.choose.remote.structure.cache.credentials.file=Choose remote sbt structure cache credentials file ### org/jetbrains/sbt/shell/SbtProcessManager.scala sbt.shell.disable.version.override=Disable version override diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtExternalSystemManager.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtExternalSystemManager.scala index 08c1263ac01..a9f42973b4b 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtExternalSystemManager.scala +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtExternalSystemManager.scala @@ -23,6 +23,7 @@ import org.jetbrains.sbt.project.settings._ import org.jetbrains.sbt.settings.{SbtExternalSystemConfigurable, SbtSettings} import java.io.File +import java.net.URI import scala.jdk.CollectionConverters._ class SbtExternalSystemManager @@ -84,6 +85,22 @@ object SbtExternalSystemManager { val vmOptions = getVmOptions(settingsState, jreHome) val environment = Map.empty ++ getAndroidEnvironmentVariables(projectJdkName) + val localStructureCacheFile = + Option(settingsState.localStructureCache) + .filter(_.nonEmpty) + .map(new File(_)) + + val remoteStructureCacheUri = + Option(settingsState.remoteStructureCacheUri) + .filter(_.nonEmpty) + .map(v => if (v.endsWith("/")) v else s"$v/") + .map(new URI(_)) + + val remoteStructureCacheCredentialsFile = + Option(settingsState.remoteStructureCacheCredentialsPath) + .filter(_.nonEmpty) + .map(new File(_)) + new SbtExecutionSettings( realProjectPath, vmExecutable, @@ -92,6 +109,9 @@ object SbtExternalSystemManager { environment, customLauncher, customSbtStructureFile, + localStructureCacheFile, + remoteStructureCacheUri, + remoteStructureCacheCredentialsFile, projectJdkName, projectSettings.resolveClassifiers, projectSettings.resolveJavadocs, @@ -99,7 +119,8 @@ object SbtExternalSystemManager { projectSettings.useSbtShellForImport, projectSettings.enableDebugSbtShell, projectSettings.allowSbtVersionOverride, - projectSettings.preferScala2 + projectSettings.preferScala2, + settingsState.allowManualDependencyDownload ) } diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtProjectResolver.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtProjectResolver.scala index f6a8bd25cd6..8d7b79d5876 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtProjectResolver.scala +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtProjectResolver.scala @@ -12,6 +12,7 @@ import com.intellij.openapi.project.{Project, ProjectManager} import com.intellij.openapi.roots.DependencyScope import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.registry.RegistryManager +import com.intellij.util.io.DigestUtil import org.jetbrains.annotations.{NonNls, Nullable, TestOnly} import org.jetbrains.plugins.scala._ import org.jetbrains.plugins.scala.build._ @@ -23,6 +24,7 @@ import org.jetbrains.plugins.scala.project.external.{AndroidJdk, JdkByHome, JdkB import org.jetbrains.plugins.scala.util.ScalaNotificationGroups import org.jetbrains.sbt.SbtUtil._ import org.jetbrains.sbt.project.SbtProjectResolver._ +import org.jetbrains.sbt.project.SbtStructureCache.BuildDefinitionHash import org.jetbrains.sbt.project.data._ import org.jetbrains.sbt.project.module.SbtModuleType import org.jetbrains.sbt.project.settings._ @@ -30,11 +32,13 @@ import org.jetbrains.sbt.project.structure.SbtStructureDump.PrintProcessOutputOn import org.jetbrains.sbt.project.structure._ import org.jetbrains.sbt.resolvers.{SbtIvyResolver, SbtMavenResolver, SbtResolver} import org.jetbrains.sbt.structure.XmlSerializer._ -import org.jetbrains.sbt.structure.{BuildData, Configuration, ConfigurationData, DependencyData, DirectoryData, JavaData, ModuleDependencyData, ModuleIdentifier, ProjectData} +import org.jetbrains.sbt.structure.{BuildData, Configuration, ConfigurationData, DependencyData, DirectoryData, JavaData, ModuleDependencyData, ModuleIdentifier, ProjectData, StructureData} import org.jetbrains.sbt.{RichBoolean, Sbt, SbtBundle, SbtUtil, usingTempFile, structure => sbtStructure} import java.io.{File, FileNotFoundException} +import java.nio.charset.StandardCharsets import java.util.{Collections, Locale, UUID} +import scala.collection.mutable.ArrayBuffer import scala.collection.{MapView, mutable} import scala.concurrent.Await import scala.concurrent.duration.Duration @@ -135,15 +139,114 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti conversionResult.get // ok to throw here, that's the way ExternalSystem likes it } + private def buildDefinitionHash( + projectRoot: File, + sbtVersion: Version, + settings: SbtExecutionSettings + ): BuildDefinitionHash = { + val rootPath = projectRoot.toPath + val filesToDigest = new ArrayBuffer[String] + val extensions = List(".sbt", ".scala", ".properties") + + def addFile(f: File): Unit = + filesToDigest.addOne(rootPath.relativize(f.toPath).toFile.getPath.replace('\\', '/')) + + projectRoot.listFiles((_, f) => f.endsWith(".sbt")).foreach(addFile) + + def scanDir(dir: File): Unit = + if (dir.exists() && dir.isDirectory) { + dir.listFiles().foreach { f => + if (f.isDirectory && f.getName != "target") { + scanDir(f) + } else if (f.isFile && extensions.exists(f.getName.endsWith)) { + addFile(f) + } + } + } + + scanDir(new File(projectRoot, "project")) + + def byte(b: Boolean): Byte = if (b) 1 else 0 + + val digest = DigestUtil.sha1() + log.info(s"Hashing SBT version ${sbtVersion.presentation}") + digest.update(sbtVersion.presentation.getBytes(StandardCharsets.UTF_8)) + + val settingsStr = + settings.resolveClassifiers.seq("resolveClassifiers") ++ + settings.resolveSbtClassifiers.seq("resolveSbtClassifiers") ++ + settings.resolveJavadocs.seq("resolveJavadocs") ++ + settings.preferScala2.seq("preferScala2") + + log.info(s"Hashing SBT settings: ${settingsStr.mkString(",")}") + digest.update(byte(settings.resolveClassifiers)) + digest.update(byte(settings.resolveSbtClassifiers)) + digest.update(byte(settings.resolveJavadocs)) + digest.update(byte(settings.preferScala2)) + + // files must be hashed in deterministic order + filesToDigest.sortInPlace() + filesToDigest.foreach { path => + val resolved = rootPath.resolve(path) + log.info(s"Hashing file $path of size ${resolved.toFile.length()}") + digest.update(path.getBytes(StandardCharsets.UTF_8)) + DigestUtil.updateContentHash(digest, resolved) + } + val hash = DigestUtil.digestToHash(digest) + log.info(s"Build definition hash is $hash") + BuildDefinitionHash(hash) + } + + //TODO: PoC, assumes specific project structure (e.g. that someone didn't customize 'target' dir to sth else) + private def cleanSbtUpdateCache(projectRoot: File): Unit = { + FileUtil.delete(projectRoot / "target/update/update-cache") + FileUtil.delete(projectRoot / "project/target") + FileUtil.delete(projectRoot / "project/project/target") + } + private def dumpStructure(projectRoot: File, sbtLauncher: File, sbtVersion: Version, - settings:SbtExecutionSettings, + settings: SbtExecutionSettings, @Nullable project: Project )(implicit reporter: BuildReporter): Try[(Elem, BuildMessages)] = { val useShellImport = settings.useShellForImport && shellImportSupported(sbtVersion) && project != null val options = dumpOptions(settings) + def doDumpStructureCached(structureFile: File): Try[(Elem, BuildMessages)] = + SbtStructureCaches.get(settings) match { + case None => doDumpStructure(structureFile) + case Some(cache) => + log.info("sbt structure cache detected") + val key = buildDefinitionHash(projectRoot, sbtVersion, settings) + val localizer = new StructureLocalizer(projectRoot, settings.allowManualDependencyDownload) + + log.info(s"looking for cached structure under key ${key.hash}") + cache.getCachedStructureXml(key) match { + case Some(cachedXml) => + log.info("found cached structure file") + Try { + val unlocalizedStructure = XML.loadString(cachedXml).deserialize[StructureData].getRight + localizer.localize(unlocalizedStructure) + .map(localizedStructure => (localizedStructure.serialize, BuildMessages.empty)) + .getOrElse(doDumpStructure(structureFile).get) + } + + case None => + log.info("no cached structure file found") + cleanSbtUpdateCache(projectRoot) + val result = doDumpStructure(structureFile) + result.foreach { + case (elem, _) => + val localizedData = elem.deserialize[StructureData].getRight + val unlocalizedData = localizer.unlocalize(localizedData) + val unlocalizedElem = unlocalizedData.serialize + cache.cacheStructureXml(key, unlocalizedElem.toString) + } + result + } + } + def doDumpStructure(structureFile: File): Try[(Elem, BuildMessages)] = { val structureFilePath = normalizePath(structureFile) @@ -226,10 +329,10 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti Try((elem, BuildMessages.empty)) } else if (writeStructureFile) { log.warn(s"reused structure file created: $structureFilePath") - doDumpStructure(structureFilePath) + doDumpStructureCached(structureFilePath) } else { usingTempFile("sbt-structure", Some(".xml")) { structureFile => - doDumpStructure(structureFile) + doDumpStructureCached(structureFile) } } } @@ -260,16 +363,16 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti ) private def dumpOptions(settings: SbtExecutionSettings): Seq[String] = { - Seq("download") ++ + Seq("download") ++ settings.resolveClassifiers.seq("resolveClassifiers") ++ settings.resolveJavadocs.seq("resolveJavadocs") ++ settings.resolveSbtClassifiers.seq("resolveSbtClassifiers") } /** - * Create project preview without using sbt, since sbt import can fail and users would have to do a manual edit of the project. - * Also sbt boot makes the whole process way too slow. - */ + * Create project preview without using sbt, since sbt import can fail and users would have to do a manual edit of the project. + * Also sbt boot makes the whole process way too slow. + */ private def dummyProject(projectRoot: File, settings: SbtExecutionSettings, sbtVersion: String): Node[ESProjectData] = { // TODO add default scala sdk and sbt libs (newest versions or so) @@ -308,10 +411,10 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } /** - * This implementation is the same as in sbt.Project.normalizeModuleId to avoid inconsistencies in the import process. - * Normalize a String so that it is suitable for use as a dependency management module identifier. - * This is a best effort implementation, since valid characters are not documented or consistent. - */ + * This implementation is the same as in sbt.Project.normalizeModuleId to avoid inconsistencies in the import process. + * Normalize a String so that it is suitable for use as a dependency management module identifier. + * This is a best effort implementation, since valid characters are not documented or consistent. + */ private def normalizeModuleId(s: String) = s.toLowerCase(Locale.ENGLISH) .replaceAll("""\W+""", "-") @@ -361,9 +464,9 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } /** Choose a project jdk based on information from sbt settings and IDE. - * More specific settings from sbt are preferred over IDE settings, on the assumption that the sbt project definition - * is what is more likely to be under source control. - */ + * More specific settings from sbt are preferred over IDE settings, on the assumption that the sbt project definition + * is what is more likely to be under source control. + */ private def chooseJdk(project: sbtStructure.ProjectData, defaultJdk: Option[String]): Option[SdkReference] = { // TODO put some of this logic elsewhere in resolving process? val androidSdk = project.android.map(android => AndroidJdk(android.targetVersion)) @@ -377,7 +480,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti .orElse(default) } - private def createModuleDependencies(projectToModule: Map[ProjectData,ModuleNode]): Unit = { + private def createModuleDependencies(projectToModule: Map[ProjectData, ModuleNode]): Unit = { projectToModule.foreach { case (moduleProject, moduleNode) => moduleProject.dependencies.projects.foreach { dependencyId => val dependency = @@ -392,7 +495,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } } - private def createModules(projects: Seq[sbtStructure.ProjectData], libraryNodes: Seq[LibraryNode], moduleFilesDirectory: File): Map[ProjectData,ModuleNode] = { + private def createModules(projects: Seq[sbtStructure.ProjectData], libraryNodes: Seq[LibraryNode], moduleFilesDirectory: File): Map[ProjectData, ModuleNode] = { val unmanagedSourcesAndDocsLibrary = libraryNodes.map(_.data).find(_.getExternalName == Sbt.UnmanagedSourcesAndDocsName) val nameToProjects = projects.groupBy(_.name) @@ -428,7 +531,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti dependency.setScope(DependencyScope.COMPILE) moduleNode.add(dependency) } - (project,moduleNode) + (project, moduleNode) } val projectToModuleMap = projectToModule.toMap @@ -441,7 +544,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti val repositoryModules = data.repository.map(_.modules).getOrElse(Seq.empty) val (modulesWithoutBinaries, modulesWithBinaries) = repositoryModules.partition(_.binaries.isEmpty) val otherModuleIds = projects.flatMap(_.dependencies.modules.map(_.id)).toSet -- - repositoryModules.map(_.id).toSet + repositoryModules.map(_.id).toSet val libs = modulesWithBinaries.map(createResolvedLibrary) ++ otherModuleIds.map(createUnresolvedLibrary) @@ -461,15 +564,15 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti .orElse(java.flatMap(_.home).map(JdkByHome)) val data = SbtModuleExtData( - scalaVersion = scala.map(_.version), - scalacClasspath = scala.fold(Seq.empty[File])(_.allCompilerJars), + scalaVersion = scala.map(_.version), + scalacClasspath = scala.fold(Seq.empty[File])(_.allCompilerJars), scaladocExtraClasspath = scala.fold(Seq.empty[File])(_.extraJars), - scalacOptions = scala.fold(Seq.empty[String])(_.options), - sdk = sdk, - javacOptions = java.fold(Seq.empty[String])(_.options), - packagePrefix = packagePrefix, - basePackage = basePackages.headOption, // TODO Rename basePackages to basePackage in sbt-ide-settings? - compileOrder = CompileOrder.valueOf(compileOrder) + scalacOptions = scala.fold(Seq.empty[String])(_.options), + sdk = sdk, + javacOptions = java.fold(Seq.empty[String])(_.options), + packagePrefix = packagePrefix, + basePackage = basePackages.headOption, // TODO Rename basePackages to basePackage in sbt-ide-settings? + compileOrder = CompileOrder.valueOf(compileOrder) ) new ModuleExtNode(data) } @@ -488,7 +591,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } private def createCommandData(project: sbtStructure.ProjectData) = { - project.commands.map { c => + project.commands.map { c => new SbtCommandNode(SbtCommandData(c.name, c.help)) } } @@ -588,13 +691,14 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti return extractedExcludes.distinct val managedDirectories = project.configurations - .flatMap(configuration => configuration.sources ++ configuration.resources) - .filter(_.managed) - .map(_.file) + .flatMap(configuration => configuration.sources ++ configuration.resources) + .filter(_.managed) + .map(_.file) val defaultNames = Set("main", "test") val relevantDirectories = managedDirectories.filter(file => file.exists || !defaultNames.contains(file.getName)) + def isRelevant(f: File): Boolean = !relevantDirectories.forall(_.isOutsideOf(f)) if (isRelevant(project.target)) { @@ -618,7 +722,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } val buildId = buildBaseProject - .map(_.name + Sbt.BuildModuleSuffix) + .map(_.name + Sbt.BuildModuleSuffix) .getOrElse(build.uri.toString) val buildBaseDir = buildBaseProject @@ -691,10 +795,10 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti private def validRootPathsIn(project: sbtStructure.ProjectData, scope: String) (selector: sbtStructure.ConfigurationData => Seq[sbtStructure.DirectoryData]): Seq[sbtStructure.DirectoryData] = { project.configurations - .find(_.id == scope) - .map(selector) - .getOrElse(Seq.empty) - .filterNot(_.file.isOutsideOf(project.base)) + .find(_.id == scope) + .map(selector) + .getOrElse(Seq.empty) + .filterNot(_.file.isOutsideOf(project.base)) } protected def createLibraryDependencies(dependencies: Seq[sbtStructure.ModuleDependencyData]) @@ -711,7 +815,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } private def createUnmanagedDependencies(dependencies: Seq[sbtStructure.JarDependencyData]) - (moduleData: ModuleData): Seq[LibraryDependencyNode] = { + (moduleData: ModuleData): Seq[LibraryDependencyNode] = { dependencies.groupBy(it => scopeFor(it.configurations)).toSeq.map { case (scope, dependency) => val name = scope match { case DependencyScope.COMPILE => Sbt.UnmanagedLibraryName @@ -759,7 +863,7 @@ class SbtProjectResolver extends ExternalSystemProjectResolver[SbtExecutionSetti } override def cancelTask(taskId: ExternalSystemTaskId, listener: ExternalSystemTaskNotificationListener): Boolean = - //noinspection UnitInMap + //noinspection UnitInMap activeProcessDumper .map(_.cancel()) .isDefined diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtStructureCaches.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtStructureCaches.scala new file mode 100644 index 00000000000..545357b777c --- /dev/null +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/project/SbtStructureCaches.scala @@ -0,0 +1,152 @@ +package org.jetbrains.sbt.project + +import com.intellij.openapi.diagnostic.Logger +import org.apache.commons.io.FileUtils +import org.jetbrains.sbt.project.SbtStructureCache.BuildDefinitionHash +import org.jetbrains.sbt.project.settings.SbtExecutionSettings +import org.tukaani.xz.{LZMA2Options, XZInputStream, XZOutputStream} + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, File, FileReader, IOException} +import java.net.http.HttpRequest.BodyPublishers +import java.net.http.HttpResponse.BodyHandlers +import java.net.http.{HttpClient, HttpRequest} +import java.net.{Authenticator, PasswordAuthentication, URI} +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.Properties +import scala.annotation.tailrec +import scala.concurrent.TimeoutException +import scala.util.Using +import scala.util.chaining._ + +object SbtStructureCaches { + //TODO: cache the caches per project or sth? + def get(settings: SbtExecutionSettings): Option[SbtStructureCache] = { + val localCache = settings.localStructureCacheFile.map(new LocalStructureCache(_)) + val remoteCache = settings.remoteStructureCacheUri.map { uri => + new RemoteStructureCache(uri, settings.remoteStructureCacheCredentialsFile) + } + val cacheChain = localCache.toList ++ remoteCache.toList + if (cacheChain.nonEmpty) Some(new CompositeStructureCache(cacheChain)) + else None + } +} + +class LocalStructureCache(cacheDir: File) extends SbtStructureCache { + private val log = Logger.getInstance(getClass) + + private def file(key: BuildDefinitionHash): File = + new File(cacheDir, s"$key.xml") + + def getCachedStructureXml(key: BuildDefinitionHash): Option[String] = + Option(file(key)) + .filter(f => f.exists() && f.isFile) + .map { f => + log.info(s"reading $key from local cache file") + FileUtils.readFileToString(f, StandardCharsets.UTF_8) + } + + def cacheStructureXml(key: BuildDefinitionHash, data: String): Unit = { + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + log.info(s"writing $key to local cache file") + FileUtils.write(file(key), data, StandardCharsets.UTF_8) + } +} + +class RemoteStructureCache( + repositoryUri: URI, + credentialsFile: Option[File], +) extends SbtStructureCache { + private val log = Logger.getInstance(getClass) + + private lazy val authenticator = credentialsFile.flatMap { file => + try { + val (user, password) = { + val props = new Properties + Using.resource(new FileReader(file))(props.load) + val u = props.getProperty("user") + val p = props.getProperty("password") + require(u != null, "username not found in credentials file") + require(p != null, "password not found in credentials file") + (u, p.toCharArray) + } + Some(new Authenticator { + override def getPasswordAuthentication: PasswordAuthentication = + new PasswordAuthentication(user, password) + }) + } catch { + case e@(_: IOException | _: IllegalArgumentException) => + log.warn(s"failed to read remote cache credentials file $file", e) + None + } + } + + private lazy val httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .tap(b => authenticator.fold(b)(b.authenticator)) + .build() + + private def fileRequest(key: BuildDefinitionHash): HttpRequest.Builder = + HttpRequest + .newBuilder(repositoryUri.resolve(s"$key.xml.xz")) + .timeout(Duration.ofSeconds(30)) + + def getCachedStructureXml(key: BuildDefinitionHash): Option[String] = try { + val request = fileRequest(key).GET().build() + val bodySubscriber = BodyHandlers.ofByteArray() + log.info(s"GET ${request.uri()}") + val resp = httpClient.send(request, bodySubscriber) + + resp.statusCode() match { + case 200 => + val is = new XZInputStream(new ByteArrayInputStream(resp.body())) + Some(new String(is.readAllBytes(), StandardCharsets.UTF_8)) + case 404 => None + case other => + log.warn(s"failed to read $key from remote cache, received HTTP status $other") + None + } + } catch { + case e@(_: IOException | _: TimeoutException) => + log.warn(s"failed to read $key from remote cache", e) + None + } + + def cacheStructureXml(key: BuildDefinitionHash, data: String): Unit = try { + val baos = new ByteArrayOutputStream + Using.resource(new XZOutputStream(baos, new LZMA2Options))(_.write(data.getBytes(StandardCharsets.UTF_8))) + val request = fileRequest(key).PUT(BodyPublishers.ofByteArray(baos.toByteArray)).build() + log.info(s"PUT ${request.uri()}") + val resp = httpClient.send(request, BodyHandlers.discarding()) + if (resp.statusCode() >= 300) { + log.warn(s"failed to save $key in remote cache, received HTTP status ${resp.statusCode()}") + } + } catch { + case e@(_: IOException | _: TimeoutException) => + log.warn(s"failed to save $key in remote cache", e) + } +} + +class CompositeStructureCache(caches: List[SbtStructureCache]) extends SbtStructureCache { + def getCachedStructureXml(key: BuildDefinitionHash): Option[String] = { + @tailrec def loop(tried: List[SbtStructureCache], notTried: List[SbtStructureCache]): Option[String] = + notTried match { + case Nil => None + case head :: tail => + head.getCachedStructureXml(key) match { + case Some(value) => + tried.foreach(_.cacheStructureXml(key, value)) + Some(value) + case None => + loop(head :: tried, tail) + } + } + + loop(Nil, caches) + } + + def cacheStructureXml(key: BuildDefinitionHash, data: String): Unit = + caches.foreach(_.cacheStructureXml(key, data)) +} diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/project/StructureLocalizer.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/project/StructureLocalizer.scala new file mode 100644 index 00000000000..08671bf03d3 --- /dev/null +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/project/StructureLocalizer.scala @@ -0,0 +1,250 @@ +package org.jetbrains.sbt.project + +import com.intellij.ide.customize.transferSettings.db.WindowsEnvVariables +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.util.SystemInfo +import com.intellij.util.SystemProperties +import org.jetbrains.sbt.structure._ + +import java.io.{File, IOException} +import java.net.URI +import java.net.http.HttpResponse.BodyHandlers +import java.net.http.{HttpClient, HttpRequest} +import scala.concurrent.TimeoutException +import scala.util.control.NoStackTrace + +object StructureLocalizer { + private final val AnyResolverCache = "$ANY_RESOLVER_CACHE$" + private final val CoursierCache = "$COURSIER_CACHE$" + private final val ProjectDir = "$PROJECT_DIR$" + private final val UserHome = "$USER_HOME$" + + implicit class stringOps(private val str: String) extends AnyVal { + def ensureSuffix(suffix: String): String = + if (str.endsWith(suffix)) str else str + suffix + + def ensurePrefix(prefix: String): String = + if (str.startsWith(prefix)) str else prefix + str + } + + private def unix(path: String): String = + path.replace('\\', '/') + + private lazy val userHome: String = + unix(SystemProperties.getUserHome).ensureSuffix("/") + + private lazy val coursierCachePath: String = + if (SystemInfo.isWindows) + unix(WindowsEnvVariables.INSTANCE.getLocalApplicationData).ensureSuffix("/") + "Coursier/cache/v1/" + else if (SystemInfo.isMac) + userHome + "Library/Caches/Coursier/v1/" + else + userHome + ".cache/coursier/v1/" + + private case class LocalFileNotFound(file: File) extends Exception with NoStackTrace + + private case class ResolverInfo(uri: URI, cachePath: String) + +} +class StructureLocalizer(projectDir: File, allowManualDependencyDownload: Boolean) { + + import StructureLocalizer._ + + private val log = Logger.getInstance(classOf[StructureLocalizer]) + + private val projectPath = + unix(projectDir.getAbsolutePath).ensureSuffix("/") + + private lazy val httpClient = HttpClient.newHttpClient() + + def localize(sd: StructureData): Option[StructureData] = + try Option(transform(sd)(localize)) catch { + case LocalFileNotFound(file) => + log.warn(s"file $file not found, forcing full sbt refresh") + None + } + + def unlocalize(sd: StructureData): StructureData = + transform(sd)(unlocalize) + + private def transform(data: StructureData)(f: (File, List[ResolverInfo]) => File): StructureData = { + val allLocalCachePaths = data.projects.toList.flatMap(resolverInfos) + StructureData( + data.sbtVersion, + builds = data.builds.map(bd => BuildData( + transform(bd.uri)(f(_, Nil)), + bd.imports, + bd.classes.map(f(_, Nil)), + bd.docs.map(f(_, Nil)), + bd.sources.map(f(_, Nil)), + )), + projects = data.projects.map(transform(_)(f)), + data.repository.map(rd => RepositoryData(rd.modules.map(md => ModuleData( + md.id, + md.binaries.map(f(_, allLocalCachePaths)), + md.docs.map(f(_, allLocalCachePaths)), + md.sources.map(f(_, allLocalCachePaths)) + )))), + data.localCachePath.map(f(_, Nil)), + ) + } + + private def transform(pd: ProjectData)(f: (File, List[ResolverInfo]) => File): ProjectData = { + val localCachePaths = resolverInfos(pd) + ProjectData( + pd.id, + transform(pd.buildURI)(f(_, localCachePaths)), + pd.name, + pd.organization, + pd.version, + f(pd.base, localCachePaths), + pd.packagePrefix, + pd.basePackages, + f(pd.target, localCachePaths), + pd.configurations.map(cd => ConfigurationData( + cd.id, + cd.sources.map(dd => DirectoryData(f(dd.file, localCachePaths), dd.managed)), + cd.resources.map(dd => DirectoryData(f(dd.file, localCachePaths), dd.managed)), + cd.excludes.map(f(_, localCachePaths)), + f(cd.classes, localCachePaths), + )), + pd.java.map(jd => JavaData(jd.home.map(f(_, localCachePaths)), jd.options)), + pd.scala.map(sd => ScalaData( + sd.organization, + sd.version, + sd.libraryJars.map(f(_, localCachePaths)), + sd.compilerJars.map(f(_, localCachePaths)), + sd.extraJars.map(f(_, localCachePaths)), + sd.options + )), + pd.compileOrder, + pd.android.map(ad => AndroidData( + ad.targetVersion, + ad.manifest, + f(ad.apk, localCachePaths), + f(ad.res, localCachePaths), + f(ad.assets, localCachePaths), + f(ad.gen, localCachePaths), + f(ad.libs, localCachePaths), + ad.isLibrary, + ad.proguardConfig, + ad.apklibs.map(al => ApkLib( + al.name, + f(al.base, localCachePaths), + f(al.manifest, localCachePaths), + f(al.sources, localCachePaths), + f(al.resources, localCachePaths), + f(al.libs, localCachePaths), + f(al.gen, localCachePaths), + )), + ad.aars.map(ar => Aar(ar.name, transform(ar.project)(f))) + )), + on(pd.dependencies)(dd => DependencyData( + dd.projects.map(pdd => ProjectDependencyData( + pdd.project, pdd.buildURI.map(transform(_)(f(_, localCachePaths))), pdd.configuration, + )), + dd.modules, + dd.jars.map(jdd => JarDependencyData(f(jdd.file, localCachePaths), jdd.configurations)), + )), + pd.resolvers, + pd.play2.map(p2d => Play2Data( + p2d.playVersion, + p2d.templatesImports, + p2d.routesImports, + p2d.confDirectory.map(f(_, localCachePaths)), + f(p2d.sourceDirectory, localCachePaths), + )), + pd.settings, + pd.tasks, + pd.commands, + ) + } + + private def on[T](v: T)(f: T => T): T = f(v) + + private def resolverInfos(proj: ProjectData): List[ResolverInfo] = + proj.resolvers + .filter(rd => rd.root.matches("http(s)?://.*")) + .toList.sortBy(_.name) + .map { rd => + val uri = new URI(rd.root) + //TODO: handle URIs with escaped chars and port number + val cache = s"$coursierCachePath${uri.getScheme}/${uri.getHost}${uri.getPath}".ensureSuffix("/") + ResolverInfo(uri, cache) + } + + private def unlocalize(f: File, resolvers: List[ResolverInfo]): File = { + var path = unix(f.getAbsolutePath) + path = resolvers + .find(r => path.startsWith(r.cachePath)) + .fold(path)(ri => s"/$AnyResolverCache/${path.stripPrefix(ri.cachePath)}") + path = + if (path.startsWith(coursierCachePath)) s"/$CoursierCache/${path.stripPrefix(coursierCachePath)}" + else if (path.startsWith(projectPath)) s"/$ProjectDir/${path.stripPrefix(projectPath)}" + else if (path.startsWith(userHome)) s"/$UserHome/${path.stripPrefix(userHome)}" + else path + new File(path) + } + + private def download(from: URI, to: File): Option[File] = try { + log.info(s"GET $from") + to.getParentFile.mkdirs() + val req = HttpRequest.newBuilder(from).GET.build + val bodyHandler = BodyHandlers.ofFile(to.toPath) + val resp = httpClient.send(req, bodyHandler) + resp.statusCode match { + case 200 => Some(resp.body.toFile) + case 404 => None + case other => + log.warn(s"received http status $other trying to download $from") + None + } + } catch { + case e@(_: IOException | _: TimeoutException) => + log.warn(s"failure trying to download $from", e) + None + } + + private def download(depPath: String, resolver: List[ResolverInfo]): Option[File] = + if (!allowManualDependencyDownload) None + else resolver.iterator + .flatMap(ri => download(ri.uri.resolve(depPath), new File(ri.cachePath + depPath))) + .nextOption() + + private def localize(f: File, resolvers: List[ResolverInfo]): File = { + // The 'C:' that we're stripping appears when we create an Unix-style absolute path File on Windows + // and call '.getCanonicalPath` on it, e.g. `new File("/foo").getCanonicalPath` yields `C:\foo` + // We can't avoid that because `.getCanonicalPath` is used by XML serializer & deserializer of + // `StructureData` in `sbt-structure` (unless we rewrite that serialization entirely). + val workingDir = new File(".").getCanonicalPath + var path = f.getPath.stripPrefix(workingDir) + path = path.stripPrefix("/").stripPrefix("C:") + path = unix(path).stripPrefix("/") + if (path.startsWith(AnyResolverCache)) { + val pureDependencyPath = path.stripPrefix(AnyResolverCache).stripPrefix("/") + resolvers + .map(ri => new File(ri.cachePath + pureDependencyPath)) + .find(_.exists()) + .orElse(download(pureDependencyPath, resolvers)) + .getOrElse { + log.warn(s"could not resolve dependency $pureDependencyPath") + throw LocalFileNotFound(new File(path)) + } + } else { + path = + if (path.startsWith(CoursierCache)) coursierCachePath + path.stripPrefix(CoursierCache).stripPrefix("/") + else if (path.startsWith(ProjectDir)) projectPath + path.stripPrefix(ProjectDir).stripPrefix("/") + else if (path.startsWith(UserHome)) userHome + path.stripPrefix(UserHome).stripPrefix("/") + else path + val localFile = new File(path) + if (localFile.getName.endsWith(".jar") && !localFile.exists()) { + throw LocalFileNotFound(localFile) + } + localFile + } + } + + private def transform(uri: URI)(f: File => File): URI = + if (uri.getScheme == "file") f(new File(uri.getPath)).toURI + else uri +} diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/project/settings/SbtExecutionSettings.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/project/settings/SbtExecutionSettings.scala index 180efa392c2..5db86c3a065 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/project/settings/SbtExecutionSettings.scala +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/project/settings/SbtExecutionSettings.scala @@ -4,6 +4,7 @@ package project.settings import com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings import java.io.File +import java.net.URI class SbtExecutionSettings(val realProjectPath: String, val vmExecutable: File, @@ -12,6 +13,9 @@ class SbtExecutionSettings(val realProjectPath: String, val environment: Map[String,String], val customLauncher: Option[File], val customSbtStructureFile: Option[File], + val localStructureCacheFile: Option[File], + val remoteStructureCacheUri: Option[URI], + val remoteStructureCacheCredentialsFile: Option[File], val jdk: Option[String], val resolveClassifiers: Boolean, val resolveJavadocs: Boolean, @@ -20,4 +24,5 @@ class SbtExecutionSettings(val realProjectPath: String, val shellDebugMode: Boolean, val allowSbtVersionOverride: Boolean, val preferScala2: Boolean, + val allowManualDependencyDownload: Boolean, ) extends ExternalSystemExecutionSettings diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsControl.scala b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsControl.scala index 304b255f434..374173d1dec 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsControl.scala +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsControl.scala @@ -12,7 +12,11 @@ class SbtSettingsControl(settings: SbtSettings) extends ExternalSystemSettingsCo pane.getMaximumHeapSize == settings.maximumHeapSize && pane.getVmParameters == settings.vmParameters && pane.isCustomVM == settings.customVMEnabled && - pane.getCustomVMPath == settings.customVMPath + pane.getCustomVMPath == settings.customVMPath && + pane.getLocalStructureCachePath == settings.localStructureCachePath && + pane.getRemoteStructureCacheUri == settings.remoteStructureCacheUri && + pane.getRemoteStructureCacheCredentialsPath == settings.remoteStructureCacheCredentialsPath && + pane.isAllowManualDependencyDownload == settings.allowManualDependencyDownload } override def showUi(show: Boolean): Unit = @@ -30,6 +34,10 @@ class SbtSettingsControl(settings: SbtSettings) extends ExternalSystemSettingsCo settings.vmParameters = pane.getVmParameters settings.customVMEnabled = pane.isCustomVM settings.customVMPath = pane.getCustomVMPath + settings.localStructureCachePath = pane.getLocalStructureCachePath + settings.remoteStructureCacheUri = pane.getRemoteStructureCacheUri + settings.remoteStructureCacheCredentialsPath = pane.getRemoteStructureCacheCredentialsPath + settings.allowManualDependencyDownload = pane.isAllowManualDependencyDownload } override def reset(): Unit = { @@ -38,6 +46,10 @@ class SbtSettingsControl(settings: SbtSettings) extends ExternalSystemSettingsCo pane.setMaximumHeapSize(settings.maximumHeapSize) pane.setMyVmParameters(settings.vmParameters) pane.setCustomVMPath(settings.customVMPath, settings.customVMEnabled) + pane.setLocalStructureCachePath(settings.localStructureCachePath) + pane.setRemoteStructureCacheUri(settings.remoteStructureCacheUri) + pane.setRemoteStructureCacheCredentialsPath(settings.remoteStructureCacheCredentialsPath) + pane.setAllowManualDependencyDownload(settings.allowManualDependencyDownload) } override def validate(settings: SbtSettings) = true diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.form b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.form index b12bf4552af..afff69daa74 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.form +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.form @@ -1,18 +1,13 @@
- + - + - - - - - @@ -135,6 +130,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.java b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.java index 829e09feaed..73a50a5467e 100644 --- a/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.java +++ b/sbt/sbt-impl/src/org/jetbrains/sbt/settings/SbtSettingsPane.java @@ -32,6 +32,10 @@ public class SbtSettingsPane { private RawCommandLineEditor myVmParameters; private JPanel myContentPanel; private JrePathEditor myJrePathEditor; + private TextFieldWithBrowseButton myLocalStructureCachePath; + private JTextField myRemoteStructureCacheUri; + private TextFieldWithBrowseButton myRemoteStructureCacheCredentialsPath; + private JCheckBox myAllowManualDependencyDownload; private final Project myProject; @@ -51,6 +55,20 @@ public SbtSettingsPane(Project project) { SbtBundle.message("sbt.settings.choose.sbt.launch.jar"), project, FileChooserDescriptorFactory.createSingleLocalFileDescriptor()); + + myLocalStructureCachePath.addBrowseFolderListener( + SbtBundle.message("sbt.settings.choose.cache.directory"), + SbtBundle.message("sbt.settings.choose.local.structure.cache.directory"), + project, + FileChooserDescriptorFactory.createSingleFolderDescriptor() + ); + + myRemoteStructureCacheCredentialsPath.addBrowseFolderListener( + SbtBundle.message("sbt.settings.choose.remote.cache.credentials"), + SbtBundle.message("sbt.settings.choose.remote.structure.cache.credentials.file"), + project, + FileChooserDescriptorFactory.createSingleLocalFileDescriptor() + ); } public void createUIComponents() { @@ -125,6 +143,38 @@ public void setMyVmParameters(String value) { myVmParameters.setText(value); } + public String getLocalStructureCachePath() { + return myLocalStructureCachePath.getText(); + } + + public void setLocalStructureCachePath(String path) { + myLocalStructureCachePath.setText(path); + } + + public String getRemoteStructureCacheUri() { + return myRemoteStructureCacheUri.getText(); + } + + public void setRemoteStructureCacheUri(String path) { + myRemoteStructureCacheUri.setText(path); + } + + public String getRemoteStructureCacheCredentialsPath() { + return myRemoteStructureCacheCredentialsPath.getText(); + } + + public void setRemoteStructureCacheCredentialsPath(String path) { + myRemoteStructureCacheCredentialsPath.setText(path); + } + + public boolean isAllowManualDependencyDownload() { + return myAllowManualDependencyDownload.isSelected(); + } + + public void setAllowManualDependencyDownload(boolean value) { + myAllowManualDependencyDownload.setSelected(value); + } + /** * Method generated by IntelliJ IDEA GUI Designer * >>> IMPORTANT!! <<< @@ -135,9 +185,7 @@ public void setMyVmParameters(String value) { private void $$$setupUI$$$() { createUIComponents(); myContentPanel = new JPanel(); - myContentPanel.setLayout(new GridLayoutManager(6, 2, new Insets(0, 0, 0, 0), -1, -1)); - final Spacer spacer1 = new Spacer(); - myContentPanel.add(spacer1, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + myContentPanel.setLayout(new GridLayoutManager(9, 2, new Insets(0, 0, 0, 0), -1, -1)); final TitledSeparator titledSeparator1 = new TitledSeparator(); titledSeparator1.setText(this.$$$getMessageFromBundle$$$("messages/SbtBundle", "sbt.settings.sbtLauncher")); myContentPanel.add(titledSeparator1, new GridConstraints(3, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); @@ -175,8 +223,34 @@ public void setMyVmParameters(String value) { myVmParameters.setDialogCaption(this.$$$getMessageFromBundle$$$("messages/SbtBundle", "sbt.settings.vmParams")); myVmParameters.setEnabled(true); panel3.add(myVmParameters, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, new Dimension(250, -1), new Dimension(250, -1), null, 0, false)); + final Spacer spacer1 = new Spacer(); + myContentPanel.add(spacer1, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + final TitledSeparator titledSeparator3 = new TitledSeparator(); + titledSeparator3.setText("Structure cache"); + myContentPanel.add(titledSeparator3, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); final Spacer spacer2 = new Spacer(); - myContentPanel.add(spacer2, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false)); + myContentPanel.add(spacer2, new GridConstraints(8, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + final JPanel panel4 = new JPanel(); + panel4.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); + myContentPanel.add(panel4, new GridConstraints(6, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 1, false)); + final JLabel label3 = new JLabel(); + label3.setText("Local cache directory"); + panel4.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + myLocalStructureCachePath = new TextFieldWithBrowseButton(); + panel4.add(myLocalStructureCachePath, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(250, -1), null, null, 0, false)); + final JLabel label4 = new JLabel(); + label4.setText("Remote cache URI"); + panel4.add(label4, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + myRemoteStructureCacheUri = new JTextField(); + panel4.add(myRemoteStructureCacheUri, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(150, -1), null, 0, false)); + final JLabel label5 = new JLabel(); + label5.setText("Remote cache credentials file"); + panel4.add(label5, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + myRemoteStructureCacheCredentialsPath = new TextFieldWithBrowseButton(); + panel4.add(myRemoteStructureCacheCredentialsPath, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, new Dimension(250, -1), null, null, 0, false)); + myAllowManualDependencyDownload = new JCheckBox(); + myAllowManualDependencyDownload.setText("Allow IDEA to download dependencies on behalf of sbt"); + myContentPanel.add(myAllowManualDependencyDownload, new GridConstraints(7, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 1, false)); label1.setLabelFor(myMaximumHeapSize); ButtonGroup buttonGroup; buttonGroup = new ButtonGroup(); diff --git a/sbt/sbt-impl/test/org/jetbrains/sbt/shell/MaxJvmHeapParameterTest.scala b/sbt/sbt-impl/test/org/jetbrains/sbt/shell/MaxJvmHeapParameterTest.scala index f642ec3cb5b..c583d6f3a0e 100644 --- a/sbt/sbt-impl/test/org/jetbrains/sbt/shell/MaxJvmHeapParameterTest.scala +++ b/sbt/sbt-impl/test/org/jetbrains/sbt/shell/MaxJvmHeapParameterTest.scala @@ -8,6 +8,7 @@ import org.jetbrains.sbt.project.settings.SbtExecutionSettings import java.io.File class MaxJvmHeapParameterTest extends TestCase { + import org.junit.Assert._ val hiddenDefaultSize = JvmMemorySize.Megabytes(1500) @@ -18,12 +19,12 @@ class MaxJvmHeapParameterTest extends TestCase { val workingDir = FileUtil.createTempDirectory("maxHeapJvmParamTest", getName, true) if (jvmOpts.nonEmpty) { - val jvmOptsFile = new File(workingDir,".jvmopts") + val jvmOptsFile = new File(workingDir, ".jvmopts") FileUtil.writeToFile(jvmOptsFile, jvmOpts.mkString("\n")) } - val settings = new SbtExecutionSettings(null, null, userOpts, hiddenDefaultSize, null, null, null, null, - false, false, false, false, false ,false, true) + val settings = new SbtExecutionSettings(null, null, userOpts, hiddenDefaultSize, null, null, null, None, None, None, null, + false, false, false, false, false, false, true, false) SbtProcessManager.buildVMParameters(settings, workingDir) } @@ -37,14 +38,14 @@ class MaxJvmHeapParameterTest extends TestCase { def testUserSettingsSmallerThanHiddenDefault(): Unit = { assertEquals( - Seq(superShellDisabled,"-Xmx4g", "-Xms4g", "-Xmx1g"), + Seq(superShellDisabled, "-Xmx4g", "-Xms4g", "-Xmx1g"), buildParamSeq("-Xmx1g")("-Xmx4g", "-Xms4g") ) } def testUserSettingsGreaterThanHiddenDefault(): Unit = { assertEquals( - Seq(superShellDisabled,"-Xmx4g", "-Xms4g", "-Xmx2g"), + Seq(superShellDisabled, "-Xmx4g", "-Xms4g", "-Xmx2g"), buildParamSeq("-Xmx2g")("-Xmx4g", "-Xms4g") ) } diff --git a/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/formatting/settings/ImportsPanel.java b/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/formatting/settings/ImportsPanel.java index 6ba97126430..644198bcbaf 100644 --- a/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/formatting/settings/ImportsPanel.java +++ b/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/formatting/settings/ImportsPanel.java @@ -272,7 +272,7 @@ private static void setValue(final JRadioButton rb, final boolean value) { final JPanel panel3 = new JPanel(); panel3.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); panel2.add(panel3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); - panel3.setBorder(IdeBorderFactory.PlainSmallWithIndent.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.classes.to.use.only.with.prefix"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); + panel3.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.classes.to.use.only.with.prefix"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); myImportsWithPrefixPanel = new JPanel(); myImportsWithPrefixPanel.setLayout(new BorderLayout(0, 0)); panel3.add(myImportsWithPrefixPanel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); @@ -282,14 +282,14 @@ private static void setValue(final JRadioButton rb, final boolean value) { final JPanel panel5 = new JPanel(); panel5.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); panel4.add(panel5, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); - panel5.setBorder(IdeBorderFactory.PlainSmallWithIndent.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.import.layout"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); + panel5.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.import.layout"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); importLayoutPanel = new JPanel(); importLayoutPanel.setLayout(new BorderLayout(0, 0)); panel5.add(importLayoutPanel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); final JPanel panel6 = new JPanel(); panel6.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); panel4.add(panel6, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); - panel6.setBorder(IdeBorderFactory.PlainSmallWithIndent.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.imports.always.marked.as.used"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); + panel6.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), this.$$$getMessageFromBundle$$$("messages/ScalaBundle", "imports.panel.imports.always.marked.as.used"), TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); myAlwaysUsedImportsPanel = new JPanel(); myAlwaysUsedImportsPanel.setLayout(new BorderLayout(0, 0)); panel6.add(myAlwaysUsedImportsPanel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));