Skip to content

Instantly share code, notes, and snippets.

@alshain
Last active January 23, 2019 09:52
Show Gist options
  • Save alshain/9a1e58f46a40b2aab72db34e1c40b2f8 to your computer and use it in GitHub Desktop.
Save alshain/9a1e58f46a40b2aab72db34e1c40b2f8 to your computer and use it in GitHub Desktop.
IDE Scripting Console - Survive Reload and onFileCHange

Some helper functions to easily test and reload IntelliJ services/extension points by registering handlers in a global object by name.

Also contains minimal amount of boilerplate to get the global IDE object, the project and makes println work.

import com.intellij.ide.DataManager
import com.intellij.ide.IdeEventQueue
import com.intellij.notification.NotificationDisplayType
import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vcs.VcsDataKeys
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.util.ui.update.MergingUpdateQueue
import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
import kotlin.streams.toList
val notificationGroup: NotificationGroup = NotificationGroup("LivePluginDebug", NotificationDisplayType.STICKY_BALLOON, true)
val IDE: com.intellij.ide.script.IDE = bindings.get("IDE") as com.intellij.ide.script.IDE
val project = IDE.project!!
// Fix Kotlin scripting console output
fun println(s: String) {
kotlin.io.println(s)
IDE.print(s)
val notification = notificationGroup.createNotification("OnFileChange", s, NotificationType.INFORMATION, null)
Notifications.Bus.notify(notification)
}
fun println(s: Any?) {
println("" + s?.toString())
}
private object OnFileChangeId {}
inline fun <reified T> registerObject(registeredObject: RegisteredObject<T>, queue: T) {
val objectKey = registeredObject.name
if (IDE.get(objectKey) != null) {
throw IllegalStateException("Object already registered, type: name: $objectKey, object: ${IDE.get(objectKey)}")
}
IDE.put(objectKey, queue)
}
inline fun <reified T> registerOrUpdateObject(registeredObject: RegisteredObject<T>, newObject: T): T? {
val objectKey = registeredObject.name
val oldObject = IDE.get(objectKey)
IDE.put(objectKey, newObject)
return oldObject as? T
}
inline fun <reified T> registeredObjectOnce(registeredObject: RegisteredObject<T>, makeT: () -> T): T {
val key = registeredObject.name
val oldObject = IDE.get(key)
if (oldObject !is T) {
if (oldObject != null) {
println("Replacing ${oldObject} due to type mismatch")
}
IDE.put(key, makeT())
}
return IDE.get(key) as T
}
inline fun reloadableService(version: Int, reloadableService: ReloadableService, onDispose: () -> Unit, makeT: () -> Unit) {
val registeredObject = objectKey<Unit>(reloadableService.type, reloadableService.name)
val registeredVersion = objectKey<Int>(reloadableService.type, "${reloadableService.name}-version")
val currentVersion = registeredVersion.fromIde()
val key = registeredObject.name
if (currentVersion == null || currentVersion != version) {
if (currentVersion != null) {
// Running a different version...
(IDE.get(key) as Unit?)?.let {
IDE.put(registeredVersion.name, null)
IDE.put(key, null)
onDispose()
}
}
registerObject(registeredObject, makeT())
registerObject(registeredVersion, version)
}
}
data class RegisteredObject<T>(val name: String)
interface NameBasedService<T> {
fun register(id: String, impl: T, parentDisposable: Disposable? = null)
fun dispose(id: String)
fun reset()
}
typealias OnRegister<T> = (abc: String, T) -> Unit
typealias OnDispose<T> = (abc: String) -> Unit
interface ServiceRegistryContext<T> {
val previousService: T?
val newService: T?
}
interface ServiceInitContext<T> {
val allServices: MutableList<T>
val delegatedDisposable: Disposable
}
fun <T> makeNameBasedServiceManager(type: String, name: String, onCreate: ServiceInitContext<T>.() -> Unit): NameBasedService<T> {
val listKey = objectKey<MutableList<T>>(type, "$name-list")
val mapKey = objectKey<MutableMap<String, T>>(type, "$name-by-name")
val serviceKey = ReloadableService(type, "$name-service")
val onDispose = objectKey<OnDispose<T>>(type, "$name-on-dispose")
val onRegister = objectKey<OnRegister<T>>(type, "$name-on-register")
registeredObjectOnce(listKey, { mutableListOf()})
registeredObjectOnce(mapKey, { mutableMapOf() })
reloadableService(9, serviceKey, {
println("Discarding service $type $name")
}, {
println("Running ServiceInit")
println("Registering new onRegister")
registerOrUpdateObject(onRegister, { id, newInstance ->
mapKey.fromIde()?.apply {
if (contains(id)) {
println("Replacing $id")
val oldInstance = get(id)!!
listKey.applyFromIde {
val position = indexOf(oldInstance)
set(position, newInstance)
put(id, newInstance)
}
} else if(containsValue(newInstance)) {
throw IllegalStateException("Cannot register same instance mulitple times under different keys")
} else {
listKey.applyFromIde {
println("Adding $id")
add(newInstance)
put(id, newInstance)
}
}
}
})
println("Registering new dispose")
registerOrUpdateObject(onDispose, { id ->
mapKey.fromIde()?.apply {
if (contains(id)) {
println("Removing $id")
val oldInstance = get(id)!!
listKey.applyFromIde {
val position = indexOf(oldInstance)
removeAt(position)
}
} else {
println("Failed to dispose")
}
}
})
// Clean up
mapKey.fromIde()?.let { byName ->
listKey.fromIde()?.retainAll(byName.values)
byName.values.retainAll(listKey.fromIde()!!)
}
onCreate(object: ServiceInitContext<T> {
override val allServices: MutableList<T>
get() = listKey.fromIde()!!
override val delegatedDisposable: Disposable
get() = Disposable {
println("Does not yet work")
}
})
})
// val oldRegister = onRegister.fromIde()
// val oldOnDispose = oldOnDispose.fromIde()
return object : NameBasedService<T> {
override fun reset() {
listKey.fromIde()?.apply { clear() }
mapKey.fromIde()?.apply { clear() }
}
override fun register(id: String, impl: T, parentDisposable: Disposable?) {
if (parentDisposable != null) {
Disposer.register(parentDisposable, Disposable { onDispose.fromIde()?.invoke(id) })
}
onRegister.fromIde()?.invoke(id, impl)
}
override fun dispose(id: String) {
onDispose.fromIde()?.invoke(id)
}
}
}
val ActivityListenerService = makeNameBasedServiceManager<Runnable>("activitylistener", "service") {
IdeEventQueue.getInstance().addActivityListener(Runnable {
allServices.forEach {
it.run()
}
}, delegatedDisposable)
}
data class ReloadableService(val type: String, val name: String)
fun <T> objectKey(type: String, name: String): RegisteredObject<T> {
return RegisteredObject("liveplugin-$type-$name")
}
val ActivityListenersByName = objectKey<MutableMap<String, Runnable>>("activitylistener", "delegates-by-name")
val ActivityListeners = objectKey<MutableList<Runnable>>("activitylistener", "delegate-list")
inline fun <reified T> RegisteredObject<T>.with(block: (T) -> Unit) {
(IDE.get(this.name) as T?)?.let(block)
}
inline fun <reified T> RegisteredObject<T>.applyFromIde(block: T.() -> Unit) {
(IDE.get(this.name) as T?)?.apply(block)
}
inline fun <reified T> RegisteredObject<T>.fromIde(): T? {
return (IDE.get(this.name) as T)
}
inline fun <reified T> RegisteredObject<T>.fromIdeAsAny(): Any? {
return IDE.get(this.name)
}
inline fun <reified T> RegisteredObject<T>.fromIdeIfTypeMatches(): T? {
return (IDE.get(this.name) as? T)
}
interface OnSelectedFilesChange {
val selectedFiles: List<VirtualFile>
}
fun onFileChange(name: String, delay: Int, timeUnit: TimeUnit, project: Project, onSelectedFilesChange: OnSelectedFilesChange.() -> Unit, parentDisposable: Disposable? = null) {
var previousResult: Set<VirtualFile>? = null
val seenTypes = mutableSetOf<Class<*>>()
class MyUpdate(identity: OnFileChangeId) : com.intellij.util.ui.update.Update(identity) {
override fun run() {
IdeFocusManager.getInstance(project).doWhenFocusSettlesDown {
// println("Focus settled down")
fun getListOfFiles(): List<VirtualFile>? {
val focusOwner = IdeFocusManager.getInstance(project).focusOwner
if (focusOwner != null) {
val dataContext = DataManager.getInstance().getDataContext(focusOwner)
// println(dataContext);
val selectedItems = PlatformDataKeys.SELECTED_ITEMS.getData(dataContext)
if (selectedItems != null) {
selectedItems.forEach {
if (seenTypes.add(it::class.java)) {
println(">>> NEW TYPE: ${it::class.java.canonicalName}")
}
}
// println(Arrays.toString(selectedItems))
val directories = selectedItems.filterIsInstance(PsiDirectory::class.java)
val dirFiles = directories.map { it.virtualFile }
val result = (selectedItems.filterIsInstance(PsiElement::class.java).mapNotNull { it.containingFile?.virtualFile } + dirFiles)
return result
}
println("PsiElement")
println(PlatformDataKeys.PSI_ELEMENT.getData(dataContext))
println("PlatformDataKeys.SELECTED_ITEM")
println(PlatformDataKeys.SELECTED_ITEM.getData(dataContext))
println("VcsDataKeys.VIRTUAL_FILE_STREAM")
println(VcsDataKeys.VIRTUAL_FILE_STREAM.getData(dataContext)?.toList())
println("End data context")
}
return null
}
val listOfFiles = getListOfFiles()
val setOfFiles = listOfFiles?.toSet()
if (previousResult != setOfFiles) {
previousResult = setOfFiles
if (setOfFiles != null) {
onSelectedFilesChange(object: OnSelectedFilesChange {
override val selectedFiles: List<VirtualFile>
get() = listOfFiles
})
} else {
println("No files detected")
}
}
}
}
}
registeredObjectOnce(ActivityListenersByName) {
mutableMapOf()
}
registeredObjectOnce(ActivityListeners) {
mutableListOf()
}
val mergingTimeSpan = timeUnit.toMillis(delay.toLong()).toInt()
// Old queues hanging around isn't that bad if the mergintTImeSpan is changed...
// We keep only one activity listener per [name], it will always delegate to the latest queue when fired
// If an event is being buffered until "timeout" in old queue, we might see a file change event from that old queue
val queueKey = objectKey<MergingUpdateQueue>("queue", "onFileChange-$name")
registeredObjectOnce(queueKey) {
val queue = object : MergingUpdateQueue("livePlugin:onFileChange", mergingTimeSpan, true, null) {}
queue
}.let { queue ->
// If queue exists already, we need to change it...
queue.setMergingTimeSpan(mergingTimeSpan)
}
ActivityListenerService.register("onFileChange-$name", Runnable {
queueKey.fromIde()?.queue(MyUpdate(OnFileChangeId))
})
}
fun main() {
// ActivityListenerService.reset()
println("Executing onFileChange")
onFileChange("set-of-selected",1, TimeUnit.SECONDS, project, {
println("Set of selected files has changed;")
selectedFiles.forEach {
println(it)
}
})
println("Executed onFIleChange")
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment