/sbt-idea-plugin

Develop IntelliJ plugins with Scala and SBT

Primary LanguageScalaApache License 2.0Apache-2.0

sbt-idea-plugin

Version Build Status JetBrains team project Discord Gitter

SBT plugin that makes development of IntelliJ Platform plugins in Scala easier by providing features such as:

  • Downloading and attaching IntelliJ Platform binaries
  • Setting up the environment for running tests
  • Flexible way to define plugin artifact structure
  • Publishing the plugin to JetBrains plugin repository

For a comprehensive usage example see Scala plugin or HOCON plugin build definition.

A complete list of public IJ plugins implemented in Scala/SBT can be found on IntelliJ Platform Explorer

Note that some features of this plugin may be used independently, i.e. if you only want to print project structure or package artifacts you can depend on:

"org.jetbrains" % "sbt-declarative-visualizer" % "LATEST_VERSION" or

"org.jetbrains" % "sbt-declarative-packaging" % "LATEST_VERSION"

Please see the Known Issues section if you come across a problem, and feel free file a bug on the Issues page of this repo if you find one.

Quickstart: IJ Plugin Template Project

To quickly create a Scala based IJ Plugin we provide a template project. Create your own repo on GitHub from the JetBrains / sbt-idea-example template by clicking the green Use this template button. Clone the sources and open the build.sbt via File | Open menu in IDEA by choosing Open as a project.

Manual Installation (adding to an already existing sbt build)

From version 1.0.0, this plugin is published for sbt 0.13 and 1.0

  • Insert into project/plugins.sbt:
addSbtPlugin("org.jetbrains" % "sbt-idea-plugin" % "LATEST_VERSION")
  • Enable the plugin for your desired projects (your main plugin project and all its dependencies)

  • Run SBT and the plugin will automatically download and attach IntelliJ Platform dependencies.

  • Start coding

SBT Related Settings and Tasks

IntelliJ Platform and Plugin

intellijPluginName in ThisBuild :: SettingKey[String]

Default: name.in(LocalRootProject).value

Name of your plugin. Better set this beforehand since several other settings such as IntelliJ Platform directories and artifact names depend on it. Please see name troubleshooting for more info.

intellijBuild in ThisBuild :: SettingKey[String]

Default: LATEST-EAP-SNAPSHOT

Selected IDE's build number. Binaries and sources of this build will be downloaded from the repository and used in compilation and testing. You can find build number of your IntelliJ product in Help -> About dialog. However, it might be incomplete, so it is strongly recommended to verify it against available releases and available snapshots.

Note: minimum supported major IDEA version: 211.x (~2021.1.x)

intellijPlatform in ThisBuild :: SettingKey[IntelliJPlatform]

Default: IntelliJPlatform.IdeaCommunity

Edition of IntelliJ IDE to use in project. Currently available options are:

  • IdeaCommunity
  • IdeaUltimate
  • PyCharmCommunity
  • PyCharmProfessional
  • CLion
  • MPS

intellijPlugins :: SettingKey[IdeaPlugin]

Default: Seq.empty

IntelliJ plugins to depend on. Bundled(internal) plugins are specified by their plugin ID. Plugins from repo can be specified by the plugin's id, optional version and update channel. Plugins will be checked for compatibility against the intellijBuild you specified and updated to the latest version unless some specific version is given explicitly. Inter-plugin dependencies are also transitively resolved(e.g. depending on the Scala plugin will automatically attach Java and other plugin dependencies)

Plugin IDs can be either searched by plugin name with the help of searchPluginId task or manually

You can tune plugin resolving on individual plugin level by specifying several options to toPlugin method:

  • transitive - use transitive plugin resolution(default: true)
  • optionalDeps - resolve optional plugin dependencies(default: true)
  • excludedIds - blacklist certain plugins from transitive resolution(default: Set.empty)

❗ Please note that Java support in IJ is implemented by a plugin: com.intellij.java

❗ Please remember that you must declare plugin dependencies in plugin.xml or your plugin may fail to load.

// use properties plugin bundled with IDEA
intellijPlugins += "com.intellij.properties".toPlugin
// use Scala plugin as a dependency
intellijPlugins += "org.intellij.scala".toPlugin
// use Scala plugin version 2019.2.1
intellijPlugins += "org.intellij.scala:2019.2.1".toPlugin
// use latest nightly build from the repo
intellijPlugins += "org.intellij.scala::Nightly".toPlugin
// use specific version from Eap update channel
intellijPlugins += "org.intellij.scala:2019.3.2:Eap".toPlugin
// add JavaScript plugin but without its Grazie plugin dependency
intellijPlugins += "JavaScript".toPlugin(excludedIds = Set("tanvd.grazi"))

searchPluginId :: Map[String, (String, Boolean)]

Usage: searchPluginId [--nobundled|--noremote] <plugin name regexp>

Searches and prints plugins across locally installed IJ sdk and plugin marketplace. Use provided flags to limit search scope to only bundled or marketplace plugins.

> searchPluginId Prop
[info] bundled          - Properties[com.intellij.properties]
[info] bundled          - Resource Bundle Editor[com.intellij.properties.bundle.editor]

jbrInfo :: Option[JbrInfo]

Default: AutoJbr()

JetBrains Java runtime version to use when running the IDE with the plugin. By default JBR version is extracted from IDE installation metadata. Only jbr 11 is supported. Available versions can be found on jbr bintray. To disable, set to NoJbr

patchPluginXml :: SettingKey[pluginXmlOptions]

Default: pluginXmlOptions.DISABLED

Define some plugin.xml fields to be patched when building the artifact. Only the file in target folder is patched, original sources are left intact. Available options are:

patchPluginXml := pluginXmlOptions { xml =>
  xml.version           = version.value
  xml.pluginDescription = "My cool IDEA plugin"
  xml.changeNotes       = sys.env("CHANGE_LOG_FROM_CI")
  xml.sinceBuild        = (intellijBuild in ThisBuild).value
  xml.untilBuild        = "193.*"
}

intellijVMOptions :: SettingKey[IntellijVMOptions]

Fine tune java VM options for running the plugin with runIDE task. Example:

intellijVMOptions := intellijVMOptions.value.copy(xmx = 2048, xms = 256) 

ideaConfigOptions :: SettingKey[IdeaConfigBuildingOptions]

Fine tune how IntelliJ run configurations are generated when importing the project in IDEA.

runIDE [noPCE] [noDebug] [suspend] [blocking] :: InputKey[Unit]

Runs IntelliJ IDE with current plugin. This task is non-blocking by default, so you can continue using SBT console.

By default, IDE is run with non-suspending debug agent on port 5005. This can be overridden by either optional arguments above, or by modifying default intellijVMOptions. ProcessCancelledExceptiona can also be disabled for current run by providing noPCE option.

Publishing and Verification

publishPlugin [channel] :: InputKey[String]

Upload and publish your IntelliJ plugin on https://plugins.jetbrains.com. In order to publish to the repo you need to obtain permanent token and either place it into ~/.ij-plugin-repo-token file or pass via IJ_PLUGIN_REPO_TOKEN env or java property.

This task also expects an optional argument - a custom release channel. If omitted, plugin will be published to the default plugin repository channel (Stable)

runPluginVerifier :: TaskKey[File]

IntelliJ Plugin Verifier integration task allows to check the binary compatibility of the built plugin against the currently used or explicitly specified IntelliJ IDE builds. The task returns a folder with the verification reports.

The verification can be customized by changing the default options defined in the pluginVerifierOptions key.

pluginVerifierOptions := pluginVerifierOptions.value.copy(
  version = "1.254",        // use a specific verifier version
  offline = true,           // forbid the verifier from reaching the internet
  overrideIDEs  = Seq("IC-2019.3.5", "PS-2019.3.2"), // verify against specific products instead of 'intellijBuild'
  failureLevels = Set(FailureLevel.DEPRECATED_API_USAGES) // only fail if deprecated APIs are used
   // ...
)

signPlugin :: TaskKey[File]

Utility task that signs the plugin artifact before uploading to the JetBrains Marketplace. Signing is performed using the Marketplace zip signer library. To sign a plugin a valid certificate chain, and a private key are required.

Signing is disabled by default at the moment. To enable it and set the options, modify the signPluginOptions key:

signPluginOptions := signPluginOptions.value.copy(
  enabled = true,
  certFile = Some(file("/path/to/certificate")), // or via PLUGIN_SIGN_KEY env var
  privateKeyFile  = Some(file("/path/to/privateKey")), // or via PLUGIN_SIGN_CERT env var
  keyPassphrase = Some("keyPassword") // or None if password is not set(or via PLUGIN_SIGN_KEY_PWD env var)
)

If signing the plugin artifact zip is enabled via signPluginOptions, this task will be used a dependency of the publishPlugin task, so that the artifact is automatically signed before uploading to the JetBrains Marketplace

buildIntellijOptionsIndex :: TaskKey[Unit]

Builds index of options provided by the plugin to make them searchable via search everywhere action. This task should either be manually called instead of packageArtifact or before packageArtifactZip since it patches jars already built by packageArtifact.

Packaging

packageMethod :: SettingKey[PackagingMethod]

Default for root project: PackagingMethod.Standalone(targetPath = s"lib/${name.value}.jar")

Default for all other subprojects: PackagingMethod.MergeIntoParent()

Controls how current project will be treated when packaging the plugin artifact.

// produce standalone jar with the same name as the project:
packageMethod := PackagingMethod.Standalone()

// put all classes of this project into parent's jar
// NB: this option supports transitive dependencies on projects: it will walk up the dependency 
// tree to find the first Standalone() project, however if your project has multiple such parents
// this will result in an error - in this case use MergeIntoOther(project: Project) to expicitly
// specify in which project to merge into
packageMethod := PackagingMethod.MergeIntoParent()

// merge all dependencies of this project in a standalone jar
// being used together with assembleLibraries setting allows sbt-assembly like packaging
// the project may contain classes but they will be ignored during packaging
packageMethod := PackagingMethod.DepsOnly("lib/myProjectDeps.jar")
assembleLibraries := true

// skip project alltogether during packaging
packageMethod := PackagingMethod.Skip()

packageLibraryMappings :: SettingKey[Seq[(ModuleID, Option[String])]]

Default for root project: Seq.empty

Default for all other projects:

"org.scala-lang"  % "scala-.*" % ".*"        -> None ::
"org.scala-lang.modules" % "scala-.*" % ".*" -> None :: Nil

Sequence of rules to fine-tune how the library dependencies are packaged. By default all dependencies including transitive are placed in the subfolder defined by packageLibraryBaseDir(defaults to "lib") of the plugin artifact.

You can use the findLibraryMapping task to debug the library mappings

// merge all scalameta jars into a single jar
packageLibraryMappings += "org.scalameta" %% ".*" % ".*" -> Some("lib/scalameta.jar")

// skip packaging protobuf
packageLibraryMappings += "com.google.protobuf" % "protobuf-java" % ".*" -> None

// rename scala library(strip version suffix)
packageLibraryMappings += "org.scala-lang" % "scala-library" % scalaVersion -> Some("lib/scala-library.jar")

packageLibraryBaseDir :: SettingKey[File]

Default: file("lib")

Sets the per-project default sub-folder into which external libraries are packaged. Rules from packageLibraryMappings will override this setting.

NB!: This directory must be relative to the packageOutputDir so don't prepend values of the keys with absolute paths (such as target or baseDirectory) to it

NB!: IDEA plugin classloader only adds the lib folder to the classpath when loading your plugin. Modifying this setting will essentially exclude the libraries of a project from automatic classloading

packageLibraryBaseDir  := file("lib") / "third-party"

// protobuf will still be packaged into lib/protobuf.jar
packageLibraryMappings += "com.google.protobuf" % "protobuf-java" % ".*" -> Some("lib/protobuf.jar")

packageFileMappings :: TaskKey[Seq[(File, String)]]

Default: Seq.empty

Defines mappings for adding custom files to the artifact or even override files inside jars. Target path is considered to be relative to packageOutputDir.

// copy whole folder recursively to artifact root
packageFileMappings += target.value / "repo" -> "repo/"

// package single file info a jar
packageFileMappings += "resources" / "ILoopWrapperImpl.scala" ->
                            "lib/jps/repl-interface-sources.jar"
                            
// overwrite some file inside already existing jar of the artifact
packageFileMappings +=  "resources" / "META-INF" / "plugin.xml" ->
                            "lib/scalaUltimate.jar!/META-INF/plugin.xml"                            

packageAdditionalProjects :: SettingKey[Seq[Project]]

Default: Seq.empty

By default the plugin builds artifact structure based on internal classpath dependencies of the projects in an SBT build(dependsOn(...)). However, sometimes one may need to package a project that no other depends upon. This setting is used to explicitly tell the plugin which projects to package into the artifact without a need to introduce unwanted classpath dependency.

shadePatterns :: SettingKey[Seq[ShadePattern]]

Default: Seq.empty

Class shading patterns to be applied by JarJar library. Used to resolve name clashes with libraries from IntelliJ platform such as protobuf.

shadePatterns += ShadePattern("com.google.protobuf.**", "zinc.protobuf.@1")

bundleScalaLibrary in ThisBuild :: SettingKey[Boolean]

Trying to load the same classes in your plugin's classloader which have already been loaded by a parent classloader will result in classloader constraint violation. A vivid example of this scenario is depending on some other plugin, that bundles scala-library.jar(e.g. Scala plugin for IJ) and still bundling your own.

To workaround this issue sbt-idea-plugin tries to automatically detect if your plugin project has dependencies on other plugins with Scala and filter out scala-library.jar from the resulting artifact. However, the heuristic cannot cover all possible cases and thereby this setting is exposed to allow manual control over bundling the scala-library.jar

packageOutputDir :: SettingKey[File]

Default: target.value / "plugin" / intellijPluginName.in(ThisBuild).value.removeSpaces

Folder to place the assembled artifact into.

packageArtifact :: TaskKey[File]

Builds unpacked plugin distribution. This task traverses dependency graph of the build and uses settings described in the section above to create sub-artifact structure for each project. By default all child projects' classes are merged into the root project jar, which is placed into the "lib" folder of the plugin artifact, all library dependencies including transitive are placed in the "lib" folder as well.

packageArtifactZip :: TaskKey[File]

Produces ZIP file from the artifact produced by packagePlugin task. This is later used by publishPlugin as an artifact to upload.

Utils

findLibraryMapping :: InputKey[Seq[(String, Seq[(ModuleKey, Option[String])])]]

Returns detailed info about libraries and their mappings by a library substring. Helps to answer questions such as "Why is this jar in the artifact?" or "Which module introduced this jar?" Example:

sbt:scalaUltimate> show findMapping interface
[info] * (runtimeDependencies,ArrayBuffer((org.scala-sbt:compiler-interface:1.4.0-M12[],Some(lib/jps/compiler-interface.jar)), (org.scala-sbt:util-interface:1.3.0[],Some(lib/jps/sbt-interface.jar))))
[info] * (repackagedZinc,ArrayBuffer((org.scala-sbt:compiler-interface:1.4.0-M12[],Some(*)), (org.scala-sbt:launcher-interface:1.1.3[],Some(*)), (org.scala-sbt:util-interface:1.3.0[],Some(*))))
[info] * (compiler-jps,ArrayBuffer((org.scala-sbt:util-interface:1.3.0[],Some(*)), (org.scala-sbt:compiler-interface:1.4.0-M12[],Some(lib/jps/compiler-interface.jar))))
[info] * (compiler-shared,ArrayBuffer((org.scala-sbt:util-interface:1.3.0[],Some(*)), (org.scala-sbt:compiler-interface:1.4.0-M12[],Some(*))))

printProjectGraph :: TaskKey[Unit]

Prints ASCII graph of currently selected project to console. Useful for debugging complex builds.

Running the plugin

From SBT

To run the plugin from SBT simply use runIDE task. Your plugin will be automatically compiled, an artifact built and attached to new IntelliJ instance.

Debugger can later be attached to the process remotely - the default port is 5005.

From IDEA

  • sbt-idea-plugin generates IDEA-readable artifact xml and run configuration on project import
  • After artifact and run configuration have been created(they're located in .idea folder of the project) you can run or debug the new run configuration. This will compile the project, build the artifact and attach it to the new IDEA instance
  • ❗ Note that doing an "SBT Refresh" is required after making changes to your build that affect the final artifact(i.e. changing libraryDependencies), in order to update IDEA configs
  • ❗ You may need to manually build the artifact when running your plugin for the first time

Custom IntelliJ artifacts repo

Under some circumstances using a proxy may be required to access IntelliJ artifacts repo, or there even is a local artifact mirror set up. To use non-default repository for downloading IntelliJ product distributions set sbtidea.ijrepo jvm property. Example: -Dsbtidea.ijrepo=https://proxy.mycompany.com/intellij-repository

Auto enable the plugin

Sbt-idea-plugin currently breaks scalaJS compilation, and thereby has autoloading disabled. To enable it either add enablePlugins(SbtIdeaPlugin) to project definition. Example:

lazy val hocon = project.in(file(".")).settings(
  scalaVersion  := "2.12.8",
  version       := "2019.1.2",
  intellijInternalPlugins := Seq("properties"),
  libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test",
).enablePlugins(SbtIdeaPlugin)

If you with to automatically enable the plugin for all projects in your build, place the following class into top level project folder of your build.

import org.jetbrains.sbtidea.AbstractSbtIdeaPlugin

object AutoSbtIdeaPlugin extends AbstractSbtIdeaPlugin {
  override def trigger  = allRequirements
}

Known Issues and Limitations

name key in projects

Please do not explicitly set the name setting key for projects that have SbtIdeaPlugin attached. SBT will automatically set it from the lazy val's name of the project definition.

IDEA cannot correctly handle the sutuation when name key and lazy val's name of a project are different, thus making the generated artifact and run configuration xml's invalid.

Related issue: JetBrains#72

Plugin artifact not built when running from IDEA after importing the project

The generated IDEA run configurations depend on the built artifact of the plugin, so it should be built automatically when running or debugging the generated configuration.

However, when the IDEA xml configuration file is created externally, like in the case of sbt-idea-plugin, it is sometimes not picked up immediately and requires an explicit loading.

It is recommended to explicitly invoke Build | Build Artifacts | Rebuild from IDEA after importing the project for the first time(i.e. when xmls are first generated).

Development notes

To publish a new version of sbt-idea-plugin, just add a new tag in format vA.B.C (e.g.v3.13.4) and push it to the main branch. TeamCity will automatically Build/Test/Deploy it in sbt-idea-plugin configuration.
(works in internal network only)