|
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() |