Skip to content

Commit 91abe1b

Browse files
committed
feat(diff): add related select #257
1 parent 93b8316 commit 91abe1b

File tree

17 files changed

+619
-96
lines changed

17 files changed

+619
-96
lines changed

core/src/223/main/resources/META-INF/autodev-core.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@
181181
<extensionPoint qualifiedName="cc.unitmesh.langSketchProvider"
182182
interface="cc.unitmesh.devti.sketch.LanguageSketchProvider"
183183
dynamic="true" />
184+
185+
<extensionPoint qualifiedName="cc.unitmesh.relatedClassProvider"
186+
beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
187+
<with attribute="implementationClass" implements="cc.unitmesh.devti.provider.RelatedClassesProvider"/>
188+
</extensionPoint>
184189
</extensionPoints>
185190

186191
<applicationListeners>

core/src/233/main/resources/META-INF/autodev-core.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@
191191
<extensionPoint qualifiedName="cc.unitmesh.langSketchProvider"
192192
interface="cc.unitmesh.devti.sketch.LanguageSketchProvider"
193193
dynamic="true" />
194+
195+
<extensionPoint qualifiedName="cc.unitmesh.relatedClassProvider"
196+
beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
197+
<with attribute="implementationClass" implements="cc.unitmesh.devti.provider.RelatedClassesProvider"/>
198+
</extensionPoint>
194199
</extensionPoints>
195200

196201
<applicationListeners>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package cc.unitmesh.devti.gui.chat
2+
3+
import com.intellij.codeInsight.lookup.LookupElement
4+
import com.intellij.codeInsight.lookup.LookupElementDecorator
5+
import com.intellij.openapi.util.ClassConditionKey
6+
import com.intellij.openapi.vfs.VirtualFile
7+
8+
class AutoDevFileLookupElement<T : LookupElement> private constructor(
9+
delegate: T,
10+
val priority: Double,
11+
val virtualFile: VirtualFile,
12+
) : LookupElementDecorator<T>(delegate) {
13+
fun getFile(): VirtualFile {
14+
return virtualFile
15+
}
16+
17+
companion object {
18+
private val CLASS_CONDITION_KEY: ClassConditionKey<AutoDevFileLookupElement<*>> = ClassConditionKey.create(
19+
AutoDevFileLookupElement::class.java
20+
)
21+
22+
/**
23+
* @param element element
24+
* @param priority priority (higher priority puts the item closer to the beginning of the list)
25+
* @return decorated lookup element
26+
*/
27+
fun withPriority(element: LookupElement, priority: Double, virtualFile: VirtualFile): LookupElement {
28+
val prioritized = element.`as`(CLASS_CONDITION_KEY)
29+
val lookupElement = if (prioritized !== element) element else prioritized.delegate
30+
return AutoDevFileLookupElement(lookupElement, priority, virtualFile)
31+
}
32+
}
33+
}

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/AutoDevInput.kt

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.intellij.openapi.Disposable
66
import com.intellij.openapi.actionSystem.*
77
import com.intellij.openapi.actionSystem.ex.AnActionListener
88
import com.intellij.openapi.command.CommandProcessor
9+
import com.intellij.openapi.command.WriteCommandAction
910
import com.intellij.openapi.editor.Document
1011
import com.intellij.openapi.editor.Editor
1112
import com.intellij.openapi.editor.EditorModificationUtil
@@ -17,6 +18,12 @@ import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
1718
import com.intellij.openapi.fileTypes.FileTypes
1819
import com.intellij.openapi.project.DumbAwareAction
1920
import com.intellij.openapi.project.Project
21+
import com.intellij.openapi.project.guessProjectDir
22+
import com.intellij.openapi.util.TextRange
23+
import com.intellij.openapi.util.io.FileUtil
24+
import com.intellij.openapi.vfs.VirtualFile
25+
import com.intellij.psi.PsiDocumentManager
26+
import com.intellij.psi.codeStyle.CodeStyleManager
2027
import com.intellij.temporary.gui.block.findDocument
2128
import com.intellij.testFramework.LightVirtualFile
2229
import com.intellij.ui.EditorTextField
@@ -29,17 +36,6 @@ import java.util.*
2936
import javax.swing.KeyStroke
3037

3138

32-
enum class AutoDevInputTrigger {
33-
Button,
34-
Key
35-
}
36-
37-
interface AutoDevInputListener : EventListener {
38-
fun editorAdded(editor: EditorEx) {}
39-
fun onSubmit(component: AutoDevInputSection, trigger: AutoDevInputTrigger) {}
40-
fun onStop(component: AutoDevInputSection) {}
41-
}
42-
4339
class AutoDevInput(
4440
project: Project,
4541
private val listeners: List<DocumentListener>,
@@ -148,4 +144,41 @@ class AutoDevInput(
148144
inputDocument.addDocumentListener(listener)
149145
}
150146
}
151-
}
147+
148+
fun appendText(text: String) {
149+
WriteCommandAction.runWriteCommandAction(
150+
project,
151+
"Append text",
152+
"intentions.write.action",
153+
{
154+
insertStringAndSaveChange(project, text, this.editor!!.document, this.editor!!.document.textLength, false)
155+
})
156+
}
157+
}
158+
159+
fun insertStringAndSaveChange(
160+
project: Project,
161+
content: String,
162+
document: Document,
163+
startOffset: Int,
164+
withReformat: Boolean,
165+
) {
166+
if (startOffset < 0 || startOffset > document.textLength) return
167+
168+
document.insertString(startOffset, content)
169+
PsiDocumentManager.getInstance(project).commitDocument(document)
170+
171+
if (!withReformat) return
172+
173+
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document)
174+
psiFile?.let { file ->
175+
val reformatRange = TextRange(startOffset, startOffset + content.length)
176+
CodeStyleManager.getInstance(project).reformatText(file, listOf(reformatRange))
177+
}
178+
}
179+
180+
fun VirtualFile.relativePath(project: Project): String {
181+
val projectDir = project.guessProjectDir()!!.toNioPath().toFile()
182+
val relativePath = FileUtil.getRelativePath(projectDir, this.toNioPath().toFile())
183+
return relativePath ?: this.path
184+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package cc.unitmesh.devti.gui.chat
2+
3+
import com.intellij.openapi.editor.ex.EditorEx
4+
import java.util.*
5+
6+
enum class AutoDevInputTrigger {
7+
Button,
8+
Key
9+
}
10+
11+
interface AutoDevInputListener : EventListener {
12+
fun editorAdded(editor: EditorEx) {}
13+
fun onSubmit(component: AutoDevInputSection, trigger: AutoDevInputTrigger) {}
14+
fun onStop(component: AutoDevInputSection) {}
15+
}

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/AutoDevInputSection.kt

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import cc.unitmesh.devti.agent.model.CustomAgentState
88
import cc.unitmesh.devti.llms.tokenizer.Tokenizer
99
import cc.unitmesh.devti.llms.tokenizer.TokenizerFactory
1010
import cc.unitmesh.devti.settings.AutoDevSettingsState
11+
import com.intellij.codeInsight.lookup.LookupManagerListener
12+
import com.intellij.icons.AllIcons
1113
import com.intellij.ide.IdeTooltip
1214
import com.intellij.ide.IdeTooltipManager
1315
import com.intellij.openapi.Disposable
1416
import com.intellij.openapi.actionSystem.AnActionEvent
1517
import com.intellij.openapi.actionSystem.Presentation
1618
import com.intellij.openapi.actionSystem.impl.ActionButton
19+
import com.intellij.openapi.application.ApplicationManager
1720
import com.intellij.openapi.diagnostic.logger
1821
import com.intellij.openapi.editor.event.DocumentEvent
1922
import com.intellij.openapi.editor.event.DocumentListener
2023
import com.intellij.openapi.editor.ex.EditorEx
24+
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
25+
import com.intellij.openapi.fileEditor.FileEditorManagerListener
2126
import com.intellij.openapi.project.DumbAwareAction
2227
import com.intellij.openapi.project.Project
2328
import com.intellij.openapi.ui.ComboBox
@@ -27,29 +32,30 @@ import com.intellij.openapi.ui.popup.Balloon.Position
2732
import com.intellij.openapi.util.NlsContexts
2833
import com.intellij.openapi.wm.IdeFocusManager
2934
import com.intellij.openapi.wm.impl.InternalDecorator
35+
import com.intellij.psi.PsiElement
36+
import com.intellij.psi.PsiFile
37+
import com.intellij.psi.PsiManager
3038
import com.intellij.temporary.gui.block.AutoDevCoolBorder
3139
import com.intellij.ui.HintHint
3240
import com.intellij.ui.MutableCollectionComboBoxModel
3341
import com.intellij.ui.SimpleListCellRenderer
3442
import com.intellij.ui.components.JBLabel
43+
import com.intellij.ui.components.JBList
44+
import com.intellij.ui.components.JBScrollPane
3545
import com.intellij.util.EventDispatcher
3646
import com.intellij.util.ui.JBEmptyBorder
3747
import com.intellij.util.ui.JBUI
3848
import com.intellij.util.ui.UIUtil
3949
import com.intellij.util.ui.components.BorderLayoutPanel
40-
import java.awt.CardLayout
41-
import java.awt.Color
42-
import java.awt.Dimension
43-
import java.awt.Point
50+
import java.awt.*
4451
import java.awt.event.MouseAdapter
4552
import java.awt.event.MouseEvent
4653
import java.util.function.Supplier
47-
import javax.swing.Box
48-
import javax.swing.JComponent
49-
import javax.swing.JPanel
54+
import javax.swing.*
5055
import kotlin.math.max
5156
import kotlin.math.min
5257

58+
data class ModelWrapper(val psiElement: PsiElement, var panel: JPanel? = null, var namePanel: JPanel? = null)
5359
/**
5460
*
5561
*/
@@ -62,6 +68,9 @@ class AutoDevInputSection(private val project: Project, val disposable: Disposab
6268
private val stopButton: ActionButton
6369
private val buttonPanel = JPanel(CardLayout())
6470

71+
private val listModel = DefaultListModel<ModelWrapper>()
72+
private val elementsList = JBList(listModel)
73+
6574
private val defaultRag: CustomAgentConfig = CustomAgentConfig("<Select Custom Agent>", "Normal")
6675
private var customRag: ComboBox<CustomAgentConfig> = ComboBox(MutableCollectionComboBoxModel(listOf()))
6776

@@ -85,6 +94,7 @@ class AutoDevInputSection(private val project: Project, val disposable: Disposab
8594
}
8695

8796
init {
97+
setupElementsList()
8898
val sendButtonPresentation = Presentation(AutoDevBundle.message("chat.panel.send"))
8999
sendButtonPresentation.icon = AutoDevIcons.Send
90100
this.sendButtonPresentation = sendButtonPresentation
@@ -131,7 +141,10 @@ class AutoDevInputSection(private val project: Project, val disposable: Disposab
131141

132142
input.border = JBEmptyBorder(10)
133143

134-
addToCenter(input)
144+
// addToCenter(input)
145+
this.add(input, BorderLayout.CENTER)
146+
this.add(elementsList, BorderLayout.NORTH)
147+
135148
val layoutPanel = BorderLayoutPanel()
136149
val horizontalGlue = Box.createHorizontalGlue()
137150
horizontalGlue.addMouseListener(object : MouseAdapter() {
@@ -174,6 +187,80 @@ class AutoDevInputSection(private val project: Project, val disposable: Disposab
174187
this@AutoDevInputSection.initEditor()
175188
}
176189
})
190+
setupEditorListener()
191+
setupRelatedListener()
192+
}
193+
194+
private fun setupEditorListener() {
195+
project.messageBus.connect(disposable!!).subscribe(
196+
FileEditorManagerListener.FILE_EDITOR_MANAGER,
197+
object : FileEditorManagerListener {
198+
override fun selectionChanged(event: FileEditorManagerEvent) {
199+
val file = event.newFile ?: return
200+
val psiFile = PsiManager.getInstance(project).findFile(file) ?: return
201+
cc.unitmesh.devti.provider.RelatedClassesProvider.provide(psiFile.language) ?: return
202+
ApplicationManager.getApplication().invokeLater {
203+
listModel.addIfAbsent(psiFile)
204+
}
205+
}
206+
}
207+
)
208+
}
209+
210+
private fun setupRelatedListener() {
211+
project.messageBus.connect(disposable!!)
212+
.subscribe(LookupManagerListener.TOPIC, ShireInputLookupManagerListener(project) {
213+
ApplicationManager.getApplication().invokeLater {
214+
val relatedElements = cc.unitmesh.devti.provider.RelatedClassesProvider.provide(it.language)?.lookup(it)
215+
updateElements(relatedElements)
216+
}
217+
})
218+
}
219+
220+
private fun setupElementsList() {
221+
elementsList.selectionMode = ListSelectionModel.SINGLE_SELECTION
222+
elementsList.layoutOrientation = JList.HORIZONTAL_WRAP
223+
elementsList.visibleRowCount = 2
224+
elementsList.cellRenderer = RelatedFileListCellRenderer()
225+
elementsList.setEmptyText("")
226+
227+
val scrollPane = JBScrollPane(elementsList)
228+
scrollPane.preferredSize = Dimension(-1, 80)
229+
scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS
230+
scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
231+
232+
elementsList.addMouseListener(object : MouseAdapter() {
233+
override fun mouseClicked(e: MouseEvent) {
234+
val list = e.source as JBList<*>
235+
val index = list.locationToIndex(e.point)
236+
if (index != -1) {
237+
val wrapper = listModel.getElementAt(index)
238+
val cellBounds = list.getCellBounds(index, index)
239+
wrapper.panel?.components?.firstOrNull { it.contains(e.x - cellBounds.x - it.x, it.height - 1) }?.let {
240+
when {
241+
it is JPanel -> {
242+
listModel.removeElement(wrapper)
243+
wrapper.psiElement.containingFile?.let { psiFile ->
244+
val relativePath = psiFile.virtualFile.relativePath(project)
245+
input.appendText("\n/" + "file" + ":${relativePath}")
246+
listModel.indexOf(wrapper.psiElement).takeIf { it != -1 }?.let { listModel.remove(it) }
247+
val relatedElements = cc.unitmesh.devti.provider.RelatedClassesProvider.provide(psiFile.language)?.lookup(psiFile)
248+
updateElements(relatedElements)
249+
}
250+
}
251+
it is JLabel && it.icon == AllIcons.Actions.Close -> listModel.removeElement(wrapper)
252+
else -> list.clearSelection()
253+
}
254+
} ?: list.clearSelection()
255+
}
256+
}
257+
})
258+
259+
add(scrollPane, BorderLayout.NORTH)
260+
}
261+
262+
private fun updateElements(elements: List<PsiElement>?) {
263+
elements?.forEach { listModel.addIfAbsent(it) }
177264
}
178265

179266
fun showStopButton() {
@@ -290,3 +377,15 @@ class AutoDevInputSection(private val project: Project, val disposable: Disposab
290377

291378
val focusableComponent: JComponent get() = input
292379
}
380+
381+
private fun DefaultListModel<ModelWrapper>.addIfAbsent(psiFile: PsiElement) {
382+
val isValid = when (psiFile) {
383+
is PsiFile -> psiFile.isValid
384+
else -> true
385+
}
386+
if (!isValid) return
387+
388+
if (elements().asIterator().asSequence().none { it.psiElement == psiFile }) {
389+
addElement(ModelWrapper(psiFile))
390+
}
391+
}

0 commit comments

Comments
 (0)