From 203949bb761147a736eb75ab6b248fb98aab145b Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Wed, 9 Aug 2023 13:55:24 +0800 Subject: [PATCH] feat(intellij): support auth. (#341) --- .../intellijtabby/actions/OpenAuthPage.kt | 54 ++++++++++++++ .../com/tabbyml/intellijtabby/agent/Agent.kt | 18 +++++ .../intellijtabby/agent/AgentService.kt | 72 ++++++++++++++++++- .../status/StatusBarWidgetFactory.kt | 3 +- .../src/main/resources/META-INF/plugin.xml | 13 ++++ .../src/main/resources/strings.properties | 2 + 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt create mode 100644 clients/intellij/src/main/resources/strings.properties diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt new file mode 100644 index 0000000..c3d2d21 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenAuthPage.kt @@ -0,0 +1,54 @@ +package com.tabbyml.intellijtabby.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.tabbyml.intellijtabby.agent.Agent +import com.tabbyml.intellijtabby.agent.AgentService +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +open class OpenAuthPage : AnAction() { + private val logger = Logger.getInstance(OpenAuthPage::class.java) + + override fun actionPerformed(e: AnActionEvent) { + val task = object : Task.Modal( + e.project, + "Tabby Server Authorization", + true + ) { + lateinit var job: Job + override fun run(indicator: ProgressIndicator) { + val agentService = service() + job = agentService.scope.launch { + agentService.requestAuth(indicator) + } + logger.info("Authorization task started.") + while (job.isActive) { + indicator.checkCanceled() + Thread.sleep(100) + } + } + + override fun onCancel() { + logger.info("Authorization task cancelled.") + job.cancel() + } + } + ProgressManager.getInstance().run(task) + } + + override fun update(e: AnActionEvent) { + val agentService = service() + e.presentation.isVisible = agentService.status.value == Agent.Status.UNAUTHORIZED + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt index e7d0fc2..a1d21f6 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt @@ -16,7 +16,9 @@ import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.util.Key import com.intellij.util.EnvironmentUtil import com.intellij.util.io.BaseOutputReader +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.suspendCancellableCoroutine import java.io.OutputStreamWriter @@ -36,6 +38,8 @@ class Agent : ProcessAdapter() { private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED) val status = statusFlow.asStateFlow() + private val authRequiredEventFlow = MutableSharedFlow(extraBufferCapacity = 1) + val authRequiredEvent = authRequiredEventFlow.asSharedFlow() init { logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}") @@ -147,6 +151,19 @@ class Agent : ProcessAdapter() { return request("postEvent", listOf(event)) } + data class AuthUrlResponse( + val authUrl: String, + val code: String, + ) + + suspend fun requestAuthUrl(): AuthUrlResponse? { + return request("requestAuthUrl", listOf()) + } + + suspend fun waitForAuthToken(code: String) { + return request("waitForAuthToken", listOf(code)) + } + fun close() { streamWriter.close() process.killProcess() @@ -237,6 +254,7 @@ class Agent : ProcessAdapter() { "authRequired" -> { logger.info("Agent notification $event") + authRequiredEventFlow.tryEmit(Unit) } else -> { diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt index 18eae99..bf457d7 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt @@ -1,18 +1,28 @@ package com.tabbyml.intellijtabby.agent +import com.intellij.ide.BrowserUtil import com.intellij.lang.Language +import com.intellij.notification.Notification +import com.intellij.notification.NotificationType +import com.intellij.notification.Notifications import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor +import com.intellij.openapi.progress.ProgressIndicator import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile +import com.tabbyml.intellijtabby.actions.OpenAuthPage import com.tabbyml.intellijtabby.settings.ApplicationSettingsState import io.ktor.util.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -40,6 +50,32 @@ class AgentService : Disposable { updateConfig(createAgentConfig(it)) } } + + scope.launch { + logger.info("Add authRequired event listener.") + agent.authRequiredEvent.collect { + logger.info("Will show auth required notification.") + val notification = Notification( + "com.tabbyml.intellijtabby.notification.warning", + "Authorization required for Tabby server", + NotificationType.WARNING, + ) + notification.addAction(object : OpenAuthPage() { + init { + getTemplatePresentation().text = "Open Authorization Page..." + getTemplatePresentation().description = "Open the authorization web page in your web browser." + } + + override fun actionPerformed(e: AnActionEvent) { + notification.expire() + super.actionPerformed(e) + } + }) + invokeLater { + Notifications.Bus.notify(notification) + } + } + } } private fun createAgentConfig(state: ApplicationSettingsState.State): Agent.Config { @@ -65,7 +101,7 @@ class AgentService : Disposable { agent.status.first { it != Agent.Status.NOT_INITIALIZED } } - suspend fun updateConfig(config: Agent.Config) { + private suspend fun updateConfig(config: Agent.Config) { waitForInitialized() agent.updateConfig(config) } @@ -93,6 +129,40 @@ class AgentService : Disposable { agent.postEvent(event) } + suspend fun requestAuth(progress: ProgressIndicator) { + waitForInitialized() + progress.isIndeterminate = true + progress.text = "Generating authorization url..." + val authUrlResponse = agent.requestAuthUrl() + val notification = if (authUrlResponse != null) { + BrowserUtil.browse(authUrlResponse.authUrl) + progress.text = "Waiting for authorization from browser..." + agent.waitForAuthToken(authUrlResponse.code) + if (status.value == Agent.Status.READY) { + Notification( + "com.tabbyml.intellijtabby.notification.info", + "Congrats, you're authorized, start to use Tabby now.", + NotificationType.INFORMATION + ) + } else { + Notification( + "com.tabbyml.intellijtabby.notification.warning", + "Connection error, please check settings and try again.", + NotificationType.WARNING + ) + } + } else { + Notification( + "com.tabbyml.intellijtabby.notification.info", + "You are already authorized.", + NotificationType.INFORMATION + ) + } + invokeLater { + Notifications.Bus.notify(notification) + } + } + override fun dispose() { agent.close() } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt index f9755a4..3ec0af0 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/status/StatusBarWidgetFactory.kt @@ -72,6 +72,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { override fun getChildren(e: AnActionEvent?): Array { val actionManager = ActionManager.getInstance() return arrayOf( + actionManager.getAction("Tabby.OpenAuthPage"), actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"), actionManager.getAction("Tabby.OpenSettings"), ) @@ -103,7 +104,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { tooltip = "Tabby: Cannot connect to Server" } Agent.Status.UNAUTHORIZED -> { - icon = AllIcons.General.Error + icon = AllIcons.General.Warning tooltip = "Tabby: Requires authorization" } } diff --git a/clients/intellij/src/main/resources/META-INF/plugin.xml b/clients/intellij/src/main/resources/META-INF/plugin.xml index 9d1af8b..1c7eeea 100644 --- a/clients/intellij/src/main/resources/META-INF/plugin.xml +++ b/clients/intellij/src/main/resources/META-INF/plugin.xml @@ -47,6 +47,14 @@ nonDefaultProject="true"/> + + @@ -71,6 +79,11 @@ + + diff --git a/clients/intellij/src/main/resources/strings.properties b/clients/intellij/src/main/resources/strings.properties new file mode 100644 index 0000000..d4dada1 --- /dev/null +++ b/clients/intellij/src/main/resources/strings.properties @@ -0,0 +1,2 @@ +tabby.info=Tabby notification +tabby.warning=Tabby warnings \ No newline at end of file