Skip to content

Commit

Permalink
feat: Add proper memory management
Browse files Browse the repository at this point in the history
- Dispose editors when project is unloaded
- Make OllamaService an object
- Couple other simplifications of lifecycle management
  • Loading branch information
fmueller committed Jun 4, 2024
1 parent e94d6a1 commit 5dbce9a
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.fmueller.jarvis.ai

import com.github.fmueller.jarvis.conversation.Message
import com.intellij.openapi.components.Service
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
Expand All @@ -26,8 +25,7 @@ private data class ChatRequest(
@Serializable
private data class ChatResponse(val message: ChatMessage)

@Service(Service.Level.PROJECT)
class OllamaService {
object OllamaService {

suspend fun chat(messages: List<Message>): String = withContext(Dispatchers.IO) {
// TODO check if model is available
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import com.github.fmueller.jarvis.conversation.Conversation
import com.github.fmueller.jarvis.conversation.Message
import com.github.fmueller.jarvis.conversation.Role

class ChatCommand(private val ollamaService: OllamaService) : SlashCommand {
class ChatCommand : SlashCommand {

override suspend fun run(conversation: Conversation): Conversation {
if (!ollamaService.isAvailable()) {
if (!OllamaService.isAvailable()) {
conversation.addMessage(
Message(
Role.ASSISTANT,
Expand All @@ -18,7 +18,7 @@ class ChatCommand(private val ollamaService: OllamaService) : SlashCommand {
return conversation
}

val response = ollamaService.chat(conversation.messages).trim()
val response = OllamaService.chat(conversation.messages).trim()
conversation.addMessage(Message(Role.ASSISTANT, response))
return conversation
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.github.fmueller.jarvis.commands

import com.github.fmueller.jarvis.ai.OllamaService

class SlashCommandParser(private val ollamaService: OllamaService) {
object SlashCommandParser {

fun parse(message: String): SlashCommand {
val trimmedMessage = message.trim().lowercase()
Expand All @@ -14,6 +12,6 @@ class SlashCommandParser(private val ollamaService: OllamaService) {
return NewConversationCommand()
}

return ChatCommand(ollamaService)
return ChatCommand()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.github.fmueller.jarvis.conversation

import com.github.fmueller.jarvis.ai.OllamaService
import com.github.fmueller.jarvis.commands.SlashCommandParser
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
import java.time.LocalDateTime
Expand All @@ -16,19 +17,20 @@ enum class Role {

data class Message(val role: Role, val content: String, val createdAt: LocalDateTime = LocalDateTime.now())

class Conversation(ollamaService: OllamaService) {
// as long as we don't have conversation history persistence,
// we can keep the conversation in memory
// and declare it as a project-level service
@Service(Service.Level.PROJECT)
class Conversation : Disposable {

private var _messages = mutableListOf<Message>()
val messages get() = _messages.toList()

private val commandParser = SlashCommandParser(ollamaService)
private val propertyChangeSupport = PropertyChangeSupport(this)

suspend fun chat(message: String): Conversation {
addMessage(Message(Role.USER, message.trim()))

val command = commandParser.parse(message)
return command.run(this)
return SlashCommandParser.parse(message).run(this)
}

fun addMessage(message: Message) {
Expand All @@ -47,4 +49,10 @@ class Conversation(ollamaService: OllamaService) {
fun addPropertyChangeListener(listener: PropertyChangeListener) {
propertyChangeSupport.addPropertyChangeListener(listener)
}

override fun dispose() {
propertyChangeSupport.getPropertyChangeListeners("messages").forEach {
propertyChangeSupport.removePropertyChangeListener(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.github.fmueller.jarvis.conversation

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBScrollPane
import org.jdesktop.swingx.VerticalLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JPanel
import javax.swing.SwingUtilities

class ConversationPanel(conversation: Conversation, private val project: Project) {
class ConversationPanel(conversation: Conversation, private val project: Project) : Disposable {

private val panel = JPanel().apply {
layout = VerticalLayout(1)
Expand All @@ -22,6 +24,8 @@ class ConversationPanel(conversation: Conversation, private val project: Project
}

init {
Disposer.register(conversation, this)

var isUserScrolling = false

scrollableContainer.verticalScrollBar.addAdjustmentListener { e ->
Expand Down Expand Up @@ -53,11 +57,23 @@ class ConversationPanel(conversation: Conversation, private val project: Project
}

private fun update(messages: List<Message>) {
panel.removeAll()
messages.forEach { message ->
panel.add(MessagePanel(message, project))
if (messages.size <= 1) {
panel.components.filter { it is Disposable }.map { it as Disposable }.forEach { it.dispose() }
panel.removeAll()
}

if (messages.isNotEmpty()) {
val messagePanel = MessagePanel(messages.last(), project)
Disposer.register(this, messagePanel)
panel.add(messagePanel)
}

panel.revalidate()
panel.repaint()
}

override fun dispose() {
// nothing to dispose here, all message panels are disposed when the panel is cleared
// or automatically when the conversation is disposed
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.fmueller.jarvis.conversation;

Check warning on line 1 in src/main/kotlin/com/github/fmueller/jarvis/conversation/MessagePanel.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant semicolon

Redundant semicolon

import com.github.fmueller.jarvis.ui.SyntaxHighlightedCodeHelper
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.project.Project
Expand All @@ -22,15 +23,15 @@ import javax.swing.BorderFactory
import javax.swing.JEditorPane
import javax.swing.JPanel

class MessagePanel(private val message: Message, project: Project) : JPanel() {
class MessagePanel(private val message: Message, project: Project) : JPanel(), Disposable {

private companion object {
private val codeBlockPattern = Pattern.compile("```(\\w+)?\\n(.*?)\\n```", Pattern.DOTALL)
private val assistantBgColor = { UIUtil.getPanelBackground() }
private val userBgColor = { UIUtil.getTextFieldBackground() }
}

private val syntaxHelper = SyntaxHighlightedCodeHelper(project)
private val highlightedCodeHelper = SyntaxHighlightedCodeHelper(project)

init {
buildPanel()
Expand All @@ -41,12 +42,17 @@ class MessagePanel(private val message: Message, project: Project) : JPanel() {
buildPanel()
}

override fun dispose() {
highlightedCodeHelper.disposeAllEditors()
}

private fun buildPanel() {
if (message == null) {
return
}

removeAll()
highlightedCodeHelper.disposeAllEditors()

layout = VerticalLayout(5)
background = if (message.role == Role.ASSISTANT) assistantBgColor() else userBgColor()
Expand Down Expand Up @@ -126,7 +132,7 @@ class MessagePanel(private val message: Message, project: Project) : JPanel() {
}

private fun addHighlightedCode(languageId: String, code: String) {
val editor = syntaxHelper.getHighlightedEditor(languageId, code)
val editor = highlightedCodeHelper.getHighlightedEditor(languageId, code)
if (editor != null) {
editor.contentComponent.border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
val outerPanelBackground = background
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.fmueller.jarvis.toolWindow

import com.github.fmueller.jarvis.ai.OllamaService
import com.github.fmueller.jarvis.conversation.Conversation
import com.github.fmueller.jarvis.conversation.ConversationPanel
import com.github.fmueller.jarvis.conversation.InputArea
Expand Down Expand Up @@ -33,7 +32,7 @@ class ConversationWindowFactory : ToolWindowFactory {

class ConversationWindow(toolWindow: ToolWindow) {

private val conversation = Conversation(toolWindow.project.service<OllamaService>())
private val conversation = toolWindow.project.service<Conversation>()
private val conversationPanel = ConversationPanel(conversation, toolWindow.project)

@OptIn(DelicateCoroutinesApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import java.time.format.DateTimeFormatter

class SyntaxHighlightedCodeHelper(private val project: Project) {

private val createdEditors = mutableListOf<Editor>()

companion object {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss.SSS")

Check notice on line 17 in src/main/kotlin/com/github/fmueller/jarvis/ui/SyntaxHighlightedCodeHelper.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Function or property has platform type

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
}

// TODO add disposal of created editors
fun getHighlightedEditor(languageId: String, code: String): Editor? {
// TODO test with some Kotlin code and see if the language is correctly detected
val language = Language.findLanguageByID(languageId) ?: IgnoreLanguage.INSTANCE
Expand All @@ -29,6 +30,7 @@ class SyntaxHighlightedCodeHelper(private val project: Project) {
)

val editor = EditorFactory.getInstance().createEditor(file.viewProvider.document, project, fileType, true)
createdEditors.add(editor)

editor.settings.apply {
isLineNumbersShown = false
Expand All @@ -48,4 +50,9 @@ class SyntaxHighlightedCodeHelper(private val project: Project) {
}
return editor
}

fun disposeAllEditors() {
createdEditors.forEach { EditorFactory.getInstance().releaseEditor(it) }
createdEditors.clear()
}
}

0 comments on commit 5dbce9a

Please sign in to comment.