Skip to content

Commit 62de959

Browse files
committed
feat(core): add MCP file editor with preview
#371 - Implement McpEditorProvider to handle .mcp.json files- Create McpFileEditorWithPreview for split editor layout - Develop McpPreviewEditor for previewing MCP content - Add McpPreviewEditorProvider for asynchronous editor creation - Update autodev-core.xml to include new file editor provider
1 parent 1da4ae1 commit 62de959

File tree

5 files changed

+245
-0
lines changed

5 files changed

+245
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
<notificationGroup id="AutoDev.notify" displayType="STICKY_BALLOON" bundle="messages.AutoDevBundle"
6262
key="name"/>
6363

64+
<fileEditorProvider implementation="cc.unitmesh.devti.mcp.editor.McpEditorProvider"/>
65+
6466
<intentionAction>
6567
<className>cc.unitmesh.devti.intentions.AutoDevIntentionHelper</className>
6668
<categoryKey>intention.category.llm</categoryKey>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cc.unitmesh.devti.mcp.editor
2+
3+
import com.intellij.openapi.fileEditor.*
4+
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
5+
import com.intellij.openapi.project.Project
6+
import com.intellij.openapi.vfs.VirtualFile
7+
import com.intellij.testFramework.LightVirtualFile
8+
9+
class McpEditorProvider : WeighedFileEditorProvider() {
10+
override fun getEditorTypeId() = "mcp-split-editor"
11+
private val mainProvider: TextEditorProvider = TextEditorProvider.getInstance()
12+
private val previewProvider: FileEditorProvider = McpPreviewEditorProvider()
13+
14+
override fun accept(project: Project, file: VirtualFile) = file.name.contains(".mcp.json")
15+
16+
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
17+
val editor = TextEditorProvider.getInstance().createEditor(project, file)
18+
if (editor.file is LightVirtualFile) {
19+
return editor
20+
}
21+
22+
val mainEditor = mainProvider.createEditor(project, file) as TextEditor
23+
val preview = previewProvider.createEditor(project, file) as McpPreviewEditor
24+
return McpFileEditorWithPreview(mainEditor, preview, project)
25+
}
26+
27+
override fun getPolicy() = FileEditorPolicy.HIDE_OTHER_EDITORS
28+
}
29+
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cc.unitmesh.devti.mcp.editor
2+
3+
import com.intellij.icons.AllIcons
4+
import com.intellij.openapi.actionSystem.*
5+
import com.intellij.openapi.editor.event.VisibleAreaEvent
6+
import com.intellij.openapi.editor.event.VisibleAreaListener
7+
import com.intellij.openapi.editor.ex.util.EditorUtil
8+
import com.intellij.openapi.editor.impl.EditorImpl
9+
import com.intellij.openapi.fileEditor.TextEditor
10+
import com.intellij.openapi.fileEditor.TextEditorWithPreview
11+
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
12+
import com.intellij.openapi.project.DumbService
13+
import com.intellij.openapi.project.Project
14+
import com.intellij.openapi.vfs.VirtualFile
15+
16+
class McpFileEditorWithPreview(
17+
private val ourEditor: TextEditor,
18+
@JvmField var preview: McpPreviewEditor,
19+
private val project: Project,
20+
) : TextEditorWithPreview(
21+
ourEditor, preview,
22+
"Shire Split Editor",
23+
Layout.SHOW_EDITOR_AND_PREVIEW,
24+
) {
25+
val virtualFile: VirtualFile = ourEditor.file
26+
27+
init {
28+
// allow launching actions while in preview mode;
29+
preview.setMainEditor(ourEditor.editor)
30+
ourEditor.editor.scrollingModel.addVisibleAreaListener(MyVisibleAreaListener(), this)
31+
}
32+
33+
override fun dispose() {
34+
TextEditorProvider.getInstance().disposeEditor(ourEditor)
35+
}
36+
37+
inner class MyVisibleAreaListener : VisibleAreaListener {
38+
private var previousLine = 0
39+
40+
override fun visibleAreaChanged(event: VisibleAreaEvent) {
41+
val editor = event.editor
42+
val y = editor.scrollingModel.verticalScrollOffset
43+
val currentLine = if (editor is EditorImpl) editor.yToVisualLine(y) else y / editor.lineHeight
44+
if (currentLine == previousLine) {
45+
return
46+
}
47+
48+
previousLine = currentLine
49+
preview.scrollToSrcOffset(EditorUtil.getVisualLineEndOffset(editor, currentLine))
50+
}
51+
}
52+
53+
override fun createToolbar(): ActionToolbar {
54+
return ActionManager.getInstance()
55+
.createActionToolbar(ActionPlaces.EDITOR_TOOLBAR, createActionGroup(project), true)
56+
.also {
57+
it.targetComponent = editor.contentComponent
58+
}
59+
}
60+
61+
private fun createActionGroup(project: Project): ActionGroup {
62+
return DefaultActionGroup(
63+
object : AnAction("Preview", "Preview Tip", AllIcons.Actions.Preview) {
64+
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
65+
override fun update(e: AnActionEvent) {
66+
e.presentation.isEnabled = !DumbService.isDumb(project)
67+
}
68+
69+
override fun actionPerformed(e: AnActionEvent) {
70+
DumbService.getInstance(project).runWhenSmart {
71+
preview.component.isVisible = true
72+
preview.updateDisplayedContent()
73+
}
74+
}
75+
},
76+
object : AnAction("Refresh", "Refresh", AllIcons.Actions.Refresh) {
77+
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
78+
override fun update(e: AnActionEvent) {
79+
e.presentation.isEnabled = !DumbService.isDumb(project)
80+
}
81+
82+
override fun actionPerformed(e: AnActionEvent) {
83+
DumbService.getInstance(project).runWhenSmart {
84+
preview.updateDisplayedContent()
85+
}
86+
}
87+
},
88+
// Separator(),
89+
// object : AnAction("Help", "Help", AllIcons.Actions.Help) {
90+
// override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
91+
// override fun actionPerformed(e: AnActionEvent) {
92+
// BrowserUtil.browse(AutoDevBundle.message("editor.preview.help.url"))
93+
// }
94+
// }
95+
)
96+
}
97+
98+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package cc.unitmesh.devti.mcp.editor
2+
3+
import com.intellij.openapi.application.smartReadAction
4+
import com.intellij.openapi.editor.Editor
5+
import com.intellij.openapi.editor.ScrollType
6+
import com.intellij.openapi.fileEditor.FileEditor
7+
import com.intellij.openapi.fileEditor.FileEditorState
8+
import com.intellij.openapi.project.Project
9+
import com.intellij.openapi.util.UserDataHolder
10+
import com.intellij.openapi.util.UserDataHolderBase
11+
import com.intellij.openapi.vfs.VirtualFile
12+
import com.intellij.psi.PsiManager
13+
import com.intellij.ui.JBColor
14+
import com.intellij.ui.components.JBLabel
15+
import com.intellij.ui.components.JBScrollPane
16+
import com.intellij.ui.dsl.builder.Align
17+
import com.intellij.ui.dsl.builder.panel
18+
import com.intellij.util.ui.JBUI
19+
import com.intellij.util.ui.UIUtil
20+
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.launch
22+
import kotlinx.coroutines.runBlocking
23+
import java.awt.BorderLayout
24+
import java.beans.PropertyChangeListener
25+
import javax.swing.JComponent
26+
import javax.swing.JPanel
27+
import javax.swing.ScrollPaneConstants
28+
29+
/**
30+
* Display shire file render prompt and have a sample file as view
31+
*/
32+
open class McpPreviewEditor(
33+
val project: Project,
34+
val virtualFile: VirtualFile,
35+
) : UserDataHolder by UserDataHolderBase(), FileEditor {
36+
val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
37+
private var mainEditor = MutableStateFlow<Editor?>(null)
38+
private val mainPanel = JPanel(BorderLayout())
39+
private val visualPanel: JBScrollPane = JBScrollPane(
40+
mainPanel,
41+
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
42+
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
43+
)
44+
45+
private var editorPanel: JPanel? = null
46+
47+
init {
48+
val corePanel = panel {
49+
row {
50+
val label = JBLabel("MCP Preview (Experimental)").apply {
51+
fontColor = UIUtil.FontColor.BRIGHTER
52+
background = JBColor(0xF5F5F5, 0x2B2D30)
53+
font = JBUI.Fonts.label(16.0f).asBold()
54+
border = JBUI.Borders.empty(0, 16)
55+
isOpaque = true
56+
}
57+
58+
cell(label).align(Align.FILL).resizableColumn()
59+
}
60+
}
61+
62+
this.mainPanel.add(corePanel, BorderLayout.CENTER)
63+
}
64+
65+
fun setMainEditor(editor: Editor) {
66+
check(mainEditor.value == null)
67+
mainEditor.value = editor
68+
}
69+
70+
fun updateDisplayedContent() {
71+
72+
}
73+
74+
fun scrollToSrcOffset(offset: Int) {
75+
76+
}
77+
78+
override fun getComponent(): JComponent = visualPanel
79+
override fun getName(): String = "MCP Preview"
80+
override fun setState(state: FileEditorState) {}
81+
override fun isModified(): Boolean = false
82+
override fun isValid(): Boolean = true
83+
override fun getFile(): VirtualFile = virtualFile
84+
override fun getPreferredFocusedComponent(): JComponent? = null
85+
override fun addPropertyChangeListener(listener: PropertyChangeListener) {}
86+
override fun removePropertyChangeListener(listener: PropertyChangeListener) {}
87+
override fun dispose() {}
88+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cc.unitmesh.devti.mcp.editor
2+
3+
import com.intellij.openapi.fileEditor.AsyncFileEditorProvider
4+
import com.intellij.openapi.fileEditor.FileEditor
5+
import com.intellij.openapi.fileEditor.FileEditorPolicy
6+
import com.intellij.openapi.fileEditor.WeighedFileEditorProvider
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.openapi.vfs.VirtualFile
9+
10+
class McpPreviewEditorProvider : WeighedFileEditorProvider(), AsyncFileEditorProvider {
11+
override fun accept(project: Project, file: VirtualFile) = file.name.contains(".mcp.json")
12+
13+
override fun createEditor(project: Project, virtualFile: VirtualFile): FileEditor {
14+
return McpPreviewEditor(project, virtualFile)
15+
}
16+
17+
override fun createEditorAsync(project: Project, file: VirtualFile): AsyncFileEditorProvider.Builder {
18+
return object : AsyncFileEditorProvider.Builder() {
19+
override fun build(): FileEditor {
20+
return McpPreviewEditor(project, file)
21+
}
22+
}
23+
}
24+
25+
override fun getEditorTypeId(): String = "mcp-preview-editor"
26+
27+
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR
28+
}

0 commit comments

Comments
 (0)