refactor: code clean up and minor UI improvements

This commit is contained in:
Carl-Robert Linnupuu 2025-01-26 01:51:18 +00:00
parent f7c49f5f90
commit 89a3b669c5
18 changed files with 541 additions and 451 deletions

View file

@ -34,10 +34,10 @@ import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel;
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.GitCommitTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.PersonaTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.PersonaTagDetails;
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails;
import ee.carlrobert.codegpt.util.EditorUtil;
import ee.carlrobert.codegpt.util.file.FileUtil;
import git4idea.GitCommit;
@ -128,7 +128,7 @@ public class ChatToolWindowTabPanel implements Disposable {
userInputPanel.addCommitReferences(gitCommits);
}
public List<HeaderTagDetails> getSelectedTags() {
public List<TagDetails> getSelectedTags() {
return userInputPanel.getSelectedTags();
}
@ -155,15 +155,15 @@ public class ChatToolWindowTabPanel implements Disposable {
return getReferencedFiles(userInputPanel.getSelectedTags());
}
private List<ReferencedFile> getReferencedFiles(List<? extends HeaderTagDetails> tags) {
private List<ReferencedFile> getReferencedFiles(List<? extends TagDetails> tags) {
return tags.stream()
.filter(FileTagDetails.class::isInstance)
.map(it -> ReferencedFile.from(((FileTagDetails) it).getVirtualFile()))
.toList();
}
private <T extends HeaderTagDetails> Optional<T> findTagOfType(
List<? extends HeaderTagDetails> tags,
private <T extends TagDetails> Optional<T> findTagOfType(
List<? extends TagDetails> tags,
Class<T> tagClass) {
return tags.stream()
.filter(tagClass::isInstance)
@ -307,7 +307,7 @@ public class ChatToolWindowTabPanel implements Disposable {
.executeOnPooledThread(() -> requestHandler.call(callParameters));
}
private Unit handleSubmit(String text, List<? extends HeaderTagDetails> appliedTags) {
private Unit handleSubmit(String text, List<? extends TagDetails> appliedTags) {
var messageBuilder = new MessageBuilder(project, text).withInlays(appliedTags);
List<ReferencedFile> referencedFiles = getReferencedFiles();

View file

@ -4,13 +4,13 @@ import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.ReferencedFile
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.ui.textarea.TagProcessorFactory
import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
class MessageBuilder(private val project: Project, private val text: String) {
private val message = Message("")
private var inlayContent: String = ""
fun withInlays(appliedTags: List<HeaderTagDetails>): MessageBuilder {
fun withInlays(appliedTags: List<TagDetails>): MessageBuilder {
if (appliedTags.isNotEmpty()) {
inlayContent = processTags(message, appliedTags)
}
@ -42,7 +42,7 @@ class MessageBuilder(private val project: Project, private val text: String) {
private fun processTags(
message: Message,
tags: List<HeaderTagDetails>
tags: List<TagDetails>
): String = buildString {
tags.forEach {
TagProcessorFactory.getProcessor(project, it).process(message, it, this)

View file

@ -0,0 +1,65 @@
package ee.carlrobert.codegpt.ui
import java.awt.Container
import java.awt.Dimension
import java.awt.FlowLayout
class WrapLayout(align: Int, hgap: Int, vgap: Int) : FlowLayout(align, hgap, vgap) {
override fun preferredLayoutSize(target: Container): Dimension {
return layoutSize(target, true)
}
override fun minimumLayoutSize(target: Container): Dimension {
return layoutSize(target, false)
}
private fun layoutSize(target: Container, preferred: Boolean): Dimension {
synchronized(target.treeLock) {
val targetWidth = target.width
var width = targetWidth
if (targetWidth == 0) {
width = Int.MAX_VALUE
}
val insets = target.insets
val horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2)
val maxWidth = width - horizontalInsetsAndGap
val dim = Dimension(0, 0)
var rowWidth = 0
var rowHeight = 0
for (i in 0 until target.componentCount) {
val m = target.getComponent(i)
if (m.isVisible) {
val d = if (preferred) m.preferredSize else m.minimumSize
if (rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight)
rowWidth = 0
rowHeight = 0
}
if (rowWidth != 0) {
rowWidth += hgap
}
rowWidth += d.width
rowHeight = maxOf(rowHeight, d.height)
}
}
addRow(dim, rowWidth, rowHeight)
dim.width += horizontalInsetsAndGap
dim.height += insets.top + insets.bottom + vgap * 2
return dim
}
}
private fun addRow(dim: Dimension, rowWidth: Int, rowHeight: Int) {
dim.width = maxOf(dim.width, rowWidth)
if (dim.height > 0) {
dim.height += vgap
}
dim.height += rowHeight
}
}

View file

@ -1,8 +1,8 @@
package ee.carlrobert.codegpt.ui.textarea
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
interface TagProcessor {
fun process(message: Message, tagDetails: HeaderTagDetails, promptBuilder: StringBuilder)
fun process(message: Message, tagDetails: TagDetails, promptBuilder: StringBuilder)
}

View file

@ -6,13 +6,13 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.ui.textarea.header.*
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
import ee.carlrobert.codegpt.util.GitUtil
import git4idea.GitCommit
object TagProcessorFactory {
fun getProcessor(project: Project, tagDetails: HeaderTagDetails): TagProcessor {
fun getProcessor(project: Project, tagDetails: TagDetails): TagProcessor {
return when (tagDetails) {
is FileTagDetails -> FileTagProcessor()
is SelectionTagDetails -> SelectionTagProcessor()
@ -30,7 +30,7 @@ object TagProcessorFactory {
class FileTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is FileTagDetails) {
@ -46,7 +46,7 @@ class FileTagProcessor : TagProcessor {
class SelectionTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
val selectionModel = (tagDetails as? SelectionTagDetails)?.selectionModel ?: return
@ -68,7 +68,7 @@ class SelectionTagProcessor : TagProcessor {
class DocumentationTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is DocumentationTagDetails) {
@ -81,7 +81,7 @@ class DocumentationTagProcessor : TagProcessor {
class PersonaTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is PersonaTagDetails) {
@ -94,7 +94,7 @@ class PersonaTagProcessor : TagProcessor {
class FolderTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is FolderTagDetails) {
@ -121,7 +121,7 @@ class FolderTagProcessor : TagProcessor {
class WebTagProcessor : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is WebTagDetails) {
@ -134,7 +134,7 @@ class WebTagProcessor : TagProcessor {
class GitCommitTagProcessor(private val project: Project) : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is GitCommitTagDetails) {
@ -168,7 +168,7 @@ class GitCommitTagProcessor(private val project: Project) : TagProcessor {
class CurrentGitChangesTagProcessor(private val project: Project) : TagProcessor {
override fun process(
message: Message,
tagDetails: HeaderTagDetails,
tagDetails: TagDetails,
promptBuilder: StringBuilder
) {
if (tagDetails !is CurrentGitChangesTagDetails) {

View file

@ -33,10 +33,10 @@ import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
import ee.carlrobert.codegpt.ui.IconActionButton
import ee.carlrobert.codegpt.ui.textarea.header.GitCommitTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.SelectionTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.UserInputHeaderPanel
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.SelectionTagDetails
import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel
import git4idea.GitCommit
@ -51,7 +51,7 @@ class UserInputPanel(
private val conversation: Conversation,
private val totalTokensPanel: TotalTokensPanel,
parentDisposable: Disposable,
private val onSubmit: (String, List<HeaderTagDetails>) -> Unit,
private val onSubmit: (String, List<TagDetails>) -> Unit,
private val onStop: () -> Unit
) : JPanel(BorderLayout()) {
@ -60,10 +60,7 @@ class UserInputPanel(
}
private val suggestionsPopupManager = SuggestionsPopupManager(project, this)
private val userInputHeaderPanel = UserInputHeaderPanel(
project,
suggestionsPopupManager
)
private val userInputHeaderPanel = UserInputHeaderPanel(project, suggestionsPopupManager)
private val promptTextField =
PromptTextField(project, suggestionsPopupManager, ::updateUserTokens) {
handleSubmit(it, userInputHeaderPanel.getSelectedTags())
@ -106,7 +103,7 @@ class UserInputPanel(
Disposer.register(parentDisposable, promptTextField)
}
fun getSelectedTags(): List<HeaderTagDetails> {
fun getSelectedTags(): List<TagDetails> {
return userInputHeaderPanel.getSelectedTags()
}
@ -139,7 +136,7 @@ class UserInputPanel(
}
}
fun addTag(tagDetails: HeaderTagDetails) {
fun addTag(tagDetails: TagDetails) {
userInputHeaderPanel.addTag(tagDetails)
val text = promptTextField.text
if (text.isNotEmpty() && text.last() == '@') {
@ -190,7 +187,7 @@ class UserInputPanel(
override fun getInsets(): Insets = JBUI.insets(4)
private fun handleSubmit(text: String, appliedTags: List<HeaderTagDetails> = emptyList()) {
private fun handleSubmit(text: String, appliedTags: List<TagDetails> = emptyList()) {
if (text.isNotEmpty() && submitButton.isEnabled) {
onSubmit(text, appliedTags)
promptTextField.clear()
@ -271,64 +268,4 @@ class UserInputPanel(
else -> false
}
}
}
class WrapLayout(align: Int, hgap: Int, vgap: Int) : FlowLayout(align, hgap, vgap) {
override fun preferredLayoutSize(target: Container): Dimension {
return layoutSize(target, true)
}
override fun minimumLayoutSize(target: Container): Dimension {
return layoutSize(target, false)
}
private fun layoutSize(target: Container, preferred: Boolean): Dimension {
synchronized(target.treeLock) {
val targetWidth = target.width
var width = targetWidth
if (targetWidth == 0) {
width = Int.MAX_VALUE
}
val insets = target.insets
val horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2)
val maxWidth = width - horizontalInsetsAndGap
val dim = Dimension(0, 0)
var rowWidth = 0
var rowHeight = 0
for (i in 0 until target.componentCount) {
val m = target.getComponent(i)
if (m.isVisible) {
val d = if (preferred) m.preferredSize else m.minimumSize
if (rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight)
rowWidth = 0
rowHeight = 0
}
if (rowWidth != 0) {
rowWidth += hgap
}
rowWidth += d.width
rowHeight = maxOf(rowHeight, d.height)
}
}
addRow(dim, rowWidth, rowHeight)
dim.width += horizontalInsetsAndGap
dim.height += insets.top + insets.bottom + vgap * 2
return dim
}
}
private fun addRow(dim: Dimension, rowWidth: Int, rowHeight: Int) {
dim.width = maxOf(dim.width, rowWidth)
if (dim.height > 0) {
dim.height += vgap
}
dim.height += rowHeight
}
}

View file

@ -1,31 +0,0 @@
package ee.carlrobert.codegpt.ui.textarea.header
import com.intellij.icons.AllIcons
import com.intellij.util.IconUtil
import java.awt.Cursor
import java.awt.Dimension
import java.awt.Graphics
import javax.swing.JButton
class AddButton(onAdd: () -> Unit) : JButton() {
init {
addActionListener {
onAdd()
}
cursor = Cursor(Cursor.HAND_CURSOR)
preferredSize = Dimension(20, 20)
isContentAreaFilled = false
isOpaque = false
border = null
toolTipText = "Add Context"
icon = IconUtil.scale(AllIcons.General.InlineAdd, null, 0.75f)
rolloverIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
pressedIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
}
override fun paintComponent(g: Graphics) {
PaintUtil.drawRoundedBackground(g, this, true)
super.paintComponent(g)
}
}

View file

@ -1,21 +0,0 @@
package ee.carlrobert.codegpt.ui.textarea.header
import com.intellij.icons.AllIcons
import com.intellij.icons.AllIcons.Actions.Close
import com.intellij.util.ui.JBUI
import java.awt.Dimension
import javax.swing.JButton
class CloseButton(onClose: () -> Unit) : JButton(Close) {
init {
addActionListener {
onClose()
}
preferredSize = Dimension(Close.iconWidth, Close.iconHeight)
border = JBUI.Borders.emptyLeft(4)
isContentAreaFilled = false
toolTipText = "Remove"
rolloverIcon = AllIcons.Actions.CloseHovered
}
}

View file

@ -0,0 +1,62 @@
package ee.carlrobert.codegpt.ui.textarea.header
import com.intellij.util.ui.JBUI
import java.awt.Container
import java.awt.Dimension
import java.awt.FlowLayout
import javax.swing.JPanel
class CustomFlowLayout(private val spacing: Int = 4) : FlowLayout(LEFT, 0, 0) {
override fun layoutContainer(target: Container) {
synchronized(target.treeLock) {
val insets = target.insets
var x = insets.left
val y = insets.top
val visibleComponents = target.components.filter { it.isVisible }
visibleComponents.forEachIndexed { index, component ->
val dim = component.preferredSize
component.setBounds(x, y, dim.width, dim.height)
x += dim.width
if (index < visibleComponents.size - 1) {
x += spacing
}
}
}
}
}
const val DEFAULT_SPACING = 4
class CustomFlowPanel : JPanel(CustomFlowLayout(DEFAULT_SPACING)) {
init {
isOpaque = false
border = JBUI.Borders.empty()
}
override fun getPreferredSize(): Dimension {
var width = 0
var height = 0
var visibleCount = 0
for (component in components) {
if (component.isVisible) {
width += component.preferredSize.width
height = maxOf(height, component.preferredSize.height)
visibleCount++
}
}
if (visibleCount > 1) {
// gaps between multiple visible components
width += DEFAULT_SPACING * (visibleCount - 1)
}
return Dimension(width, height)
}
override fun getMinimumSize(): Dimension = preferredSize
override fun getMaximumSize(): Dimension = preferredSize
}

View file

@ -1,5 +1,7 @@
package ee.carlrobert.codegpt.ui.textarea.header
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
@ -10,36 +12,39 @@ import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.components.JBLabel
import com.intellij.util.IconUtil
import com.intellij.util.ui.JBUI
import com.jetbrains.rd.util.UUID
import ee.carlrobert.codegpt.EditorNotifier
import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier
import ee.carlrobert.codegpt.ui.textarea.WrapLayout
import ee.carlrobert.codegpt.ui.WrapLayout
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager
import ee.carlrobert.codegpt.util.EditorUtil
import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor
import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditorFile
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Graphics
import javax.swing.JButton
import javax.swing.JPanel
import kotlin.math.max
import kotlin.math.min
class UserInputHeaderPanel(
private val project: Project,
suggestionsPopupManager: SuggestionsPopupManager
) : JPanel(WrapLayout(FlowLayout.LEFT, 4, 4)) {
) : JPanel(WrapLayout(FlowLayout.LEFT, 4, 4)), TagManagerListener {
companion object {
private const val MAX_VISIBLE_TAGS = 3
private const val INITIAL_VISIBLE_FILES = 2
private const val TAG_INSERTION_OFFSET = 4
private const val TAG_INSERTION_OFFSET = 1
}
private val tags = mutableSetOf<HeaderTagDetails>()
private val selectedFileTags = mutableSetOf<FileTagDetails>()
private val selectedFileHeaderTag = object : SelectedFileHeaderTag(project) {
private val tagManager = TagManager()
private val selectedFileTagPanel = object : SelectedFileTagPanel(project) {
override fun onClose() {
this.isVisible = false
if (tags.isEmpty()) {
if (tagManager.getTags().isEmpty()) {
emptyText.isVisible = true
}
}
@ -47,168 +52,11 @@ class UserInputHeaderPanel(
private val emptyText = JBLabel("No context included").apply {
foreground = JBUI.CurrentTheme.Label.disabledForeground()
font = JBUI.Fonts.smallFont()
border = JBUI.Borders.emptyLeft(2)
border = JBUI.Borders.empty(4, 4, 4, 0)
isVisible = getSelectedEditor(project) == null
}
private val selectionHeaderTag = SelectionHeaderTag(project)
init {
initializeUI(suggestionsPopupManager)
initializeEventListeners()
}
fun getSelectedTags(): List<HeaderTagDetails> {
val selectedTags: MutableList<HeaderTagDetails> =
tags.filter { it.selected }.toMutableList()
val selectedFile = (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile
if (selectedFileHeaderTag.isVisible && selectedFileHeaderTag.tagDetails.selected && selectedFile != null) {
selectedTags.add(FileTagDetails(selectedFile))
}
(selectionHeaderTag.tagDetails as? SelectionTagDetails)?.let {
selectedTags.add(it)
}
return selectedTags
}
fun addTag(tagDetails: HeaderTagDetails) {
if (selectedFileHeaderTag.isVisible
&& tagDetails is FileTagDetails
&& (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile == tagDetails.virtualFile
) {
return
}
if (tags.add(tagDetails)) {
emptyText.isVisible = false
if (tagDetails is FileTagDetails) {
selectedFileTags.add(tagDetails)
}
val tag = createTag(tagDetails)
val lastSelectionTagIndex = getLastSelectedTagIndex()
if (lastSelectionTagIndex != -1) {
add(tag, lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1)
} else {
add(tag, TAG_INSERTION_OFFSET)
}
val unselectedTags = components
.filter { it !is SelectedFileHeaderTag && it !is SelectionHeaderTag }
.filterIsInstance<HeaderTag>()
.filter { !it.tagDetails.selected }
if (unselectedTags.size > 2) {
removeTag(unselectedTags.last().tagDetails.id)
}
revalidate()
repaint()
}
}
private fun createTag(tagDetails: HeaderTagDetails) =
object : HeaderTag(tagDetails, tagDetails is FileTagDetails) {
override fun onSelect(tagDetails: HeaderTagDetails) {
tagDetails.selected = !tagDetails.selected
update(tagDetails.name, tagDetails.icon)
if (tagDetails is FileTagDetails) {
if (tagDetails.selected) {
selectedFileTags.add(tagDetails)
updateTagPosition(this)
val canAddNewTag = tags
.filter { it is FileTagDetails && !it.selected }.size <= 2
if (canAddNewTag) {
addNextOpenFile()
}
} else {
project.service<FileEditorManager>().openFile(tagDetails.virtualFile)
}
}
}
override fun onClose() {
selectedFileTags.removeIf { it.id == tagDetails.id }
removeTag(tagDetails.id)
}
}
private fun updateTagPosition(tag: HeaderTag) {
remove(tag)
val lastSelectionTagIndex = getLastSelectedTagIndex()
if (lastSelectionTagIndex != -1) {
add(tag, lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1)
} else {
add(tag, max(getFirstUnselectedTagIndex(), TAG_INSERTION_OFFSET))
}
repaint()
revalidate()
}
private fun getFilteredHeaderTags(): List<HeaderTag> = components
.filter { it !is SelectedFileHeaderTag && it !is SelectionHeaderTag }
.filterIsInstance<HeaderTag>()
private fun getLastSelectedTagIndex(): Int =
getFilteredHeaderTags().indexOfLast { it.tagDetails.selected }
private fun getFirstUnselectedTagIndex(): Int =
getFilteredHeaderTags().indexOfFirst { !it.tagDetails.selected }
private fun getSelectedFileTag(file: VirtualFile): FileTagDetails? =
selectedFileTags.find { it.virtualFile == file }
private fun getFileTag(file: VirtualFile): FileTagDetails? =
tags.filterIsInstance<FileTagDetails>().find { it.virtualFile == file }
private fun isFileTagExists(file: VirtualFile): Boolean {
return getFileTag(file) != null || selectedFileHeaderTag.virtualFile == file
}
private fun getSortedOpenFiles(project: Project): MutableList<FileTagDetails> =
EditorUtil.getOpenLocalFiles(project)
.filterNot { isFileTagExists(it) }
.map { FileTagDetails(it) }
.toMutableList()
private fun addNextOpenFile() {
EditorUtil.getOpenLocalFiles(project)
.firstOrNull { !isFileTagExists(it) }
?.let {
addTag(FileTagDetails(it, false))
}
}
private fun removeFileTag(virtualFile: VirtualFile) {
getFileTag(virtualFile)?.let {
removeTag(it.id)
}
}
private fun removeTag(id: UUID) {
val tagToRemove =
tags.find { it.id == id } ?: throw IllegalArgumentException("Tag with id $id not found")
if (tags.removeIf { it.id == tagToRemove.id }) {
val componentToRemove = components.find { it is HeaderTag && it.id == id } ?: return
remove(componentToRemove)
if (tags.isEmpty() && !selectedFileHeaderTag.isVisible) {
emptyText.isVisible = true
}
revalidate()
repaint()
}
}
private fun initializeUI(suggestionsPopupManager: SuggestionsPopupManager) {
isOpaque = false
border = JBUI.Borders.empty()
private val selectionTagPanel = SelectionTagPanel(project)
private val defaultHeaderTagsPanel = CustomFlowPanel().apply {
add(AddButton {
if (suggestionsPopupManager.isPopupVisible()) {
suggestionsPopupManager.hidePopup()
@ -217,17 +65,145 @@ class UserInputHeaderPanel(
}
})
add(emptyText)
add(selectionHeaderTag)
add(selectedFileHeaderTag)
add(selectionTagPanel)
add(selectedFileTagPanel)
}
if (tags.size <= 2) {
val selectedFile = getSelectedEditor(project)?.virtualFile
getSortedOpenFiles(project)
.take(INITIAL_VISIBLE_FILES)
.forEach {
addTag(FileTagDetails(it.virtualFile, selectedFile == it.virtualFile))
}
init {
tagManager.addListener(this)
initializeUI()
initializeEventListeners()
}
fun getSelectedTags(): List<TagDetails> {
val selectedTags = tagManager.getSelectedTags().toMutableList()
val selectedFile = getSelectedFile()
if (selectedFileTagPanel.isVisible && selectedFileTagPanel.tagDetails.selected && selectedFile != null) {
selectedTags.add(FileTagDetails(selectedFile))
}
(selectionTagPanel.tagDetails as? SelectionTagDetails)?.let {
selectedTags.add(it)
}
return selectedTags
}
fun addTag(tagDetails: TagDetails) {
tagManager.addTag(tagDetails)
}
override fun onTagAdded(tag: TagDetails) {
emptyText.isVisible = false
add(createTag(tag), getNextInsertionIndex())
revalidate()
repaint()
}
private fun getNextInsertionIndex(): Int {
val lastSelectionTagIndex = getLastSelectedTagIndex()
return if (lastSelectionTagIndex != -1) {
min(lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1, components.size)
} else {
TAG_INSERTION_OFFSET
}
}
override fun onTagRemoved(tag: TagDetails) {
val componentToRemove =
components.find { it is TagPanel && it.id == tag.id } ?: return
remove(componentToRemove)
if (getSelectedEditorFile(project) == null) {
selectedFileTagPanel.isVisible = false
}
if (tagManager.getTags().isEmpty() && !selectedFileTagPanel.isVisible) {
emptyText.isVisible = true
}
revalidate()
repaint()
}
override fun onTagSelectionChanged(tag: TagDetails) {
val existingTagComponent =
components.filterIsInstance<TagPanel>().find { it.id == tag.id }
?: return
existingTagComponent.update(tag.name, tag.icon)
updateTagPosition(existingTagComponent)
}
private fun createTag(tagDetails: TagDetails) =
object : TagPanel(tagDetails, tagDetails is FileTagDetails) {
override fun onSelect(tagDetails: TagDetails) {
if (tagDetails is FileTagDetails) {
if (tagDetails.selected) {
project.service<FileEditorManager>().openFile(tagDetails.virtualFile)
return
}
tagDetails.selected = true
val canAddNewTag = tagManager.getTags()
.filterIsInstance<FileTagDetails>()
.count { !it.selected } <= 2
if (canAddNewTag) {
addNextOpenFile()
}
update(tagDetails.name, tagDetails.icon)
}
}
override fun onClose() {
tagManager.removeTag(tagDetails.id)
}
}
private fun updateTagPosition(tag: TagPanel) {
remove(tag)
add(tag, getNextInsertionIndex())
revalidate()
repaint()
}
private fun getLastSelectedTagIndex(): Int =
components
.filter { it !is SelectedFileTagPanel && it !is SelectionTagPanel }
.filterIsInstance<TagPanel>()
.indexOfLast { it.tagDetails.selected }
private fun getSortedOpenFileTags(): MutableList<FileTagDetails> =
EditorUtil.getOpenLocalFiles(project)
.filterNot { tagManager.isFileTagExists(it) }
.map { FileTagDetails(it) }
.toMutableList()
private fun addNextOpenFile() {
getSortedOpenFileTags()
.firstOrNull { it.virtualFile != getSelectedFile() }
?.let {
tagManager.addTag(it.apply { selected = false })
}
}
private fun initializeUI() {
isOpaque = false
border = JBUI.Borders.empty()
add(defaultHeaderTagsPanel)
addInitialTags()
}
private fun addInitialTags() {
val selectedFile = getSelectedEditor(project)?.virtualFile
getSortedOpenFileTags()
.filterNot { it.virtualFile == selectedFile }
.take(INITIAL_VISIBLE_FILES)
.forEach {
tagManager.addTag(it.apply { selected = false })
}
}
private fun initializeEventListeners() {
@ -243,6 +219,29 @@ class UserInputHeaderPanel(
}
}
private class AddButton(onAdd: () -> Unit) : JButton() {
init {
addActionListener {
onAdd()
}
cursor = Cursor(Cursor.HAND_CURSOR)
preferredSize = Dimension(20, 20)
isContentAreaFilled = false
isOpaque = false
border = null
toolTipText = "Add Context"
icon = IconUtil.scale(AllIcons.General.InlineAdd, null, 0.75f)
rolloverIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
pressedIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
}
override fun paintComponent(g: Graphics) {
PaintUtil.drawRoundedBackground(g, this, true)
super.paintComponent(g)
}
}
private inner class EditorSelectionChangeListener : EditorNotifier.SelectionChange {
override fun selectionChanged(selectionModel: SelectionModel, virtualFile: VirtualFile) {
handleSelectionChange(selectionModel, virtualFile)
@ -252,18 +251,18 @@ class UserInputHeaderPanel(
selectionModel: SelectionModel,
virtualFile: VirtualFile
) {
selectionHeaderTag.update(virtualFile, selectionModel)
selectionTagPanel.update(virtualFile, selectionModel)
}
}
private inner class EditorCreatedListener : EditorNotifier.Created {
override fun editorCreated(editor: Editor) {
editor.virtualFile?.let { editorFile ->
if (selectedFileHeaderTag.isVisible && (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile == editorFile) {
if (selectedFileTagPanel.isVisible && getSelectedFile() == editorFile) {
return
}
addTag(FileTagDetails(editorFile, false))
tagManager.addTag(FileTagDetails(editorFile, false))
}
}
}
@ -271,30 +270,32 @@ class UserInputHeaderPanel(
private inner class EditorReleasedListener : EditorNotifier.Released {
override fun editorReleased(editor: Editor) {
if (editor.editorKind == EditorKind.MAIN_EDITOR && !editor.isDisposed && editor.virtualFile != null) {
removeFileTag(editor.virtualFile)
if (tags.isEmpty() && getSelectedEditorFile(project) == null) {
selectedFileHeaderTag.isVisible = false
emptyText.isVisible = true
}
tagManager.removeFileTag(editor.virtualFile)
}
}
}
private fun getSelectedFile(): VirtualFile? {
return (selectedFileTagPanel.tagDetails as? FileTagDetails)?.virtualFile
}
private inner class FileSelectionListener : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
val fileTags = tags.filterIsInstance<FileTagDetails>()
val currentTagDetails = (selectedFileTagPanel.tagDetails as? FileTagDetails) ?: return
event.newFile?.let { newFile ->
if (fileTags.any { it.virtualFile == newFile }) {
removeFileTag(newFile)
if (event.oldFile == currentTagDetails.virtualFile) {
tagManager.removeFileTag(newFile)
tagManager.addTag(currentTagDetails)
}
selectedFileHeaderTag.update(newFile)
emptyText.isVisible = false
event.oldFile?.let { oldFile ->
val prevSelectedTag = getSelectedFileTag(oldFile)
addTag(prevSelectedTag ?: FileTagDetails(oldFile, false))
val existingTag = tagManager.getFileTag(newFile)
runInEdt {
if (existingTag == null) {
selectedFileTagPanel.update(FileTagDetails(newFile))
} else {
selectedFileTagPanel.update(existingTag)
}
emptyText.isVisible = false
}
}
}
@ -303,9 +304,9 @@ class UserInputHeaderPanel(
private inner class IncludedFilesListener : IncludeFilesInContextNotifier {
override fun filesIncluded(includedFiles: MutableList<VirtualFile>) {
includedFiles
.filterNot { isFileTagExists(it) }
.filterNot { tagManager.isFileTagExists(it) }
.forEach {
addTag(FileTagDetails(it))
tagManager.addTag(FileTagDetails(it))
}
}
}

View file

@ -0,0 +1,60 @@
package ee.carlrobert.codegpt.ui.textarea.header.tag
import com.intellij.icons.AllIcons
import com.intellij.openapi.editor.SelectionModel
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.settings.prompts.PersonaDetails
import ee.carlrobert.codegpt.ui.DocumentationDetails
import git4idea.GitCommit
import java.util.*
import javax.swing.Icon
open class TagDetails(
open val name: String,
val icon: Icon? = null,
open var selected: Boolean = true
) {
val id: UUID = UUID.randomUUID()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TagDetails) return false
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
data class FileTagDetails(var virtualFile: VirtualFile, override var selected: Boolean = true) :
TagDetails(virtualFile.name, virtualFile.fileType.icon)
data class SelectionTagDetails(
var virtualFile: VirtualFile?,
var selectionModel: SelectionModel?,
) :
TagDetails(
"${virtualFile?.name} (${selectionModel?.selectionStartPosition?.line}:${selectionModel?.selectionEndPosition?.line})",
Icons.InSelection
)
data class DocumentationTagDetails(var documentationDetails: DocumentationDetails) :
TagDetails(documentationDetails.name, AllIcons.Toolwindows.Documentation)
data class PersonaTagDetails(var personaDetails: PersonaDetails) :
TagDetails(personaDetails.name, AllIcons.General.User)
data class GitCommitTagDetails(var gitCommit: GitCommit) :
TagDetails(gitCommit.id.asString().take(6), AllIcons.Vcs.CommitNode)
class CurrentGitChangesTagDetails :
TagDetails("Current Git Changes", AllIcons.Vcs.CommitNode)
data class FolderTagDetails(var folder: VirtualFile) :
TagDetails(folder.name, AllIcons.Nodes.Folder)
class WebTagDetails : TagDetails("Web", AllIcons.General.Web)
class EmptyTagDetails : TagDetails("")

View file

@ -0,0 +1,52 @@
package ee.carlrobert.codegpt.ui.textarea.header.tag
import com.intellij.openapi.vfs.VirtualFile
import java.util.*
class TagManager {
private val tags = mutableSetOf<TagDetails>()
private val listeners = mutableListOf<TagManagerListener>()
fun addListener(listener: TagManagerListener) {
listeners.add(listener)
}
fun getTags(): Set<TagDetails> = tags.toSet()
fun getSelectedTags(): List<TagDetails> = tags.filter { it.selected }
fun addTag(tagDetails: TagDetails) {
if (tags.add(tagDetails)) {
listeners.forEach { it.onTagAdded(tagDetails) }
}
}
fun removeFileTag(virtualFile: VirtualFile) {
getFileTag(virtualFile)?.let {
removeTag(it.id)
}
}
fun removeTag(id: UUID) {
tags.find { it.id == id }
?.let { tag ->
if (tags.removeIf { it.id == tag.id }) {
listeners.forEach { it.onTagRemoved(tag) }
}
}
}
fun getFileTag(file: VirtualFile): FileTagDetails? =
tags.filterIsInstance<FileTagDetails>().find { it.virtualFile == file }
fun isFileTagExists(file: VirtualFile): Boolean = getFileTag(file) != null
fun clear() {
val tagsToRemove = tags.toList()
tags.clear()
tagsToRemove.forEach { tag ->
listeners.forEach { it.onTagRemoved(tag) }
}
}
}

View file

@ -0,0 +1,7 @@
package ee.carlrobert.codegpt.ui.textarea.header.tag
interface TagManagerListener {
fun onTagAdded(tag: TagDetails)
fun onTagRemoved(tag: TagDetails)
fun onTagSelectionChanged(tag: TagDetails)
}

View file

@ -1,83 +1,34 @@
package ee.carlrobert.codegpt.ui.textarea.header
package ee.carlrobert.codegpt.ui.textarea.header.tag
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.runInEdt
import com.intellij.icons.AllIcons.Actions.Close
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.SelectionModel
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.components.JBLabel
import com.intellij.util.IconUtil
import com.intellij.util.ui.JBUI
import com.jetbrains.rd.util.UUID
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.settings.prompts.PersonaDetails
import ee.carlrobert.codegpt.ui.DocumentationDetails
import ee.carlrobert.codegpt.ui.textarea.header.PaintUtil
import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor
import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditorFile
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Graphics
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.Icon
import javax.swing.JButton
import javax.swing.JPanel
import javax.swing.SwingUtilities
import git4idea.GitCommit as Git4IdeaGitCommit
open class HeaderTagDetails(
open val name: String,
val icon: Icon? = null,
open var selected: Boolean = true
) {
val id: UUID = UUID.randomUUID()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is HeaderTagDetails) return false
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
data class FileTagDetails(var virtualFile: VirtualFile, override var selected: Boolean = true) :
HeaderTagDetails(virtualFile.name, virtualFile.fileType.icon)
data class SelectionTagDetails(
var virtualFile: VirtualFile?,
var selectionModel: SelectionModel?,
) :
HeaderTagDetails(
"${virtualFile?.name} (${selectionModel?.selectionStartPosition?.line}:${selectionModel?.selectionEndPosition?.line})",
Icons.InSelection
)
data class DocumentationTagDetails(var documentationDetails: DocumentationDetails) :
HeaderTagDetails(documentationDetails.name, AllIcons.Toolwindows.Documentation)
data class PersonaTagDetails(var personaDetails: PersonaDetails) :
HeaderTagDetails(personaDetails.name, AllIcons.General.User)
data class GitCommitTagDetails(var gitCommit: Git4IdeaGitCommit) :
HeaderTagDetails(gitCommit.id.asString().take(6), AllIcons.Vcs.CommitNode)
class CurrentGitChangesTagDetails :
HeaderTagDetails("Current Git Changes", AllIcons.Vcs.CommitNode)
data class FolderTagDetails(var folder: VirtualFile) :
HeaderTagDetails(folder.name, AllIcons.Nodes.Folder)
class WebTagDetails : HeaderTagDetails("Web", AllIcons.General.Web)
class EmptyTagDetails : HeaderTagDetails("")
abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectable: Boolean = true) :
JPanel() {
abstract class TagPanel(
var tagDetails: TagDetails,
private val selectable: Boolean = true
) : JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)) {
val id: UUID = tagDetails.id
@ -93,7 +44,7 @@ abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectabl
setupUI()
}
abstract fun onSelect(tagDetails: HeaderTagDetails)
abstract fun onSelect(tagDetails: TagDetails)
abstract fun onClose()
@ -108,7 +59,6 @@ abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectabl
} else {
JBUI.CurrentTheme.Label.disabledForeground(false)
}
repaint()
}
override fun paintComponent(g: Graphics) {
@ -116,11 +66,15 @@ abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectabl
PaintUtil.drawRoundedBackground(g, this, tagDetails.selected)
}
private fun createLabel(tagDetails: HeaderTagDetails): JBLabel {
private fun createLabel(tagDetails: TagDetails): JBLabel {
return (if (tagDetails.icon == null) {
JBLabel(tagDetails.name)
} else {
JBLabel(tagDetails.name, tagDetails.icon.scale(), SwingUtilities.LEADING)
JBLabel(
tagDetails.name,
IconUtil.scale(tagDetails.icon, null, 0.65f),
SwingUtilities.LEADING
)
}).apply {
foreground = if (tagDetails.selected) {
service<EditorColorsManager>().globalScheme.defaultForeground
@ -133,8 +87,7 @@ abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectabl
private fun setupUI() {
isOpaque = false
layout = FlowLayout(FlowLayout.LEFT, 0, 2)
border = JBUI.Borders.empty(0, 6, 0, 4)
border = JBUI.Borders.empty(2, 8)
cursor = if (selectable) Cursor(Cursor.HAND_CURSOR) else Cursor(Cursor.DEFAULT_CURSOR)
add(label)
@ -149,13 +102,27 @@ abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectabl
}
})
}
private class CloseButton(onClose: () -> Unit) : JButton(Close) {
init {
addActionListener {
onClose()
}
preferredSize = Dimension(Close.iconWidth, Close.iconHeight)
border = JBUI.Borders.emptyLeft(4)
isContentAreaFilled = false
toolTipText = "Remove"
rolloverIcon = AllIcons.Actions.CloseHovered
}
}
}
abstract class SelectedFileHeaderTag(
abstract class SelectedFileTagPanel(
private val project: Project,
var virtualFile: VirtualFile? = getSelectedEditorFile(project)
) : HeaderTag(
if (virtualFile == null) HeaderTagDetails("")
virtualFile: VirtualFile? = getSelectedEditorFile(project)
) : TagPanel(
if (virtualFile == null) TagDetails("")
else FileTagDetails(virtualFile)
) {
@ -163,47 +130,42 @@ abstract class SelectedFileHeaderTag(
isVisible = getSelectedEditorFile(project) != null
}
override fun onSelect(tagDetails: HeaderTagDetails) {
override fun onSelect(tagDetails: TagDetails) {
if (tagDetails is FileTagDetails) {
if (tagDetails.selected) {
project.service<FileEditorManager>().openFile(tagDetails.virtualFile)
}
tagDetails.selected = !tagDetails.selected
update(tagDetails.virtualFile.name, tagDetails.virtualFile.fileType.icon)
}
}
fun update(virtualFile: VirtualFile) {
tagDetails = FileTagDetails(virtualFile)
runInEdt {
isVisible = true
update(virtualFile.name, virtualFile.fileType.icon)
}
fun update(tagDetails: FileTagDetails) {
this.tagDetails = tagDetails
isVisible = true
update(tagDetails.name, tagDetails.virtualFile.fileType.icon)
}
}
fun getDefaultSelectionTagDetails(project: Project): HeaderTagDetails {
val editor = getSelectedEditor(project)
val selectionModel = editor?.selectionModel
return if (selectionModel?.hasSelection() == true) {
SelectionTagDetails(editor.virtualFile, selectionModel)
} else {
EmptyTagDetails()
}
}
class SelectionHeaderTag(project: Project) : HeaderTag(
class SelectionTagPanel(project: Project) : TagPanel(
getDefaultSelectionTagDetails(project),
false
) {
companion object {
fun getDefaultSelectionTagDetails(project: Project): TagDetails {
val editor = getSelectedEditor(project)
val selectionModel = editor?.selectionModel
return if (selectionModel?.hasSelection() == true) {
SelectionTagDetails(editor.virtualFile, selectionModel)
} else {
EmptyTagDetails()
}
}
}
init {
isVisible = tagDetails !is EmptyTagDetails
}
override fun onSelect(tagDetails: HeaderTagDetails) {
override fun onSelect(tagDetails: TagDetails) {
}
override fun onClose() {
@ -218,6 +180,4 @@ class SelectionHeaderTag(project: Project) : HeaderTag(
virtualFile.fileType.icon
)
}
}
fun Icon.scale() = IconUtil.scale(this, null, 0.65f)
}

View file

@ -1,11 +1,11 @@
package ee.carlrobert.codegpt.ui.textarea.header
package ee.carlrobert.codegpt.ui.textarea.header.tag
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
object TagUtil {
fun <T : HeaderTagDetails> isTagTypePresent(
fun <T : TagDetails> isTagTypePresent(
project: Project,
tagClass: Class<T>
): Boolean {
@ -16,7 +16,7 @@ object TagUtil {
.any { tagClass.isInstance(it) }
}
fun <T : HeaderTagDetails> getExistingTags(
fun <T : TagDetails> getExistingTags(
project: Project,
tagClass: Class<T>
): List<T> {

View file

@ -17,7 +17,7 @@ import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.ui.AddDocumentationDialog
import ee.carlrobert.codegpt.ui.DocumentationDetails
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
import ee.carlrobert.codegpt.ui.textarea.header.*
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
import git4idea.GitCommit
import javax.swing.Icon

View file

@ -15,10 +15,10 @@ import ee.carlrobert.codegpt.settings.prompts.PersonaDetails
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.ui.DocumentationDetails
import ee.carlrobert.codegpt.ui.textarea.header.DocumentationTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.PersonaTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.TagUtil
import ee.carlrobert.codegpt.ui.textarea.header.tag.DocumentationTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.PersonaTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagUtil
import ee.carlrobert.codegpt.util.GitUtil
import ee.carlrobert.codegpt.util.file.FileUtil
import kotlinx.coroutines.Dispatchers

View file

@ -12,8 +12,6 @@ import com.intellij.openapi.util.io.FileUtil.createDirectory
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileFilter
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings.getLlamaModelsPath
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails
import java.io.File
import java.io.FileOutputStream
import java.io.IOException