Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brown-regions-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"kilo-code": patch
---

- Fixed webview flickering in JetBrains plugin for smoother UI rendering
- Improved thread management in JetBrains plugin to prevent UI freezes
3,708 changes: 3,672 additions & 36 deletions deps/patches/vscode/jetbrains.patch

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0

// kilocode_change - new file
package ai.kilocode.jetbrains.actions

import ai.kilocode.jetbrains.git.CommitMessageService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ai.kilocode.jetbrains.config

/**
* Configurable performance settings for event debouncing and concurrency control.
* These settings allow tuning the balance between responsiveness and resource usage.
*/
object PerformanceSettings {
/**
* Debounce delay for file system events in milliseconds.
* Higher values reduce processing load but may delay file sync.
* Default: 50ms
*/
var fileEventDebounceMs: Long = 50

/**
* Debounce delay for editor activation events in milliseconds.
* Higher values reduce processing load during rapid editor switching.
* Default: 100ms
*/
var editorActivationDebounceMs: Long = 100

/**
* Debounce delay for editor edit events in milliseconds.
* Higher values reduce processing load during typing but may delay updates.
* Default: 50ms
*/
var editorEditDebounceMs: Long = 50

/**
* Maximum number of concurrent RPC calls allowed.
* This prevents resource exhaustion from too many simultaneous operations.
* Default: 100
*/
var maxConcurrentRpcCalls: Int = 100
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why this was here, removing it is no issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been removing headers from the files we've modified because LLM models tend to add this header to new files as well.


package ai.kilocode.jetbrains.core

import ai.kilocode.jetbrains.monitoring.DisposableTracker
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -34,7 +31,9 @@ class ExtensionSocketServer() : ISocketServer {

// Server thread
private var serverThread: Thread? = null


private val clientThreads = ConcurrentHashMap<Socket, Thread>()

// Current project path
private var projectPath: String = ""

Expand Down Expand Up @@ -69,6 +68,8 @@ class ExtensionSocketServer() : ISocketServer {

isRunning = true
logger.info("Starting socket server on port: $port")

DisposableTracker.register("ExtensionSocketServer", this)

// Start the thread to accept connections
serverThread = thread(start = true, name = "ExtensionSocketServer") {
Expand Down Expand Up @@ -105,12 +106,30 @@ class ExtensionSocketServer() : ISocketServer {
Thread.currentThread().interrupt()
}

// Interrupt all client handler threads
clientThreads.forEach { (socket, thread) ->
try {
thread.interrupt()
} catch (e: Exception) {
logger.warn("Failed to interrupt client handler thread", e)
}
}

// Wait for client handler threads to finish
clientThreads.forEach { (socket, thread) ->
try {
thread.join(2000) // Wait up to 2 seconds per thread
} catch (e: InterruptedException) {
logger.warn("Interrupted while waiting for client handler thread to finish")
Thread.currentThread().interrupt()
}
}
clientThreads.clear()

// Close all client managers and wait for them to finish
clientManagers.forEach { (socket, manager) ->
try {
logger.info("Disposing client manager for socket: ${socket.inetAddress}")
manager.dispose()
logger.info("Client manager disposed for socket: ${socket.inetAddress}")
} catch (e: Exception) {
logger.warn("Failed to dispose client manager", e)
}
Expand All @@ -135,6 +154,8 @@ class ExtensionSocketServer() : ISocketServer {
serverThread = null
serverSocket = null

DisposableTracker.unregister("ExtensionSocketServer")

logger.info("Socket server stopped completely")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package ai.kilocode.jetbrains.editor

import ai.kilocode.jetbrains.monitoring.ScopeRegistry
import ai.kilocode.jetbrains.monitoring.DisposableTracker
import ai.kilocode.jetbrains.plugin.SystemObjectProvider
import ai.kilocode.jetbrains.util.URI
import com.intellij.diff.DiffContentFactory
Expand Down Expand Up @@ -32,15 +30,26 @@ import com.intellij.openapi.vfs.readText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import java.io.File
import java.io.FileInputStream
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max

private data class FileEvent(
val uri: String,
val added: Boolean,
val isText: Boolean
)

@Service(Service.Level.PROJECT)
class EditorAndDocManager(val project: Project) : Disposable {

Expand All @@ -57,8 +66,22 @@ class EditorAndDocManager(val project: Project) : Disposable {

private var job: Job? = null
private val editorStateService: EditorStateService = EditorStateService(project)

private val fileEventScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val fileEventChannel = Channel<FileEvent>(Channel.CONFLATED)

init {
ScopeRegistry.register("EditorAndDocManager.fileEventScope", fileEventScope)

@OptIn(FlowPreview::class)
fileEventScope.launch {
fileEventChannel.consumeAsFlow()
.debounce(50) // 50ms debounce
.collect { event ->
sync2ExtHost(URI.file(event.uri), event.added, event.isText)
}
}

ideaEditorListener = object : FileEditorManagerListener {
// Update and synchronize editor state when file is opened
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
Expand All @@ -79,14 +102,19 @@ class EditorAndDocManager(val project: Project) : Disposable {
if (older == null) {
val uri = URI.file(editor.file.path)
val isText = FileDocumentManager.getInstance().getDocument(file) != null
CoroutineScope(Dispatchers.IO).launch {
val handle = sync2ExtHost(uri, false, isText)
handle.ideaEditor = editor
val group = tabManager.createTabGroup(EditorGroupColumn.BESIDE.value, true)
val options = TabOptions(isActive = true)
val tab = group.addTab(EditorTabInput(uri, uri.path, ""), options)
handle.tab = tab
handle.group = group
fileEventChannel.trySend(FileEvent(uri.toString(), false, isText))
// Store editor reference for later use
fileEventScope.launch {
delay(100) // Wait for debounced sync to complete
val handle = getEditorHandleByUri(uri, false)
if (handle != null) {
handle.ideaEditor = editor
val group = tabManager.createTabGroup(EditorGroupColumn.BESIDE.value, true)
val options = TabOptions(isActive = true)
val tab = group.addTab(EditorTabInput(uri, uri.path, ""), options)
handle.tab = tab
handle.group = group
}
}
}
}
Expand Down Expand Up @@ -478,6 +506,9 @@ class EditorAndDocManager(val project: Project) : Disposable {
}

override fun dispose() {
ScopeRegistry.unregister("EditorAndDocManager.fileEventScope")
fileEventChannel.close()
fileEventScope.cancel()
messageBusConnection.dispose()
}

Expand All @@ -499,7 +530,7 @@ class EditorAndDocManager(val project: Project) : Disposable {

private fun scheduleUpdate() {
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
job = fileEventScope.launch {
delay(10)
processUpdates()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package ai.kilocode.jetbrains.editor

import ai.kilocode.jetbrains.monitoring.ScopeRegistry
import ai.kilocode.jetbrains.monitoring.DisposableTracker
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Document
Expand All @@ -15,13 +13,23 @@ import com.intellij.openapi.vfs.LocalFileSystem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import java.io.File
import kotlin.math.max
import kotlin.math.min

private sealed class EditorEvent {
data class Activation(val active: Boolean) : EditorEvent()
data class Edit(val lines: List<String>, val versionId: Int?) : EditorEvent()
}

/**
* Manages the state and behavior of an editor instance
* Handles synchronization between IntelliJ editor and VSCode editor state
Expand All @@ -39,8 +47,35 @@ class EditorHolder(
val diff: Boolean,
private val stateManager: EditorAndDocManager,
) {

val logger = Logger.getInstance(EditorHolder::class.java)
private val editorOperationScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val activationEventChannel = Channel<Boolean>(Channel.CONFLATED)
private val editEventChannel = Channel<EditorEvent.Edit>(Channel.CONFLATED)

init {
ScopeRegistry.register("EditorHolder.editorOperationScope-$id", editorOperationScope)

@OptIn(FlowPreview::class)
editorOperationScope.launch {
activationEventChannel.consumeAsFlow()
.debounce(100) // 100ms debounce for activation
.collect { active ->
delay(100)
stateManager.didUpdateActive(this@EditorHolder)
}
}

@OptIn(FlowPreview::class)
editorOperationScope.launch {
editEventChannel.consumeAsFlow()
.debounce(50) // 50ms debounce for edits
.collect { event ->
document.lines = event.lines
document.versionId = event.versionId ?: (document.versionId + 1)
stateManager.updateDocument(document)
}
}
}

/**
* Indicates whether this editor is currently active.
Expand Down Expand Up @@ -126,10 +161,7 @@ class EditorHolder(
editorDocument = file?.let { FileDocumentManager.getInstance().getDocument(it) }
}
}
CoroutineScope(Dispatchers.IO).launch {
delay(100)
stateManager.didUpdateActive(this@EditorHolder)
}
activationEventChannel.trySend(active)
}

fun revealRange(range: Range) {
Expand Down Expand Up @@ -191,7 +223,7 @@ class EditorHolder(
editorDocument?.setText(newContent)
}
}
CoroutineScope(Dispatchers.IO).launch {
editorOperationScope.launch {
delay(1000)
val file = File(document.uri.path).parentFile
if (file.exists()) {
Expand Down Expand Up @@ -233,9 +265,7 @@ class EditorHolder(
}

fun updateDocumentContent(lines: List<String>, versionId: Int? = null) {
document.lines = lines
document.versionId = versionId ?: (document.versionId + 1)
debouncedUpdateDocument()
editEventChannel.trySend(EditorEvent.Edit(lines, versionId))
}

/**
Expand Down Expand Up @@ -305,4 +335,17 @@ class EditorHolder(
stateManager.updateDocument(document)
}
}

/**
* Disposes resources and cancels ongoing operations.
* Should be called when the editor is no longer needed.
*/
fun dispose() {
ScopeRegistry.unregister("EditorHolder.editorOperationScope-$id")
activationEventChannel.close()
editEventChannel.close()
editorOperationScope.cancel()
editorUpdateJob?.cancel()
documentUpdateJob?.cancel()
}
}
Loading