feat(intellij): add statusbar and settings (#333)

* fix: intellij job coroutines and cancellation.

* feat: intellij plugin add settings.

* fix: intellij plugin language id map.

* fix: intellij log completion events.

* feat(intellij): add status bar.

* docs: update docs for intellij plugin.
release-0.0
Zhiming Ma 2023-08-04 12:02:32 +08:00 committed by GitHub
parent 4eaae27ed3
commit 20e9788f29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 672 additions and 114 deletions

View File

@ -5,7 +5,7 @@ plugins {
}
group = "com.tabbyml"
version = "0.0.1-SNAPSHOT"
version = "0.0.1"
repositories {
mavenCentral()
@ -41,7 +41,7 @@ tasks {
into("build/idea-sandbox/plugins/intellij-tabby/node_scripts")
}
buildPlugin {
buildSearchableOptions {
dependsOn(copyNodeScripts)
}
@ -57,5 +57,6 @@ tasks {
publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
channels.set(listOf("alpha"))
}
}

View File

@ -0,0 +1,12 @@
package com.tabbyml.intellijtabby.actions
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.ShowSettingsUtil
import com.tabbyml.intellijtabby.settings.ApplicationConfigurable
class OpenSettings: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, ApplicationConfigurable::class.java)
}
}

View File

@ -0,0 +1,29 @@
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.tabbyml.intellijtabby.settings.ApplicationSettingsState
class ToggleAutoCompletionEnabled : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
settings.isAutoCompletionEnabled = !settings.isAutoCompletionEnabled
}
override fun update(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
if (settings.isAutoCompletionEnabled) {
e.presentation.text = "Disable Auto Completion"
e.presentation.description = "Tabby does not show completion suggestions automatically, you can still request them on demand."
} else {
e.presentation.text = "Enable Auto Completion"
e.presentation.description = "Tabby shows inline completion suggestions automatically."
}
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View File

@ -5,21 +5,15 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.components.service
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.editor.InlineCompletionService
import com.tabbyml.intellijtabby.editor.CompletionScheduler
class TriggerCompletion : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val agentService = service<AgentService>()
val inlineCompletionService = service<InlineCompletionService>()
val completionScheduler = service<CompletionScheduler>()
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
val offset = editor.caretModel.primaryCaret.offset
inlineCompletionService.dismiss()
agentService.getCompletion(editor, offset)?.thenAccept {
inlineCompletionService.show(editor, offset, it)
}
completionScheduler.schedule(editor, offset, triggerDelay = 0, manually = true)
}
override fun update(e: AnActionEvent) {

View File

@ -1,6 +1,7 @@
package com.tabbyml.intellijtabby.agent
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
@ -9,13 +10,16 @@ import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.diagnostic.Logger
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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.OutputStreamWriter
import java.util.concurrent.CompletableFuture
class Agent : ProcessAdapter() {
private val logger = Logger.getInstance(Agent::class.java)
@ -23,11 +27,17 @@ class Agent : ProcessAdapter() {
private val process: KillableProcessHandler
private val streamWriter: OutputStreamWriter
var status = "notInitialized"
private set
enum class Status {
NOT_INITIALIZED,
READY,
DISCONNECTED,
UNAUTHORIZED,
}
private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED)
val status = statusFlow.asStateFlow()
init {
logger.info("Agent init.")
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
@ -49,7 +59,7 @@ class Agent : ProcessAdapter() {
}
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
process = object: KillableProcessHandler(cmd) {
process = object : KillableProcessHandler(cmd) {
override fun readerOptions(): BaseOutputReader.Options {
return BaseOutputReader.Options.forMostlySilentProcess()
}
@ -59,12 +69,46 @@ class Agent : ProcessAdapter() {
streamWriter = process.processInput.writer()
}
fun initialize(): CompletableFuture<Boolean> {
return request("initialize", listOf(mapOf("client" to "intellij-tabby")))
data class Config(
val server: Server? = null,
val completion: Completion? = null,
val logs: Logs? = null,
val anonymousUsageTracking: AnonymousUsageTracking? = null,
) {
data class Server(
val endpoint: String,
)
data class Completion(
val maxPrefixLines: Int,
val maxSuffixLines: Int,
)
data class Logs(
val level: String,
)
data class AnonymousUsageTracking(
val disabled: Boolean,
)
}
fun updateConfig(): CompletableFuture<Boolean> {
return request("updateConfig", listOf(emptyMap<Any, Any>()))
suspend fun initialize(config: Config): Boolean {
val appInfo = ApplicationInfo.getInstance().fullApplicationName
val pluginId = "com.tabbyml.intellij-tabby"
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
return request(
"initialize", listOf(
mapOf(
"config" to config,
"client" to "$appInfo $pluginId $pluginVersion",
)
)
)
}
suspend fun updateConfig(config: Config): Boolean {
return request("updateConfig", listOf(config))
}
data class CompletionRequest(
@ -84,28 +128,60 @@ class Agent : ProcessAdapter() {
)
}
fun getCompletions(request: CompletionRequest): CompletableFuture<CompletionResponse?> {
suspend fun getCompletions(request: CompletionRequest): CompletionResponse? {
return request("getCompletions", listOf(request))
}
data class LogEventRequest(
val type: EventType,
@SerializedName("completion_id") val completionId: String,
@SerializedName("choice_index") val choiceIndex: Int,
) {
enum class EventType {
@SerializedName("view") VIEW,
@SerializedName("select") SELECT,
}
}
suspend fun postEvent(event: LogEventRequest): Boolean {
return request("postEvent", listOf(event))
}
fun close() {
streamWriter.close()
process.destroyProcess()
}
private var requestId = 1
private var ongoingRequest = mutableMapOf<Int, (response: String) -> Unit>()
private inline fun <reified T : Any?> request(func: String, args: List<Any> = emptyList()): CompletableFuture<T> {
val id = requestId++
val data = listOf(id, mapOf("func" to func, "args" to args))
val json = gson.toJson(data)
streamWriter.write(json + "\n")
streamWriter.flush()
logger.info("Agent request: $json")
val future = CompletableFuture<T>()
ongoingRequest[id] = { response ->
logger.info("Agent response: $response")
val result = gson.fromJson<T>(response, object : TypeToken<T>() {}.type)
future.complete(result)
private suspend inline fun <reified T : Any?> request(func: String, args: List<Any> = emptyList()): T =
suspendCancellableCoroutine { continuation ->
val id = requestId++
ongoingRequest[id] = { response ->
logger.info("Agent response: $response")
val result = gson.fromJson<T>(response, object : TypeToken<T>() {}.type)
continuation.resumeWith(Result.success(result))
}
val data = listOf(id, mapOf("func" to func, "args" to args))
val json = gson.toJson(data)
logger.info("Agent request: $json")
streamWriter.write(json + "\n")
streamWriter.flush()
continuation.invokeOnCancellation {
logger.info("Agent request cancelled")
val cancellationId = requestId++
ongoingRequest[cancellationId] = { response ->
logger.info("Agent cancellation response: $response")
}
val cancellationData = listOf(cancellationId, mapOf("func" to "cancelRequest", "args" to listOf(id)))
val cancellationJson = gson.toJson(cancellationData)
logger.info("Agent cancellation request: $cancellationJson")
streamWriter.write(cancellationJson + "\n")
streamWriter.flush()
}
}
return future
}
private var outputBuffer: String = ""
@ -131,7 +207,9 @@ class Agent : ProcessAdapter() {
logger.info("Parsed agent output: $data")
val id = (data[0] as Number).toInt()
if (id == 0) {
handleNotification(gson.toJson(data[1]))
if (data[1] is Map<*, *>) {
handleNotification(data[1] as Map<*, *>)
}
} else {
ongoingRequest[id]?.let { callback ->
callback(gson.toJson(data[1]))
@ -140,7 +218,30 @@ class Agent : ProcessAdapter() {
}
}
private fun handleNotification(event: String) {
logger.info("Agent notification: $event")
private fun handleNotification(event: Map<*, *>) {
when (event["event"]) {
"statusChanged" -> {
logger.info("Agent notification $event")
statusFlow.value = when (event["status"]) {
"notInitialized" -> Status.NOT_INITIALIZED
"ready" -> Status.READY
"disconnected" -> Status.DISCONNECTED
"unauthorized" -> Status.UNAUTHORIZED
else -> Status.NOT_INITIALIZED
}
}
"configUpdated" -> {
logger.info("Agent notification $event")
}
"authRequired" -> {
logger.info("Agent notification $event")
}
else -> {
logger.error("Agent notification, unknown event name: ${event["event"]}")
}
}
}
}

View File

@ -1,44 +1,145 @@
package com.tabbyml.intellijtabby.agent
import com.intellij.lang.Language
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ReadAction
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.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import java.util.concurrent.CompletableFuture
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Service
class AgentService {
class AgentService : Disposable {
private val logger = Logger.getInstance(AgentService::class.java)
private val agent: CompletableFuture<Agent?> = CompletableFuture<Agent?>()
private var agent: Agent = Agent()
val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
val status get() = agent.status
init {
try {
val instance = Agent()
instance.initialize().thenApply {
logger.info("Agent init done: $it")
agent.complete(instance)
val settings = service<ApplicationSettingsState>()
scope.launch {
try {
agent.initialize(createAgentConfig(settings.data))
logger.info("Agent init done.")
} catch (e: Error) {
logger.error("Agent init failed: $e")
}
}
scope.launch {
settings.state.collect {
updateConfig(createAgentConfig(it))
}
} catch (_: Error) {
agent.complete(null)
}
}
fun getCompletion(editor: Editor, offset: Int): CompletableFuture<Agent.CompletionResponse>? {
return agent.thenCompose {agent ->
ReadAction.compute<PsiFile, Throwable> {
editor.project?.let { project ->
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
}
}?.let { file ->
agent?.getCompletions(Agent.CompletionRequest(
private fun createAgentConfig(state: ApplicationSettingsState.State): Agent.Config {
return Agent.Config(
server = if (state.serverEndpoint.isNotBlank()) {
Agent.Config.Server(
endpoint = state.serverEndpoint,
)
} else {
null
},
anonymousUsageTracking = if (state.isAnonymousUsageTrackingDisabled) {
Agent.Config.AnonymousUsageTracking(
disabled = true,
)
} else {
null
},
)
}
private suspend fun waitForInitialized() {
agent.status.first { it != Agent.Status.NOT_INITIALIZED }
}
suspend fun updateConfig(config: Agent.Config) {
waitForInitialized()
agent.updateConfig(config)
}
suspend fun getCompletion(editor: Editor, offset: Int): Agent.CompletionResponse? {
waitForInitialized()
return ReadAction.compute<PsiFile, Throwable> {
editor.project?.let { project ->
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
}
}?.let { file ->
agent.getCompletions(
Agent.CompletionRequest(
file.virtualFile.path,
file.language.id, // FIXME: map language id
file.getLanguageId(),
editor.document.text,
offset
))
}
)
)
}
}
suspend fun postEvent(event: Agent.LogEventRequest) {
waitForInitialized()
agent.postEvent(event)
}
override fun dispose() {
agent.close()
}
companion object {
// Language id: https://code.visualstudio.com/docs/languages/identifiers
private fun PsiFile.getLanguageId(): String {
if (this.language != Language.ANY
&& this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf("txt", "text", "textmate")
) {
if (languageIdMap.containsKey(this.language.id)) {
return languageIdMap[this.language.id]!!
}
return this.language.id.toLowerCasePreservingASCIIRules()
.replace("#", "sharp")
.replace("++", "pp")
.replace(" ", "")
}
return if (filetypeMap.containsKey(this.fileType.defaultExtension)) {
filetypeMap[this.fileType.defaultExtension]!!
} else {
this.fileType.defaultExtension.toLowerCasePreservingASCIIRules()
}
}
private val languageIdMap = mapOf(
"ObjectiveC" to "objective-c",
"ObjectiveC++" to "objective-cpp",
)
private val filetypeMap = mapOf(
"py" to "python",
"js" to "javascript",
"cjs" to "javascript",
"mjs" to "javascript",
"jsx" to "javascriptreact",
"ts" to "typescript",
"tsx" to "typescriptreact",
"kt" to "kotlin",
"md" to "markdown",
"cc" to "cpp",
"cs" to "csharp",
"m" to "objective-c",
"mm" to "objective-cpp",
"sh" to "shellscript",
"zsh" to "shellscript",
"bash" to "shellscript",
"txt" to "plaintext",
)
}
}

View File

@ -4,59 +4,50 @@ 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.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.tabbyml.intellijtabby.agent.AgentService
import java.util.*
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Service
class CompletionScheduler {
private val logger = Logger.getInstance(CompletionScheduler::class.java)
data class CompletionContext(val editor: Editor, val offset: Int, val timer: Timer)
data class CompletionContext(val editor: Editor, val offset: Int, val job: Job)
private var project: Project? = null
var scheduled: CompletionContext? = null
private set
fun schedule(editor: Editor, offset: Int) {
clear()
fun schedule(editor: Editor, offset: Int, triggerDelay: Long = 150, manually: Boolean = false) {
val agentService = service<AgentService>()
val inlineCompletionService = service<InlineCompletionService>()
inlineCompletionService.dismiss()
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
logger.info("Scheduled completion task running")
agentService.getCompletion(editor, offset)?.thenAccept {
inlineCompletionService.show(editor, offset, it)
}
val settings = service<ApplicationSettingsState>()
clear()
val job = agentService.scope.launch {
if (!manually && !settings.isAutoCompletionEnabled) {
return@launch
}
}, 150)
scheduled = CompletionContext(editor, offset, timer)
logger.info("Schedule completion at $offset after $triggerDelay ms.")
if (project != editor.project) {
project = editor.project
editor.project?.messageBus?.connect()?.subscribe(
FileEditorManagerListener.FILE_EDITOR_MANAGER,
object: FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
logger.info("FileEditorManagerListener selectionChanged.")
clear()
}
}
)
delay(triggerDelay)
if (!manually && !settings.isAutoCompletionEnabled) {
return@launch
}
logger.info("Trigger completion at $offset")
agentService.getCompletion(editor, offset)?.let {
inlineCompletionService.show(editor, offset, it)
}
}
scheduled = CompletionContext(editor, offset, job)
}
fun clear() {
scheduled?.let {
it.timer.cancel()
scheduled = null
}
val inlineCompletionService = service<InlineCompletionService>()
inlineCompletionService.dismiss()
scheduled?.let {
it.job.cancel()
scheduled = null
}
}
}

View File

@ -2,11 +2,16 @@ package com.tabbyml.intellijtabby.editor
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.*
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.util.messages.MessageBusConnection
class EditorListener : EditorFactoryListener {
private val logger = Logger.getInstance(EditorListener::class.java)
private val messagesConnection = mutableMapOf<Editor, MessageBusConnection>()
override fun editorCreated(event: EditorFactoryEvent) {
val editor = event.editor
@ -33,5 +38,25 @@ class EditorListener : EditorFactoryListener {
}
}
})
editor.project?.messageBus?.connect()?.let {
it.subscribe(
FileEditorManagerListener.FILE_EDITOR_MANAGER,
object: FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
logger.info("FileEditorManagerListener selectionChanged.")
completionScheduler.clear()
}
}
)
messagesConnection[editor] = it
}
}
override fun editorReleased(event: EditorFactoryEvent) {
messagesConnection[event.editor]?.let {
it.disconnect()
it.dispose()
}
}
}

View File

@ -3,6 +3,7 @@ package com.tabbyml.intellijtabby.editor
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.command.WriteCommandAction
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.editor.EditorCustomElementRenderer
@ -11,6 +12,8 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.ui.JBColor
import com.tabbyml.intellijtabby.agent.Agent
import com.tabbyml.intellijtabby.agent.AgentService
import kotlinx.coroutines.launch
import java.awt.Graphics
import java.awt.Rectangle
@ -19,7 +22,13 @@ import java.awt.Rectangle
class InlineCompletionService {
private val logger = Logger.getInstance(InlineCompletionService::class.java)
data class InlineCompletion(val editor: Editor, val text: String, val offset: Int, val inlays: List<Inlay<*>>)
data class InlineCompletion(
val editor: Editor,
val offset: Int,
val completion: Agent.CompletionResponse,
val text: String,
val inlays: List<Inlay<*>>,
)
var shownInlineCompletion: InlineCompletion? = null
private set
@ -30,13 +39,24 @@ class InlineCompletionService {
return
}
invokeLater {
// FIXME: support multiple choices
val text = completion.choices.first().text
logger.info("Showing inline completion at $offset: $text")
val lines = text.split("\n")
val inlays = lines
.mapIndexed { index, line -> createInlayLine(editor, offset, line, index) }
.filterNotNull()
shownInlineCompletion = InlineCompletion(editor, text, offset, inlays)
shownInlineCompletion = InlineCompletion(editor, offset, completion, text, inlays)
}
val agentService = service<AgentService>()
agentService.scope.launch {
agentService.postEvent(
Agent.LogEventRequest(
type = Agent.LogEventRequest.EventType.VIEW,
completionId = completion.id,
choiceIndex = completion.choices.first().index
)
)
}
}
@ -50,6 +70,16 @@ class InlineCompletionService {
invokeLater {
it.inlays.forEach(Disposer::dispose)
}
val agentService = service<AgentService>()
agentService.scope.launch {
agentService.postEvent(
Agent.LogEventRequest(
type = Agent.LogEventRequest.EventType.SELECT,
completionId = it.completion.id,
choiceIndex = it.completion.choices.first().index
)
)
}
shownInlineCompletion = null
}
}

View File

@ -0,0 +1,39 @@
package com.tabbyml.intellijtabby.settings
import com.intellij.openapi.components.service
import com.intellij.openapi.options.Configurable
import javax.swing.JComponent
class ApplicationConfigurable : Configurable {
private lateinit var settingsPanel: ApplicationSettingsPanel
override fun getDisplayName(): String {
return "Tabby"
}
override fun createComponent(): JComponent {
settingsPanel = ApplicationSettingsPanel()
return settingsPanel.mainPanel
}
override fun isModified(): Boolean {
val settings = service<ApplicationSettingsState>()
return settingsPanel.isAutoCompletionEnabled != settings.isAutoCompletionEnabled
|| settingsPanel.serverEndpoint != settings.serverEndpoint
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
}
override fun apply() {
val settings = service<ApplicationSettingsState>()
settings.isAutoCompletionEnabled = settingsPanel.isAutoCompletionEnabled
settings.serverEndpoint = settingsPanel.serverEndpoint
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
}
override fun reset() {
val settings = service<ApplicationSettingsState>()
settingsPanel.isAutoCompletionEnabled = settings.isAutoCompletionEnabled
settingsPanel.serverEndpoint = settings.serverEndpoint
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
}
}

View File

@ -0,0 +1,41 @@
package com.tabbyml.intellijtabby.settings
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import javax.swing.JPanel
class ApplicationSettingsPanel {
private val isAutoCompletionEnabledCheckBox = JBCheckBox("Enable auto completion")
private val serverEndpointTextField = JBTextField()
private val isAnonymousUsageTrackingDisabledCheckBox = JBCheckBox("Disable anonymous usage tracking")
val mainPanel: JPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Server endpoint", serverEndpointTextField, 1, false)
.addTooltip("A http or https URL of Tabby server endpoint.")
.addTooltip("If leave empty, server endpoint config in `~/.tabby/agent/config.toml` will be used")
.addTooltip("Default to 'http://localhost:8080'.")
.addSeparator()
.addComponent(isAutoCompletionEnabledCheckBox, 1)
.addComponent(isAnonymousUsageTrackingDisabledCheckBox, 1)
.addComponentFillVertically(JPanel(), 0)
.panel
var isAutoCompletionEnabled: Boolean
get() = isAutoCompletionEnabledCheckBox.isSelected
set(value) {
isAutoCompletionEnabledCheckBox.isSelected = value
}
var serverEndpoint: String
get() = serverEndpointTextField.text
set(value) {
serverEndpointTextField.text = value
}
var isAnonymousUsageTrackingDisabled: Boolean
get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected
set(value) {
isAnonymousUsageTrackingDisabledCheckBox.isSelected = value
}
}

View File

@ -0,0 +1,56 @@
package com.tabbyml.intellijtabby.settings
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@Service
@State(
name = "com.tabbyml.intellijtabby.settings.ApplicationSettingsState",
storages = [Storage("intellij-tabby.xml")]
)
class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsState> {
var isAutoCompletionEnabled: Boolean = true
set(value) {
field = value
stateFlow.value = this.data
}
var serverEndpoint: String = ""
set(value) {
field = value
stateFlow.value = this.data
}
var isAnonymousUsageTrackingDisabled: Boolean = false
set(value) {
field = value
stateFlow.value = this.data
}
data class State(
val isAutoCompletionEnabled: Boolean,
val serverEndpoint: String,
val isAnonymousUsageTrackingDisabled: Boolean,
)
val data: State
get() = State(
isAutoCompletionEnabled = isAutoCompletionEnabled,
serverEndpoint = serverEndpoint,
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
)
private val stateFlow = MutableStateFlow(data)
val state = stateFlow.asStateFlow()
override fun getState(): ApplicationSettingsState {
return this
}
override fun loadState(state: ApplicationSettingsState) {
XmlSerializerUtil.copyBean(state, this)
}
}

View File

@ -0,0 +1,121 @@
package com.tabbyml.intellijtabby.status
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.ListPopup
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup
import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory
import com.tabbyml.intellijtabby.agent.Agent
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
override fun getId(): String {
return StatusBarWidgetFactory::class.java.name
}
override fun getDisplayName(): String {
return "Tabby"
}
override fun createWidget(project: Project): StatusBarWidget {
return object : EditorBasedStatusBarPopup(project, false) {
val scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
val text = "Tabby"
var icon = AllIcons.Actions.Refresh
var tooltip = "Tabby: Initializing"
init {
val settings = service<ApplicationSettingsState>()
val agentService = service<AgentService>()
scope.launch {
settings.state.combine(agentService.status) { settings, agentStatus ->
Pair(settings, agentStatus)
}.collect {
updateStatus(it.first, it.second)
}
}
}
override fun ID(): String {
return "${StatusBarWidgetFactory::class.java.name}.widget"
}
override fun createInstance(project: Project): StatusBarWidget {
return createWidget(project)
}
override fun getWidgetState(file: VirtualFile?): WidgetState {
return WidgetState(tooltip, text, true).also {
it.icon = icon
}
}
override fun createPopup(context: DataContext?): ListPopup? {
if (context == null) {
return null
}
return JBPopupFactory.getInstance().createActionGroupPopup(
tooltip,
object : ActionGroup() {
override fun getChildren(e: AnActionEvent?): Array<AnAction> {
val actionManager = ActionManager.getInstance()
return arrayOf(
actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"),
actionManager.getAction("Tabby.OpenSettings"),
)
}
},
context,
false,
null,
10,
)
}
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Agent.Status) {
if (!settingsState.isAutoCompletionEnabled) {
icon = AllIcons.Windows.CloseSmall
tooltip = "Tabby: Auto completion is disabled"
} else {
when(agentStatus) {
Agent.Status.NOT_INITIALIZED -> {
icon = AllIcons.Actions.Refresh
tooltip = "Tabby: Initializing"
}
Agent.Status.READY -> {
icon = AllIcons.Actions.Checked
tooltip = "Tabby: Ready"
}
Agent.Status.DISCONNECTED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Cannot connect to Server"
}
Agent.Status.UNAUTHORIZED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Requires authorization"
}
}
}
invokeLater {
update { myStatusBar?.updateWidget(ID()) }
}
}
}
}
override fun disposeWidget(widget: StatusBarWidget) {
// Nothing to do
}
}

View File

@ -14,8 +14,21 @@
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
<description><![CDATA[
Early version of Tabby for IntelliJ.<br>
Require Node.js 16.0+ installed and added to PATH.<br>
<h1 id="tabby-plugin-for-intellij-platform">Tabby Plugin for IntelliJ Platform</h1>
<p>Tabby is an AI coding assistant that can suggest multi-line code or full functions in real-time.</p>
<p><strong>Requirements</strong> Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> 16.0+ installed and added into <code>PATH</code> enviroment variable. </p>
<p>For more information, please check out our <a href="https://tabbyml.com/">Website</a> and <a href="https://github.com/TabbyML/tabby">GitHub</a>.
If you encounter any problem or have any suggestion, please <a href="https://github.com/TabbyML/tabby/issues/new">open an issue</a>, or join our <a href="https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA">Slack community</a> for more support!</p>
<h2 id="demo">Demo</h2>
<p>Try our online demo <a href="https://tabbyml.github.io/tabby/playground">here</a>.</p>
<p><img src="https://tabbyml.github.io/tabby/img/demo.gif" alt="Demo"></p>
<h2 id="get-started">Get Started</h2>
<ol>
<li>Please following <a href="https://tabbyml.github.io/tabby/docs/self-hosting/">this guide</a> to setup a self-hosted Tabby server.</li>
<li>Open the settings page <code>Settings &gt; Editor &gt; Tabby</code>, or click the <code>Tabby</code> status bar item and <code>Open Settings...</code>. Fill in the server endpoint URL to connect the plugin to your Tabby server. The status bar item will show a checked icon if the connection is successful.</li>
<li>Once setup is complete, Tabby will provide inline suggestions automatically, and you can accept suggestions by just pressing the <code>Tab</code> key.</li>
<li>You can find more actions and hotkey in the IDE tools menu <code>Code &gt; Tabby</code>.</li>
</ol>
]]></description>
<!-- Product and plugin compatibility requirements.
@ -25,30 +38,46 @@
<!-- Extension points defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
<extensions defaultExtensionNs="com.intellij">
<projectConfigurable
parentId="editor"
instance="com.tabbyml.intellijtabby.settings.ApplicationConfigurable"
id="com.tabbyml.intellijtabby.settings.ApplicationConfigurable"
displayName="Tabby"
nonDefaultProject="true"/>
<editorFactoryListener implementation="com.tabbyml.intellijtabby.editor.EditorListener"/>
<statusBarWidgetFactory implementation="com.tabbyml.intellijtabby.status.StatusBarWidgetFactory"/>
</extensions>
<actions>
<group id="Tabby.ToolsMenu" popup="true" text="Tabby" description="Tabby AI code assistant">
<add-to-group group-id="ToolsMenu" anchor="last"/>
<add-to-group group-id="CodeMenu" anchor="after" relative-to-action="CodeCompletionGroup"/>
<action id="Tabby.TriggerCompletion"
class="com.tabbyml.intellijtabby.actions.TriggerCompletion"
text="Trigger Completion"
description="Trigger completion at current position.">
text="Show Inline Completion"
description="Show inline completion suggestions at the caret's position.">
<keyboard-shortcut first-keystroke="alt BACK_SLASH" keymap="$default"/>
</action>
<action id="Tabby.AcceptCompletion"
class="com.tabbyml.intellijtabby.actions.AcceptCompletion"
text="Accept Completion"
description="Trigger completion at current position.">
description="Accept the shown suggestions and insert the text.">
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
</action>
<action id="Tabby.DismissCompletion"
class="com.tabbyml.intellijtabby.actions.DismissCompletion"
text="Dismiss Completion"
description="Trigger completion at current position.">
description="Hide the shown suggestions.">
<keyboard-shortcut first-keystroke="ESCAPE" keymap="$default"/>
</action>
<separator/>
<action id="Tabby.ToggleAutoCompletionEnabled"
class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled">
</action>
<action id="Tabby.OpenSettings"
class="com.tabbyml.intellijtabby.actions.OpenSettings"
text="Open Settings..."
description="Show settings for Tabby.">
</action>
</group>
</actions>
</idea-plugin>

View File

@ -1,12 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.0845 7.94025V4H24.0203V7.9896H16.029V4H7.91553V7.94025H4V36H16.0044V32.0045C16.0058 30.9457 16.4274 29.9308 17.1766 29.1826C17.9258 28.4345 18.9412 28.0143 20 28.0143C21.0588 28.0143 22.0743 28.4345 22.8234 29.1826C23.5726 29.9308 23.9942 30.9457 23.9956 32.0045V36H36V7.94025H32.0845Z"
fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="2.94192" y1="4.89955" x2="37.7772" y2="39.7345"
gradientUnits="userSpaceOnUse">
<stop offset="0.15937" stop-color="#3BEA62"/>
<stop offset="0.5404" stop-color="#3C99CC"/>
<stop offset="0.93739" stop-color="#6B57FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 818 B