feat(intellij): support auth. (#341)
parent
220fcc0d65
commit
203949bb76
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
tabby.info=Tabby notification
|
||||||
|
tabby.warning=Tabby warnings
|
||||||
Loading…
Reference in New Issue