Skip to content

Instantly share code, notes, and snippets.

@Yasushi
Forked from gregsh/- IDE Scripting.md
Last active April 6, 2024 06:23
Show Gist options
  • Save Yasushi/dcd4fa9e8992b995453759830d48150a to your computer and use it in GitHub Desktop.
Save Yasushi/dcd4fa9e8992b995453759830d48150a to your computer and use it in GitHub Desktop.
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())
(clojure.core/use '[clojure.core :exclude [read write]])
(defn get-service [key] (.getService (.-project IDE) key))
(defn all-scope [] (com.intellij.psi.search.GlobalSearchScope/allScope (.project IDE)))
(defn find-file [^String x] (com.intellij.psi.search.FilenameIndex/getVirtualFilesByName (.project IDE) x, (all-scope)) )
(defn find-psi [^String x] (com.intellij.psi.search.FilenameIndex/getFilesByName (.project IDE), x, (all-scope)))
(defn first-psi [^String x] (first (find-psi x)))
(defn first-file [^String x] (first (find-file x)))
(comment
(def file-editors (get-service com.intellij.openapi.fileEditor.FileEditorManager))
(.openFile file-editors (first-file "build.gradle"))
(vec (.getSelectedFiles file-editors)))
(doseq [container-manager (list (.project IDE) (.application IDE))]
(let [service-container (.getServiceContainerInternal container-manager)]
(doseq [key (filter #(instance? String %)
(.. service-container (instanceHoldersAndKeys) (keySet)))]
(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)
instance (.getComponent container-manager key)]
(try
;; (.print IDE (str "shortName:" shortName))
(intern *ns* (symbol shortName) instance)
(catch Exception e (.print IDE 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 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))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment