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
parent
4eaae27ed3
commit
20e9788f29
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.tabbyml"
|
group = "com.tabbyml"
|
||||||
version = "0.0.1-SNAPSHOT"
|
version = "0.0.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
@ -41,7 +41,7 @@ tasks {
|
||||||
into("build/idea-sandbox/plugins/intellij-tabby/node_scripts")
|
into("build/idea-sandbox/plugins/intellij-tabby/node_scripts")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPlugin {
|
buildSearchableOptions {
|
||||||
dependsOn(copyNodeScripts)
|
dependsOn(copyNodeScripts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,5 +57,6 @@ tasks {
|
||||||
|
|
||||||
publishPlugin {
|
publishPlugin {
|
||||||
token.set(System.getenv("PUBLISH_TOKEN"))
|
token.set(System.getenv("PUBLISH_TOKEN"))
|
||||||
|
channels.set(listOf("alpha"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,21 +5,15 @@ import com.intellij.openapi.actionSystem.AnAction
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
import com.tabbyml.intellijtabby.agent.AgentService
|
import com.tabbyml.intellijtabby.editor.CompletionScheduler
|
||||||
import com.tabbyml.intellijtabby.editor.InlineCompletionService
|
|
||||||
|
|
||||||
|
|
||||||
class TriggerCompletion : AnAction() {
|
class TriggerCompletion : AnAction() {
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
val agentService = service<AgentService>()
|
val completionScheduler = service<CompletionScheduler>()
|
||||||
val inlineCompletionService = service<InlineCompletionService>()
|
|
||||||
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
|
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
|
||||||
val offset = editor.caretModel.primaryCaret.offset
|
val offset = editor.caretModel.primaryCaret.offset
|
||||||
|
completionScheduler.schedule(editor, offset, triggerDelay = 0, manually = true)
|
||||||
inlineCompletionService.dismiss()
|
|
||||||
agentService.getCompletion(editor, offset)?.thenAccept {
|
|
||||||
inlineCompletionService.show(editor, offset, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(e: AnActionEvent) {
|
override fun update(e: AnActionEvent) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tabbyml.intellijtabby.agent
|
package com.tabbyml.intellijtabby.agent
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.intellij.execution.configurations.GeneralCommandLine
|
import com.intellij.execution.configurations.GeneralCommandLine
|
||||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
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.ProcessEvent
|
||||||
import com.intellij.execution.process.ProcessOutputTypes
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
import com.intellij.ide.plugins.PluginManagerCore
|
import com.intellij.ide.plugins.PluginManagerCore
|
||||||
|
import com.intellij.openapi.application.ApplicationInfo
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.extensions.PluginId
|
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.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
class Agent : ProcessAdapter() {
|
class Agent : ProcessAdapter() {
|
||||||
private val logger = Logger.getInstance(Agent::class.java)
|
private val logger = Logger.getInstance(Agent::class.java)
|
||||||
|
|
@ -23,11 +27,17 @@ class Agent : ProcessAdapter() {
|
||||||
private val process: KillableProcessHandler
|
private val process: KillableProcessHandler
|
||||||
private val streamWriter: OutputStreamWriter
|
private val streamWriter: OutputStreamWriter
|
||||||
|
|
||||||
var status = "notInitialized"
|
enum class Status {
|
||||||
private set
|
NOT_INITIALIZED,
|
||||||
|
READY,
|
||||||
|
DISCONNECTED,
|
||||||
|
UNAUTHORIZED,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED)
|
||||||
|
val status = statusFlow.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
logger.info("Agent init.")
|
|
||||||
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
|
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
|
||||||
|
|
||||||
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
|
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
|
||||||
|
|
@ -49,7 +59,7 @@ class Agent : ProcessAdapter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
|
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
|
||||||
process = object: KillableProcessHandler(cmd) {
|
process = object : KillableProcessHandler(cmd) {
|
||||||
override fun readerOptions(): BaseOutputReader.Options {
|
override fun readerOptions(): BaseOutputReader.Options {
|
||||||
return BaseOutputReader.Options.forMostlySilentProcess()
|
return BaseOutputReader.Options.forMostlySilentProcess()
|
||||||
}
|
}
|
||||||
|
|
@ -59,12 +69,46 @@ class Agent : ProcessAdapter() {
|
||||||
streamWriter = process.processInput.writer()
|
streamWriter = process.processInput.writer()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initialize(): CompletableFuture<Boolean> {
|
data class Config(
|
||||||
return request("initialize", listOf(mapOf("client" to "intellij-tabby")))
|
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> {
|
suspend fun initialize(config: Config): Boolean {
|
||||||
return request("updateConfig", listOf(emptyMap<Any, Any>()))
|
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(
|
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))
|
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 requestId = 1
|
||||||
private var ongoingRequest = mutableMapOf<Int, (response: String) -> Unit>()
|
private var ongoingRequest = mutableMapOf<Int, (response: String) -> Unit>()
|
||||||
|
|
||||||
private inline fun <reified T : Any?> request(func: String, args: List<Any> = emptyList()): CompletableFuture<T> {
|
private suspend inline fun <reified T : Any?> request(func: String, args: List<Any> = emptyList()): T =
|
||||||
val id = requestId++
|
suspendCancellableCoroutine { continuation ->
|
||||||
val data = listOf(id, mapOf("func" to func, "args" to args))
|
val id = requestId++
|
||||||
val json = gson.toJson(data)
|
ongoingRequest[id] = { response ->
|
||||||
streamWriter.write(json + "\n")
|
logger.info("Agent response: $response")
|
||||||
streamWriter.flush()
|
val result = gson.fromJson<T>(response, object : TypeToken<T>() {}.type)
|
||||||
logger.info("Agent request: $json")
|
continuation.resumeWith(Result.success(result))
|
||||||
val future = CompletableFuture<T>()
|
}
|
||||||
ongoingRequest[id] = { response ->
|
val data = listOf(id, mapOf("func" to func, "args" to args))
|
||||||
logger.info("Agent response: $response")
|
val json = gson.toJson(data)
|
||||||
val result = gson.fromJson<T>(response, object : TypeToken<T>() {}.type)
|
logger.info("Agent request: $json")
|
||||||
future.complete(result)
|
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 = ""
|
private var outputBuffer: String = ""
|
||||||
|
|
||||||
|
|
@ -131,7 +207,9 @@ class Agent : ProcessAdapter() {
|
||||||
logger.info("Parsed agent output: $data")
|
logger.info("Parsed agent output: $data")
|
||||||
val id = (data[0] as Number).toInt()
|
val id = (data[0] as Number).toInt()
|
||||||
if (id == 0) {
|
if (id == 0) {
|
||||||
handleNotification(gson.toJson(data[1]))
|
if (data[1] is Map<*, *>) {
|
||||||
|
handleNotification(data[1] as Map<*, *>)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ongoingRequest[id]?.let { callback ->
|
ongoingRequest[id]?.let { callback ->
|
||||||
callback(gson.toJson(data[1]))
|
callback(gson.toJson(data[1]))
|
||||||
|
|
@ -140,7 +218,30 @@ class Agent : ProcessAdapter() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotification(event: String) {
|
private fun handleNotification(event: Map<*, *>) {
|
||||||
logger.info("Agent notification: $event")
|
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"]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,44 +1,145 @@
|
||||||
package com.tabbyml.intellijtabby.agent
|
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.application.ReadAction
|
||||||
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.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiFile
|
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
|
@Service
|
||||||
class AgentService {
|
class AgentService : Disposable {
|
||||||
private val logger = Logger.getInstance(AgentService::class.java)
|
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 {
|
init {
|
||||||
try {
|
val settings = service<ApplicationSettingsState>()
|
||||||
val instance = Agent()
|
scope.launch {
|
||||||
instance.initialize().thenApply {
|
try {
|
||||||
logger.info("Agent init done: $it")
|
agent.initialize(createAgentConfig(settings.data))
|
||||||
agent.complete(instance)
|
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>? {
|
private fun createAgentConfig(state: ApplicationSettingsState.State): Agent.Config {
|
||||||
return agent.thenCompose {agent ->
|
return Agent.Config(
|
||||||
ReadAction.compute<PsiFile, Throwable> {
|
server = if (state.serverEndpoint.isNotBlank()) {
|
||||||
editor.project?.let { project ->
|
Agent.Config.Server(
|
||||||
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
|
endpoint = state.serverEndpoint,
|
||||||
}
|
)
|
||||||
}?.let { file ->
|
} else {
|
||||||
agent?.getCompletions(Agent.CompletionRequest(
|
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.virtualFile.path,
|
||||||
file.language.id, // FIXME: map language id
|
file.getLanguageId(),
|
||||||
editor.document.text,
|
editor.document.text,
|
||||||
offset
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,59 +4,50 @@ 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.fileEditor.FileEditorManagerEvent
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.tabbyml.intellijtabby.agent.AgentService
|
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
|
@Service
|
||||||
class CompletionScheduler {
|
class CompletionScheduler {
|
||||||
private val logger = Logger.getInstance(CompletionScheduler::class.java)
|
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
|
var scheduled: CompletionContext? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
fun schedule(editor: Editor, offset: Int, triggerDelay: Long = 150, manually: Boolean = false) {
|
||||||
fun schedule(editor: Editor, offset: Int) {
|
|
||||||
clear()
|
|
||||||
val agentService = service<AgentService>()
|
val agentService = service<AgentService>()
|
||||||
val inlineCompletionService = service<InlineCompletionService>()
|
val inlineCompletionService = service<InlineCompletionService>()
|
||||||
inlineCompletionService.dismiss()
|
val settings = service<ApplicationSettingsState>()
|
||||||
val timer = Timer()
|
clear()
|
||||||
timer.schedule(object : TimerTask() {
|
val job = agentService.scope.launch {
|
||||||
override fun run() {
|
if (!manually && !settings.isAutoCompletionEnabled) {
|
||||||
logger.info("Scheduled completion task running")
|
return@launch
|
||||||
agentService.getCompletion(editor, offset)?.thenAccept {
|
|
||||||
inlineCompletionService.show(editor, offset, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 150)
|
logger.info("Schedule completion at $offset after $triggerDelay ms.")
|
||||||
scheduled = CompletionContext(editor, offset, timer)
|
|
||||||
|
|
||||||
if (project != editor.project) {
|
delay(triggerDelay)
|
||||||
project = editor.project
|
if (!manually && !settings.isAutoCompletionEnabled) {
|
||||||
editor.project?.messageBus?.connect()?.subscribe(
|
return@launch
|
||||||
FileEditorManagerListener.FILE_EDITOR_MANAGER,
|
}
|
||||||
object: FileEditorManagerListener {
|
logger.info("Trigger completion at $offset")
|
||||||
override fun selectionChanged(event: FileEditorManagerEvent) {
|
agentService.getCompletion(editor, offset)?.let {
|
||||||
logger.info("FileEditorManagerListener selectionChanged.")
|
inlineCompletionService.show(editor, offset, it)
|
||||||
clear()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
scheduled = CompletionContext(editor, offset, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
scheduled?.let {
|
|
||||||
it.timer.cancel()
|
|
||||||
scheduled = null
|
|
||||||
}
|
|
||||||
val inlineCompletionService = service<InlineCompletionService>()
|
val inlineCompletionService = service<InlineCompletionService>()
|
||||||
inlineCompletionService.dismiss()
|
inlineCompletionService.dismiss()
|
||||||
|
scheduled?.let {
|
||||||
|
it.job.cancel()
|
||||||
|
scheduled = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,11 +2,16 @@ package com.tabbyml.intellijtabby.editor
|
||||||
|
|
||||||
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.event.*
|
import com.intellij.openapi.editor.event.*
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
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 {
|
class EditorListener : EditorFactoryListener {
|
||||||
private val logger = Logger.getInstance(EditorListener::class.java)
|
private val logger = Logger.getInstance(EditorListener::class.java)
|
||||||
|
private val messagesConnection = mutableMapOf<Editor, MessageBusConnection>()
|
||||||
|
|
||||||
override fun editorCreated(event: EditorFactoryEvent) {
|
override fun editorCreated(event: EditorFactoryEvent) {
|
||||||
val editor = event.editor
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tabbyml.intellijtabby.editor
|
||||||
import com.intellij.openapi.application.invokeLater
|
import com.intellij.openapi.application.invokeLater
|
||||||
import com.intellij.openapi.command.WriteCommandAction
|
import com.intellij.openapi.command.WriteCommandAction
|
||||||
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.editor.EditorCustomElementRenderer
|
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.openapi.util.Disposer
|
||||||
import com.intellij.ui.JBColor
|
import com.intellij.ui.JBColor
|
||||||
import com.tabbyml.intellijtabby.agent.Agent
|
import com.tabbyml.intellijtabby.agent.Agent
|
||||||
|
import com.tabbyml.intellijtabby.agent.AgentService
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
|
|
||||||
|
|
@ -19,7 +22,13 @@ import java.awt.Rectangle
|
||||||
class InlineCompletionService {
|
class InlineCompletionService {
|
||||||
private val logger = Logger.getInstance(InlineCompletionService::class.java)
|
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
|
var shownInlineCompletion: InlineCompletion? = null
|
||||||
private set
|
private set
|
||||||
|
|
@ -30,13 +39,24 @@ class InlineCompletionService {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
invokeLater {
|
invokeLater {
|
||||||
|
// FIXME: support multiple choices
|
||||||
val text = completion.choices.first().text
|
val text = completion.choices.first().text
|
||||||
logger.info("Showing inline completion at $offset: $text")
|
logger.info("Showing inline completion at $offset: $text")
|
||||||
val lines = text.split("\n")
|
val lines = text.split("\n")
|
||||||
val inlays = lines
|
val inlays = lines
|
||||||
.mapIndexed { index, line -> createInlayLine(editor, offset, line, index) }
|
.mapIndexed { index, line -> createInlayLine(editor, offset, line, index) }
|
||||||
.filterNotNull()
|
.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 {
|
invokeLater {
|
||||||
it.inlays.forEach(Disposer::dispose)
|
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
|
shownInlineCompletion = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,21 @@
|
||||||
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
|
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 -->
|
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
|
||||||
<description><![CDATA[
|
<description><![CDATA[
|
||||||
Early version of Tabby for IntelliJ.<br>
|
<h1 id="tabby-plugin-for-intellij-platform">Tabby Plugin for IntelliJ Platform</h1>
|
||||||
Require Node.js 16.0+ installed and added to PATH.<br>
|
<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 > Editor > 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 > Tabby</code>.</li>
|
||||||
|
</ol>
|
||||||
]]></description>
|
]]></description>
|
||||||
|
|
||||||
<!-- Product and plugin compatibility requirements.
|
<!-- Product and plugin compatibility requirements.
|
||||||
|
|
@ -25,30 +38,46 @@
|
||||||
<!-- Extension points defined by the plugin.
|
<!-- Extension points defined by the plugin.
|
||||||
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
|
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<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"/>
|
<editorFactoryListener implementation="com.tabbyml.intellijtabby.editor.EditorListener"/>
|
||||||
|
<statusBarWidgetFactory implementation="com.tabbyml.intellijtabby.status.StatusBarWidgetFactory"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
<actions>
|
<actions>
|
||||||
<group id="Tabby.ToolsMenu" popup="true" text="Tabby" description="Tabby AI code assistant">
|
<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"
|
<action id="Tabby.TriggerCompletion"
|
||||||
class="com.tabbyml.intellijtabby.actions.TriggerCompletion"
|
class="com.tabbyml.intellijtabby.actions.TriggerCompletion"
|
||||||
text="Trigger Completion"
|
text="Show Inline Completion"
|
||||||
description="Trigger completion at current position.">
|
description="Show inline completion suggestions at the caret's position.">
|
||||||
<keyboard-shortcut first-keystroke="alt BACK_SLASH" keymap="$default"/>
|
<keyboard-shortcut first-keystroke="alt BACK_SLASH" keymap="$default"/>
|
||||||
</action>
|
</action>
|
||||||
<action id="Tabby.AcceptCompletion"
|
<action id="Tabby.AcceptCompletion"
|
||||||
class="com.tabbyml.intellijtabby.actions.AcceptCompletion"
|
class="com.tabbyml.intellijtabby.actions.AcceptCompletion"
|
||||||
text="Accept Completion"
|
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"/>
|
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
|
||||||
</action>
|
</action>
|
||||||
<action id="Tabby.DismissCompletion"
|
<action id="Tabby.DismissCompletion"
|
||||||
class="com.tabbyml.intellijtabby.actions.DismissCompletion"
|
class="com.tabbyml.intellijtabby.actions.DismissCompletion"
|
||||||
text="Dismiss Completion"
|
text="Dismiss Completion"
|
||||||
description="Trigger completion at current position.">
|
description="Hide the shown suggestions.">
|
||||||
<keyboard-shortcut first-keystroke="ESCAPE" keymap="$default"/>
|
<keyboard-shortcut first-keystroke="ESCAPE" keymap="$default"/>
|
||||||
</action>
|
</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>
|
</group>
|
||||||
</actions>
|
</actions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
|
|
@ -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 |
Loading…
Reference in New Issue