Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
IDE Scripting

Here are my attempts to script an IntelliJ-based IDE using javax.script.* API (ex-JSR-223).

The list of available scripting languages and engines:

  1. Groovy - built-in, via Groovy jars and <app>/lib/groovy-jsr223-xxx.jar
  2. JavaScript (Nashorn) - built-in, via Java Runtime <app>/jbr/... (deprecated and will be removed soon)
  3. JavaScript (GraalJS) - https://plugins.jetbrains.com/plugin/12548-intellij-scripting-javascript
  4. JPython - https://plugins.jetbrains.com/plugin/12471-intellij-scripting-python
  5. JRuby - https://plugins.jetbrains.com/plugin/12549-intellij-scripting-ruby
  6. Clojure - https://plugins.jetbrains.com/plugin/12469-intellij-scripting-clojure
  7. Kotlin - https://plugins.jetbrains.com/plugin/6954-kotlin (bundled in IntelliJ IDEA)

Open IDE Scripting Console, type a statement, hit Ctrl-Enter to execute the current line or selection.

.profile.language-extension file in the same directory will be executed along with it if present.

CAUTION: Scripts exert full control over IDE VM so any damage is possible, e.g. System.exit(0).

(write #(. (document (firstFile "PsiManager.java")) (setText "Rollback!")))
(pool #(do (Thread/sleep 5000) (. IDE print "123")))
(action "TryMe!" "alt shift P" (fn [e] (let
[e (event-data e 'PSI_ELEMENT)
res (and (-> e nil? not) (.canFindUsages finds e)) ]
(. IDE print (str "can find usages of '" e "'? " (if res "Yes!" "Nope"))))))
(timer "bump" 500 #(. IDE (print (str (System/currentTimeMillis) ": bump!"))))
write { "PsiManager.java".firstFile().document().setText("Rollback!") }
pool { Thread.sleep(5000); IDE.print("123") }
action("TryMe!", "alt shift P") { e->
e = PSI_ELEMENT.from(e)
res = e != null && finds.canFindUsages(e)
IDE.print("can find usages of '" + e +"'? " + (res ? "Yes!" : "Nope"))
}
timer("bump", 500) {
IDE.print(System.currentTimeMillis() + ": bump!")
}
dispose("bump")
def ep = "com.intellij.iconLayerProvider".ep()
ep.registerExtension(new com.intellij.ide.IconLayerProvider() {
String getLayerDescription() { return "123" }
javax.swing.Icon getLayerIcon(com.intellij.openapi.util.Iconable element, boolean isLocked) {
"config".equals(element.getName()) ? com.intellij.icons.AllIcons.Nodes.FinalMark : null
}
})
ep.unregisterExtension(ep.getExtensions().last())
(doseq [x (list (.project IDE) (.application IDE))]
(let [pico (.getPicoContainer x)
field (.getDeclaredField (.getClass pico) "componentKeyToAdapterCache")
services (do (.setAccessible field true) (.get field pico))]
(doseq [key (filter #(instance? String %) (.keySet services))]
(let [match (re-find (re-matcher #"[\.]([^\.]+?)(Service|Manager|Helper|Factory)?$" key))
groups (rest match)
singular (empty? (rest groups))
words (seq (com.intellij.psi.codeStyle.NameUtil/nameToWords (first groups)))
short0 (clojure.string/join (flatten (list (.toLowerCase (first words)) (rest words))))
shortName (if (false? singular) (com.intellij.openapi.util.text.StringUtil/pluralize short0) short0)]
(try
(intern *ns* (symbol shortName) (.getComponentInstance pico key))
(catch Exception e ()))
)
)
)
)
(defn file [^String x] (.findFileByUrl virtualFiles (com.intellij.openapi.vfs.VfsUtil/pathToUrl x)))
(defn file2 [^String x] (first (filter #( = x (.getName %)) (.getOpenFiles fileEditors))))
(defn all-scope [] (com.intellij.psi.search.GlobalSearchScope/allScope (.project IDE)))
(defn findPsi [^String x] (com.intellij.psi.search.FilenameIndex/getFilesByName (.project IDE), x, (all-scope)))
(defn findFile [^String x] (com.intellij.psi.search.FilenameIndex/getVirtualFilesByName (.project IDE) x, (all-scope)) )
(defn firstPsi [^String x] (first (findPsi x)))
(defn firstFile [^String x] (first (findFile x)))
(defn ep [^String x] (.getExtensionPoint (com.intellij.openapi.extensions.Extensions/getArea nil) x))
(defn psi [x] (.findFile psis x))
(defn document [x] (.getDocument fileDocuments x))
(defn editor ([x] (.getEditor (.getSelectedEditor fileEditors x)))
([] (try (.. windows (getFocusedComponent (. IDE project)) (getEditor)) (catch Exception e ()) )) )
(defn #^Runnable runnable [x] (proxy [Runnable] [] (run [] (x))))
(defn write [x] (.. IDE application (runWriteAction (runnable x))))
(defn read [x] (.. IDE application (runReadAction (runnable x))))
(defn pool [x] (.. IDE application (executeOnPooledThread (runnable x))))
(defn swing [x] (com.intellij.util.ui.UIUtil/invokeLaterIfNeeded (runnable x)))
(defn dumpe [e] (.print IDE (str e (clojure.string/replace (str (seq (.getStackTrace e))) #"#<StackTraceElement " "\n at "))))
(defn safe [x] (try (x) (catch Exception e (dumpe e))))
(defn data-key [e] (let [c ['com.intellij.openapi.actionSystem.LangDataKeys]]
(some #(try (. (. (Class/forName (str %)) (getField (str e))) (get nil)) (catch Exception e nil) ) c)))
(defn event-data [e key] (.getData (data-key key) (.getDataContext e)))
(defn action [name & x]
(.unregisterAction actions name)
(.. keymaps getActiveKeymap (removeAllActionShortcuts name))
(if (nil? (second x)) nil
(let [[shortcut, perform] x]
(. actions (registerAction name (proxy [com.intellij.openapi.actionSystem.AnAction] [name, name, nil]
(actionPerformed [e] (perform e)) )))
(if (= shortcut nil) nil
(.. keymaps (getActiveKeymap) (addShortcut name
(new com.intellij.openapi.actionSystem.KeyboardShortcut (javax.swing.KeyStroke/getKeyStroke shortcut) nil)) )
)
nil
)
)
)
(defn dispose [x] (let [t (if (instance? com.intellij.openapi.Disposable x) (x) (.put IDE x nil))]
(if (nil? t) nil (com.intellij.openapi.util.Disposer/dispose t)) ) )
(defn timer [name & x] (do
(dispose name)
(if (nil? (second x)) nil
(let [[delay, perform] x
h (new com.intellij.util.Alarm (.project IDE))
r (runnable (fn time_fun [] (do (perform) (. h (addRequest (runnable time_fun) delay)) ) ))]
(. h (addRequest r delay))
(. IDE (put name h))
)
) ))
import static com.intellij.openapi.actionSystem.LangDataKeys.*
//////////////////////////////////////////////////////////////////////////////////////////////
metaClass.propertyMissing = {name ->
switch (name) {
case "application": return com.intellij.openapi.application.ApplicationManager.getApplication()
case "project": return com.intellij.openapi.project.ProjectManager.getInstance().getOpenProjects()[0]
case "INDEX": return com.intellij.psi.search.FilenameIndex
case "GSS": return com.intellij.psi.search.GlobalSearchScope
case "EXT": return com.intellij.openapi.extensions.Extensions
default:
def variants = []
for (t in [IDE.project, IDE.application]) {
for (obj in t.picoContainer.componentKeyToAdapterCache.keySet()) {
def key = obj instanceof String ? obj : obj instanceof Class ? obj.getName() : null
if (key == null) continue
def match = key =~ /[\.]([^\.]+?)(Service|Manager|Helper|Factory)?$/
def groups = match.size()? match[0][1..-1] : [key, null]
def singular = groups[1..-1][0] == null
def words = com.intellij.psi.codeStyle.NameUtil.nameToWords(groups[0])
def short0 = [words[0].toLowerCase(), words.length==1? "" : words[1..-1]].flatten().join()
def shortName = singular? short0 : com.intellij.openapi.util.text.StringUtil.pluralize(short0)
if (shortName.equals(name)) return t.picoContainer.getComponentInstance(obj);
if (com.intellij.openapi.util.text.StringUtil.containsIgnoreCase(groups[0], name)) variants.add(shortName)
}
}
throw new MissingPropertyException("Service or Component '$name' not found. Variants: $variants")
}
}
String.class.metaClass.file = {-> virtualFiles.findFileByUrl(com.intellij.openapi.vfs.VfsUtil.pathToUrl(delegate))}
String.class.metaClass.file2 = {-> def name = delegate; fileEditors.getOpenFiles().find {file -> file.getName().equals(name) }}
String.class.metaClass.findPsi = {-> INDEX.getFilesByName(project, delegate, GSS.allScope(project)) }
String.class.metaClass.findFile = {-> INDEX.getVirtualFilesByName(project, delegate, GSS.allScope(project)) }
String.class.metaClass.firstPsi = {-> delegate.findPsi()[0] }
String.class.metaClass.firstFile = {-> delegate.findFile()[0] }
String.class.metaClass.ep = {-> EXT.getArea(null).getExtensionPoint(delegate) }
com.intellij.openapi.actionSystem.DataKey.class.metaClass.from = {e -> delegate.getData(e.getDataContext()) }
def virtualFileMetaClass = com.intellij.openapi.vfs.VirtualFile.class.metaClass
virtualFileMetaClass.psi = {-> psiManager.findFile(delegate)}
virtualFileMetaClass.document = {-> fileDocuments.getDocument(delegate)}
virtualFileMetaClass.editor = {-> fileEditors.getSelectedEditor(delegate)?.getEditor()}
def psiMetaClass = com.intellij.psi.PsiElement.class.metaClass
psiMetaClass.document = {-> psiDocuments.getDocument(delegate)}
psiMetaClass.file = {-> delegate.getContainingFile().getVirtualFile()}
psiMetaClass.editor = {-> fileEditors.getSelectedEditor(delegate.file())?.getEditor()}
write = { c -> application.runWriteAction(c)}
read = { c -> application.runReadAction(c)}
pool = { c -> application.executeOnPooledThread(c)}
swing = { c -> com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(c)}
action = { name, shortcut = null, perform = null ->
actions.unregisterAction(name)
keymaps.getActiveKeymap().removeAllActionShortcuts(name)
if (perform == null) return
actions.registerAction(name, new com.intellij.openapi.actionSystem.AnAction(name, name, null) {
@Override
void actionPerformed(com.intellij.openapi.actionSystem.AnActionEvent e) {
perform(e)
} })
if (shortcut != null) {
keymaps.getActiveKeymap().addShortcut(name, new com.intellij.openapi.actionSystem.KeyboardShortcut(
javax.swing.KeyStroke.getKeyStroke(shortcut), null))
}
}
timer = {name, delay = 1, perform = null ->
dispose(name)
if (perform == null) return
def h = new com.intellij.util.Alarm(project)
def r = new Runnable() { public void run() {perform(); h.addRequest(this, delay); }}
h.addRequest(r, delay)
IDE.put(name, h)
}
dispose = { h ->
t = h instanceof com.intellij.openapi.Disposable ? h : IDE.put(h, null)
if (t != null) com.intellij.openapi.util.Disposer.dispose(t)
}
editor = { -> try {windows.getFocusedComponent(project).getEditor()} catch(e){}}
import com.intellij.openapi.ui.Messages
val b = bindings as Map<String, Any>
val IDE = b["IDE"] as com.intellij.ide.script.IDE
val name = System.getProperty("user.name")
IDE.print("$name>")
Messages.showInfoMessage("Hi, $name", IDE.project.name)
import com.intellij.notification.*
// Change the `pattern` and the `replacement` as needed
def pattern = ~/(.*)XXX\.(.*)/
def replacement = "\$1_xxx_.\$2"
// `Rename Multiple Files` action:
// 1. select some files in `Project` view
// 2. invoke it via Search Everywhere/Goto Action or a shortcut
// 3. see `Event Log` for the report
// Note: `action`, `write`, etc. are defined in `.profile.groovy`
action("Rename Multiple Files", "control alt R") { event ->
def files = VIRTUAL_FILE_ARRAY.getData(event.getDataContext())
def report = new StringBuilder()
for (def file : files) {
def curName = file.name
def matcher = curName =~ pattern
if (!matcher.matches()) {
report.append("$curName skipped\n")
}
else {
def newName = matcher.replaceFirst(replacement)
report.append("$curName -> $newName\n")
write {
file.rename(this, newName)
}
}
}
Notifications.Bus.notify(new Notification(
"test",
"${event.presentation.text}: ${files ? files.length : 0} file(s) processed",
report.toString(), NotificationType.INFORMATION))
}
@liuyibo123
Copy link

liuyibo123 commented Jan 2, 2018

example for javascript please

@liuyibo123
Copy link

liuyibo123 commented Jan 2, 2018

thanks

@brazilianldsjaguar
Copy link

brazilianldsjaguar commented Aug 30, 2018

Seems that the ECMAScript/JavaScript support is ECMAScript 5 - which means no import statements or anything like that. I was able to get any interaction by doing fully-declared namespaced classes, e.g.:

var processList = com.intellij.execution.process.OSProcessUtil.getProcessList();

@tdegrunt
Copy link

tdegrunt commented Sep 27, 2018

This is a very interesting feature, but without examples, it's hard. Anything in Kotlin or JavaScript?

@muthuishere
Copy link

muthuishere commented Oct 30, 2018

I am getting ex

Tried the Groovy example:

action("TryMe!", "alt shift P") { e->
    e = PSI_ELEMENT.from(e)
    res = e != null &&  finds.canFindUsages(e)
    IDE.print("can find usages of '" + e +"'? " + (res ? "Yes!" : "Nope"))
}

But got this error:

MissingMethodException: No signature of method: org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.action() is applicable for argument types: (java.lang.String, java.lang.String, Script1$_run_closure1) values: [TryMe!, alt shift P, Script1$_run_closure1@1e21e437]
Possible solutions: notify(), wait()

I'm presuming this example should add a right-click context menu called TryMe!, which finds usages of the token selected?

I am getting the same exception, Can someone help?

@frankgerhardt
Copy link

frankgerhardt commented Dec 2, 2018

Kotlin examples wanted!

@glockbender
Copy link

glockbender commented Mar 4, 2019

Kotlin examples wanted!

Kotlin example with output

import com.intellij.openapi.ui.Messages.showInfoMessage
var sum: Long = 0L
val arr = "35907 77134 453661 175096 23673 29350"
    .split(" ")
arr.forEach { sum+=it.length }

showInfoMessage((sum.toFloat() / arr.size).toString(), "test")

@mnowotnik
Copy link

mnowotnik commented Mar 25, 2019

Getting current project and editor in Kotlin:

val dataContext = DataManager.getInstance().dataContextFromFocusAsync.blockingGet(1000)!!
val project = CommonDataKeys.PROJECT.getData(dataContext)!!
val editor = BaseCodeInsightAction.getInjectedEditor(project, CommonDataKeys.EDITOR.getData(dataContext))!!

@smaudet
Copy link

smaudet commented Jun 4, 2019

IDE.print is no longer functional. It does not display to the run toolwindow and no console appears. Additionally the static method is private and constructing the IDE instance manually yields no results.

Fallback, although not pretty, is to open up File I/O directly and e.g. tail -f <yourfile>

@smaudet
Copy link

smaudet commented Jun 4, 2019

I will also say from what I could tell it is not possible to gain console output from the scripting console into the run dialog, not without either:
a) Discovering the runtime console during run and injecting something into it
b) Collecting and dumping the console output internal to the script
c) Building a new Tool Window and manually outputting console output to it.

@ice1000
Copy link

ice1000 commented Jul 24, 2019

Kotlin example request

@shawkinaw
Copy link

shawkinaw commented Oct 10, 2019

Anybody know if there's a way to run an IDE script from the command line somehow? I'd like to trigger a script I've written from an external script.

@mgroth0
Copy link

mgroth0 commented Jun 15, 2020

I'd like to echo @shawkinaw 's question. Also, can we script the embedded terminal? It would be great if I could automatically open a terminal tab, name it, and run a command

@gregsh
Copy link
Author

gregsh commented Jun 16, 2020

Also, can we script the embedded terminal?

An IDE scripting console script, like the following:

com.intellij.openapi.components.ServiceManager.getService(IDE.project,
  com.intellij.sh.run.ShRunner.class).run("ls", "/", "title")

UPD Updated to address @skissane note below.

@gregsh
Copy link
Author

gregsh commented Jun 16, 2020

I've just added a Kotlin *.kts example to show the external bindings access which is tricky at the moment,
and updated IDE Scripting.md with the list of available languages/engines.

@skissane
Copy link

skissane commented Jul 13, 2020

@gregsh I thought @shawkinaw's question was, how do I use the IntelliJ command line interface to run an IDE Console script from a shell script? You seem to be answering the opposite question of how I run a shell script from the IDE Console script?

@gregsh
Copy link
Author

gregsh commented Jul 13, 2020

Anybody know if there's a way to run an IDE script from the command line somehow?

No such thing now, but a simple plugin with a com.intellij.appStarter extension shall do the trick.

UPD I'll do that, watch for IDEA-245847.

@sesm
Copy link

sesm commented Oct 27, 2020

I've tried .project.clj but I get RuntimeException: Unable to resolve symbol: fileEditors in this context.
I guess this symbol should be added to namespace in the first loop. I checked which symbols are added and the closest I see is fileEditorProviders. Should I use that one to get fileEditors somehow?

@gregsh
Copy link
Author

gregsh commented Feb 5, 2021

Anybody know if there's a way to run an IDE script from the command line somehow?

IDEA-245847 is now included in 2021 EAP.
Usage: idea ideScript <files>...

@mvastola
Copy link

mvastola commented Mar 3, 2021

Any starter code for ruby? I.e. files we need to require

@mgroth0
Copy link

mgroth0 commented Mar 11, 2021

@mnowotnik

I want to get the editor object you referenced but my code is blocking

val dataContext = getInstance().dataContextFromFocusAsync.blockingGet(1000)!!
val project = CommonDataKeys.PROJECT.getData(dataContext)!!
val editor_data = CommonDataKeys.EDITOR.getData(dataContext)
IDE.print("here1")
val editor = BaseCodeInsightAction.getInjectedEditor(project, editor_data)!!
IDE.print("here2")

Output

here1 

I never see here2, nor any exceptions

@mnowotnik
Copy link

mnowotnik commented Mar 11, 2021

@mgroth0

Just checked, works without a problem on IDEA 2020.3

val dataContext = DataManager.getInstance().dataContextFromFocusAsync.blockingGet(10)!!
val project = CommonDataKeys.PROJECT.getData(dataContext)!!
val editor = BaseCodeInsightAction.getInjectedEditor(project, CommonDataKeys.EDITOR.getData(dataContext))!!
val ctx = SearchContext(project)

@mgroth0
Copy link

mgroth0 commented Mar 11, 2021

I've discovered part of my problem. If I run this code while focused on the editor:

val dataContext = DataManager.getInstance().dataContextFromFocusAsync.blockingGet(10)!!
IDE.print("${dataContext}")

I get this output:

component=EditorComponent file=file:///Users/matt/Library/Application Support/JetBrains/IntelliJIdea2020.3/consoles/ide/ide-scripting.kts

But for my use case, I need this to work when I'm not focused on the editor. So I'm actually focused on the embedded terminal. So as you might expect from the name of the function dataContextFromFocusAsync, I'm getting:

component=com.intellij.terminal.JBTerminalPanel[,0,0,699x249,alignmentX=0.0,alignmentY=0.0,border=,flags=1,maximumSize=,minimumSize=,preferredSize=java.awt.Dimension[width=699,height=243]]

@mgroth0
Copy link

mgroth0 commented Mar 11, 2021

To get the editor, I use

val projectMan = com.intellij.openapi.project.ProjectManager.getInstance()
val projects = projectMan.openProjects
val project = projects.first { it.name == "projectname" }
val editor = FileEditorManager.getInstance(project)

Then I can use editor.openFile(f, true) to open files. This works perfectly. However, I can not scroll to a specific line. Then I came across the code you posted @mnowotnik, and found that the editor class you demonstrated does have a scrollingModel.scrollTo method that works well. However, as I stated in my previous message, I just can't seem to get a reference to the com.intellij.openapi.editor.Editor (for a specific file, in a specific project, and without being focused on any particular component) yet, in the same way I could get a FileEditorManager instance.

@mgroth0
Copy link

mgroth0 commented Mar 11, 2021

Figured it out!
Using the project and editor values from my previous comment:

BaseCodeInsightAction.getInjectedEditor(project, editor.selectedTextEditor).scrollingModel.scrollTo(LogicalPosition(50,0), ScrollType.CENTER)

@dkandalov
Copy link

dkandalov commented Apr 7, 2021

@boonshift
Copy link

boonshift commented May 20, 2021

I occasionally accidentally close my pinned tabs and wanted a quick and dirty way to re-pin them.
After reading this page, finally cobbled together a few lines to do just that.
Pasted below in case someone else has similar need.

https://gist.github.com/boonshift/2e241311c58e019b74b8f35d1ed28981

import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx

val b = bindings as Map<String, Any>
val IDE = b["IDE"] as com.intellij.ide.script.IDE

val editor = FileEditorManagerEx.getInstance(IDE.project) as FileEditorManagerEx
val filesToPin = editor.openFiles.filter { it.name.contains("Scratch") }
filesToPin.forEach { f -> editor.currentWindow.setFilePinned(f, true) }

@skissane
Copy link

skissane commented Sep 22, 2021

@gregsh A huge thank you for IDEA-245847, I was really looking for that. I created a related PR to teach the launcher Python script how to invoke it. However, looking at the current implementation of IdeScriptStarter I can see a couple of gaps:

  • There is no way to pass arguments to scripts. If I call ideScript foo.groovy bar it tries to run bar as a second script (which it can't find) instead of passing it as an argument (somehow) to foo.groovy
  • There is zero feedback to the launcher about whether the script ran successfully. If the script doesn't exist, or contains a syntax error, the launcher still returns successfully, although the error does get logged in idea.log. If the script compiles successfully, but throws an exception during execution, you don't even get any error logged in idea.log

I can cook up some hacky workarounds to (mostly) solve the above two issues for my own purposes, but it would be great if one day there was some solution for them out of the box.

@fuxin-liu
Copy link

fuxin-liu commented Oct 27, 2021

Dear Mr Shrago:
I have installed Intellij 2021.1.3 and 2021.2.3 for testing IDE Scripting.
Samples.groovy and .profile.groovy ran well under Intellij 2021.1.3, but got the following errors under Intelli 2021.2.3:
MissingPropertyException: No such property: componentKeyToAdapterCache for class: com.intellij.openapi.project.impl.ProjectExImpl

Could you help me?
Many thanks to you!
屏幕截图(2)

@atorr0
Copy link

atorr0 commented Jun 30, 2022

@fuxin-liu , you'll need the Kotlin plugin activated in your IDE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment