diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abc78ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.gradle/ +/build/ diff --git a/src/main/kotlin/com/platfom/sync/FilesDetectionsListener.kt b/src/main/kotlin/com/platfom/sync/FilesDetectionsListener.kt index 7aa2cb8..c9e6227 100755 --- a/src/main/kotlin/com/platfom/sync/FilesDetectionsListener.kt +++ b/src/main/kotlin/com/platfom/sync/FilesDetectionsListener.kt @@ -7,51 +7,38 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.vfs.VirtualFile -import org.java_websocket.client.WebSocketClient -import org.java_websocket.handshake.ServerHandshake +import com.platfom.sync.service.PlatformSyncService +import com.platfom.sync.service.WebSocketService import java.io.File -import java.net.URI class FilesDetectionsListener : FileEditorManagerListener { - private val socks = WebSocks(URI("ws://localhost:8123/platform")) - private val editorEventMulticasts = EditorFactory.getInstance().eventMulticaster + private val platformSyncService = PlatformSyncService.getInstance() + private val webSocketService = WebSocketService.getInstance() + private val editorEventMulticaster = EditorFactory.getInstance().eventMulticaster private var recentLine = -1 private var recentPath = "" init { - socks.connect() + webSocketService.connect() } private val mouseEditorObserver = object : EditorMouseListener { override fun mouseClicked(event: EditorMouseEvent) { - super.mouseClicked(event) - - val newLinePosition = event.editor.caretModel.currentCaret.logicalPosition.line + 1 - - if (socks.isOpen) { - if (newLinePosition != recentLine) { - recentLine = newLinePosition - socks.send("line===${recentLine}") + val logicalPosition = event.editor.caretModel.logicalPosition.line + 1 + if (logicalPosition != recentLine) { + recentLine = logicalPosition + if (webSocketService.isConnected()) { + webSocketService.sendMessage("reviewerUsername===${platformSyncService.getReviewerUsername()};line===$recentLine") } } } } - override fun fileClosed(source: FileEditorManager, file: VirtualFile) { - super.fileClosed(source, file) - editorEventMulticasts.removeEditorMouseListener(mouseEditorObserver) - } - - override fun selectionChanged(editorEvent: FileEditorManagerEvent) { - super.selectionChanged(editorEvent) - editorEvent.newFile?.let { - process(editorEvent.manager, it) - } - } + override fun selectionChanged(event: FileEditorManagerEvent) { + val source = event.manager + val file = event.newFile ?: return - override fun fileOpened(source: FileEditorManager, file: VirtualFile) { - super.fileOpened(source, file) - editorEventMulticasts.addEditorMouseListener(mouseEditorObserver) + editorEventMulticaster.addEditorMouseListener(mouseEditorObserver) process(source, file) } @@ -61,30 +48,12 @@ class FilesDetectionsListener : FileEditorManagerListener { val projectLocation = fullPath.split(baseProjectPath)[0] val filePath = fullPath.replace(projectLocation, "") - if (socks.isOpen) { + if (webSocketService.isConnected()) { val newPath = filePath.replace("[/\\\\]".toRegex(), "::") if (newPath != recentPath) { recentPath = newPath - socks.send("path===${recentPath}") + webSocketService.sendMessage("reviewerUsername===${platformSyncService.getReviewerUsername()};path===${recentPath}") } } } - - class WebSocks(uri: URI) : WebSocketClient(uri) { - override fun onOpen(handshakedata: ServerHandshake?) { - println("connected, ready to observe") - } - - override fun onMessage(message: String?) { - println("incomming $message") - } - - override fun onClose(code: Int, reason: String?, remote: Boolean) { - println("connection close") - } - - override fun onError(ex: Exception?) { - println("connection error") - } - } } \ No newline at end of file diff --git a/src/main/kotlin/com/platfom/sync/action/PlatformSyncStatusAction.kt b/src/main/kotlin/com/platfom/sync/action/PlatformSyncStatusAction.kt new file mode 100644 index 0000000..312629b --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/action/PlatformSyncStatusAction.kt @@ -0,0 +1,28 @@ +package com.platfom.sync.action + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.platfom.sync.service.PlatformSyncService +import com.platfom.sync.service.WebSocketService + +class PlatformSyncStatusAction : AnAction("Status") { + private val webSocketService = WebSocketService.getInstance() + + override fun update(e: AnActionEvent) { + val service = PlatformSyncService.getInstance() + val status = service.getPlatformSyncStatus() + val isConnected = webSocketService.isConnected() + e.presentation.text = "Status: ${status.description} (Click to ${if (isConnected) "Disconnect" else "Reconnect"})" + } + + override fun actionPerformed(e: AnActionEvent) { + if (webSocketService.isConnected()) { + webSocketService.disconnect() + } else { + webSocketService.connect() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT +} \ No newline at end of file diff --git a/src/main/kotlin/com/platfom/sync/action/ReviewerIdentifierInputAction.kt b/src/main/kotlin/com/platfom/sync/action/ReviewerIdentifierInputAction.kt new file mode 100644 index 0000000..0f258a6 --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/action/ReviewerIdentifierInputAction.kt @@ -0,0 +1,32 @@ +package com.platfom.sync.action + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.Messages +import com.platfom.sync.service.PlatformSyncService + +class ReviewerUsernameInputAction : AnAction("Reviewer Username Input") { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project + val service = PlatformSyncService.getInstance() + + val input = Messages.showInputDialog( + project, + "Input your username", + "Reviewer Username", + Messages.getQuestionIcon() + ) + + if (!input.isNullOrEmpty()) { + service.saveReviewerUsername(input) + } + } + + override fun update(e: AnActionEvent) { + val storedData = PlatformSyncService.getInstance().getReviewerUsername() + e.presentation.text = if (storedData.isNullOrEmpty()) "Input Your Username" else "Reviewer Username: $storedData" + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT +} diff --git a/src/main/kotlin/com/platfom/sync/service/ReviewerIdentifierService.kt b/src/main/kotlin/com/platfom/sync/service/ReviewerIdentifierService.kt new file mode 100644 index 0000000..af6a025 --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/service/ReviewerIdentifierService.kt @@ -0,0 +1,58 @@ +package com.platfom.sync.service + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@State(name = "PlatformSyncService", storages = [Storage("PlatformSyncService.xml")]) +class PlatformSyncService : PersistentStateComponent { + + private val listeners = mutableListOf<() -> Unit>() + + data class State(var reviewerUserName: String? = null, var platformSyncStatus: PlatformSyncStatus = PlatformSyncStatus.FAILED_TO_CONNECT) + + private var state = State() + + override fun getState(): State = state + + override fun loadState(state: State) { + this.state = state + } + + fun saveReviewerUsername(username: String) { + state.reviewerUserName = username + notifyListeners() + } + + fun getReviewerUsername(): String? = state.reviewerUserName + + fun savePlatformSyncStatus(status: PlatformSyncStatus) { + state.platformSyncStatus = status + notifyListeners() + } + + fun getPlatformSyncStatus(): PlatformSyncStatus = state.platformSyncStatus + + fun addChangeListener(listener: () -> Unit) { + listeners.add(listener) + } + + fun removeChangeListener(listener: () -> Unit) { + listeners.remove(listener) + } + + private fun notifyListeners() { + listeners.forEach { it() } + } + + companion object { + fun getInstance(): PlatformSyncService = service() + } +} + +enum class PlatformSyncStatus(val description: String) { + CONNECTED("Connected"), + DISCONNECTED("Disconnected"), + FAILED_TO_CONNECT("Failed to Connect") +} \ No newline at end of file diff --git a/src/main/kotlin/com/platfom/sync/service/WebSocketService.kt b/src/main/kotlin/com/platfom/sync/service/WebSocketService.kt new file mode 100644 index 0000000..f562672 --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/service/WebSocketService.kt @@ -0,0 +1,92 @@ +package com.platfom.sync.service + +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ServerHandshake +import java.net.URI + +@Service +class WebSocketService { + private var webSocketClient: WebSocks? = null + private val platformSyncService = PlatformSyncService.getInstance() + private val listeners = mutableListOf<() -> Unit>() + + fun connect(): Boolean { + val username = platformSyncService.getReviewerUsername() + if (username.isNullOrEmpty()) { + showNotification("Username not set", "Please set your reviewer username before connecting", NotificationType.WARNING) + return false + } + + disconnect() + webSocketClient = WebSocks(platformSyncService, URI("wss://platform-sync-websocket.onrender.com")).apply { + connect() + } + notifyListeners() + return true + } + + private fun showNotification(title: String, content: String, type: NotificationType) { + NotificationGroupManager.getInstance() + .getNotificationGroup("Platform Sync Notification") + .createNotification(title, content, type) + .notify(null) + } + + fun disconnect() { + webSocketClient?.close() + webSocketClient = null + notifyListeners() + } + + fun sendMessage(message: String) { + if (webSocketClient?.isOpen == true) { + webSocketClient?.send(message) + } + } + + fun isConnected() = webSocketClient?.isOpen == true + + fun addChangeListener(listener: () -> Unit) { + listeners.add(listener) + } + + fun removeChangeListener(listener: () -> Unit) { + listeners.remove(listener) + } + + private fun notifyListeners() { + listeners.forEach { it() } + } + + private inner class WebSocks(private val platformSyncService: PlatformSyncService, uri: URI) : WebSocketClient(uri) { + override fun onOpen(handshakedata: ServerHandshake?) { + platformSyncService.savePlatformSyncStatus(PlatformSyncStatus.CONNECTED) + println("connected, ready to observe") + notifyListeners() + } + + override fun onMessage(message: String?) { + println("incoming $message") + } + + override fun onClose(code: Int, reason: String?, remote: Boolean) { + platformSyncService.savePlatformSyncStatus(PlatformSyncStatus.DISCONNECTED) + println("connection close") + notifyListeners() + } + + override fun onError(ex: Exception?) { + platformSyncService.savePlatformSyncStatus(PlatformSyncStatus.FAILED_TO_CONNECT) + println("connection error") + notifyListeners() + } + } + + companion object { + fun getInstance(): WebSocketService = service() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidget.kt b/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidget.kt new file mode 100644 index 0000000..a43b84b --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidget.kt @@ -0,0 +1,93 @@ +package com.platfom.sync.widget + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.ListPopup +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.impl.status.EditorBasedWidget +import com.intellij.ui.popup.list.ListPopupImpl +import com.platfom.sync.action.ReviewerUsernameInputAction +import com.platfom.sync.service.PlatformSyncService +import com.platfom.sync.service.WebSocketService + +class PlatformSyncStatusBarWidget(project: Project) : EditorBasedWidget(project), + StatusBarWidget.MultipleTextValuesPresentation { + private val webSocketService = WebSocketService.getInstance() + private var currentPopup: ListPopupImpl? = null + + override fun getTooltipText(): String { + val service = PlatformSyncService.getInstance() + val username = service.getReviewerUsername() ?: "Not Set" + val status = service.getPlatformSyncStatus() + return "Platform Sync | Reviewer: $username | Status: ${status.description}" + } + + override fun getSelectedValue(): String { + val service = PlatformSyncService.getInstance() + val username = service.getReviewerUsername()?.let { "($it)" } ?: "(Not Set)" + val status = service.getPlatformSyncStatus() + return "Platform Sync $username | ${status.description}" + } + + override fun getPopup(): ListPopup { + val isConnected = webSocketService.isConnected() + val menuItems = mutableListOf(MenuItem("Set Username", true)) + menuItems.add(MenuItem(if (isConnected) "Disconnect" else "Reconnect", true)) + + val step = object : BaseListPopupStep("Platform Sync Actions", menuItems) { + override fun onChosen(selectedValue: MenuItem?, finalChoice: Boolean): PopupStep<*>? { + currentPopup?.cancel() + + ApplicationManager.getApplication().invokeLater { + when (selectedValue?.text) { + "Set Username" -> { + ReviewerUsernameInputAction().actionPerformed( + com.intellij.openapi.actionSystem.AnActionEvent.createFromAnAction( + ReviewerUsernameInputAction(), + null, + "StatusBarWidget", + com.intellij.openapi.actionSystem.DataContext.EMPTY_CONTEXT + ) + ) + } + + "Disconnect" -> webSocketService.disconnect() + "Reconnect" -> webSocketService.connect() + } + } + return PopupStep.FINAL_CHOICE + } + + override fun getTextFor(value: MenuItem): String = value.text + } + + return ListPopupImpl(step).also { + currentPopup = it + } + } + + private data class MenuItem(val text: String, val enabled: Boolean) + + override fun ID(): String = "PlatformSync" + + override fun getPresentation() = this + + override fun install(statusBar: com.intellij.openapi.wm.StatusBar) { + super.install(statusBar) + PlatformSyncService.getInstance().addChangeListener { + statusBar.updateWidget(ID()) + } + WebSocketService.getInstance().addChangeListener { + statusBar.updateWidget(ID()) + } + } + + override fun dispose() { + super.dispose() + currentPopup?.cancel() + PlatformSyncService.getInstance().removeChangeListener { statusBar?.updateWidget(ID()) } + WebSocketService.getInstance().removeChangeListener { statusBar?.updateWidget(ID()) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidgetFactory.kt b/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidgetFactory.kt new file mode 100644 index 0000000..a7b1ed1 --- /dev/null +++ b/src/main/kotlin/com/platfom/sync/widget/PlatformSyncStatusBarWidgetFactory.kt @@ -0,0 +1,21 @@ +package com.platfom.sync.widget + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory + +class PlatformSyncStatusBarWidgetFactory : StatusBarWidgetFactory { + override fun getId(): String = "PlatformSync" + + override fun getDisplayName(): String = "Platform Sync" + + override fun isAvailable(project: Project): Boolean = true + + override fun createWidget(project: Project): StatusBarWidget = PlatformSyncStatusBarWidget(project) + + override fun disposeWidget(widget: StatusBarWidget) { + // No special cleanup needed + } + + override fun canBeEnabledOn(statusBar: com.intellij.openapi.wm.StatusBar): Boolean = true +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 316a292..2fe9560 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -11,7 +11,29 @@ topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/> - + + + + + + + + + + + + + 0.5.5