feat(intellij): support auth. (#341)

release-0.0
Zhiming Ma 2023-08-09 13:55:24 +08:00 committed by GitHub
parent 220fcc0d65
commit 203949bb76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 2 deletions

View File

@ -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<AgentService>()
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<AgentService>()
e.presentation.isVisible = agentService.status.value == Agent.Status.UNAUTHORIZED
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View File

@ -16,7 +16,9 @@ import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import com.intellij.util.io.BaseOutputReader import com.intellij.util.io.BaseOutputReader
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
@ -36,6 +38,8 @@ class Agent : ProcessAdapter() {
private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED) private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED)
val status = statusFlow.asStateFlow() val status = statusFlow.asStateFlow()
private val authRequiredEventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authRequiredEvent = authRequiredEventFlow.asSharedFlow()
init { init {
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}") logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
@ -147,6 +151,19 @@ class Agent : ProcessAdapter() {
return request("postEvent", listOf(event)) 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() { fun close() {
streamWriter.close() streamWriter.close()
process.killProcess() process.killProcess()
@ -237,6 +254,7 @@ class Agent : ProcessAdapter() {
"authRequired" -> { "authRequired" -> {
logger.info("Agent notification $event") logger.info("Agent notification $event")
authRequiredEventFlow.tryEmit(Unit)
} }
else -> { else -> {

View File

@ -1,18 +1,28 @@
package com.tabbyml.intellijtabby.agent package com.tabbyml.intellijtabby.agent
import com.intellij.ide.BrowserUtil
import com.intellij.lang.Language 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.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ReadAction 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.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.actions.OpenAuthPage
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import io.ktor.util.* import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,6 +50,32 @@ class AgentService : Disposable {
updateConfig(createAgentConfig(it)) 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 { private fun createAgentConfig(state: ApplicationSettingsState.State): Agent.Config {
@ -65,7 +101,7 @@ class AgentService : Disposable {
agent.status.first { it != Agent.Status.NOT_INITIALIZED } agent.status.first { it != Agent.Status.NOT_INITIALIZED }
} }
suspend fun updateConfig(config: Agent.Config) { private suspend fun updateConfig(config: Agent.Config) {
waitForInitialized() waitForInitialized()
agent.updateConfig(config) agent.updateConfig(config)
} }
@ -93,6 +129,40 @@ class AgentService : Disposable {
agent.postEvent(event) 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() { override fun dispose() {
agent.close() agent.close()
} }

View File

@ -72,6 +72,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
override fun getChildren(e: AnActionEvent?): Array<AnAction> { override fun getChildren(e: AnActionEvent?): Array<AnAction> {
val actionManager = ActionManager.getInstance() val actionManager = ActionManager.getInstance()
return arrayOf( return arrayOf(
actionManager.getAction("Tabby.OpenAuthPage"),
actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"), actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"),
actionManager.getAction("Tabby.OpenSettings"), actionManager.getAction("Tabby.OpenSettings"),
) )
@ -103,7 +104,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
tooltip = "Tabby: Cannot connect to Server" tooltip = "Tabby: Cannot connect to Server"
} }
Agent.Status.UNAUTHORIZED -> { Agent.Status.UNAUTHORIZED -> {
icon = AllIcons.General.Error icon = AllIcons.General.Warning
tooltip = "Tabby: Requires authorization" tooltip = "Tabby: Requires authorization"
} }
} }

View File

@ -47,6 +47,14 @@
nonDefaultProject="true"/> nonDefaultProject="true"/>
<editorFactoryListener implementation="com.tabbyml.intellijtabby.editor.EditorListener"/> <editorFactoryListener implementation="com.tabbyml.intellijtabby.editor.EditorListener"/>
<statusBarWidgetFactory implementation="com.tabbyml.intellijtabby.status.StatusBarWidgetFactory"/> <statusBarWidgetFactory implementation="com.tabbyml.intellijtabby.status.StatusBarWidgetFactory"/>
<notificationGroup id="com.tabbyml.intellijtabby.notification.info"
displayType="BALLOON"
bundle="strings"
key="tabby.info"/>
<notificationGroup id="com.tabbyml.intellijtabby.notification.warning"
displayType="STICKY_BALLOON"
bundle="strings"
key="tabby.warning"/>
</extensions> </extensions>
<actions> <actions>
@ -71,6 +79,11 @@
<keyboard-shortcut first-keystroke="ESCAPE" keymap="$default"/> <keyboard-shortcut first-keystroke="ESCAPE" keymap="$default"/>
</action> </action>
<separator/> <separator/>
<action id="Tabby.OpenAuthPage"
class="com.tabbyml.intellijtabby.actions.OpenAuthPage"
text="Open Authorization Page..."
description="Open the authorization web page in your web browser.">
</action>
<action id="Tabby.ToggleAutoCompletionEnabled" <action id="Tabby.ToggleAutoCompletionEnabled"
class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled"> class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled">
</action> </action>

View File

@ -0,0 +1,2 @@
tabby.info=Tabby notification
tabby.warning=Tabby warnings