Skip to content

Commit db345a2

Browse files
CopilotMte90
andauthored
Fix IDE plugin UI: dark mode support and markdown rendering (#28)
Co-authored-by: Mte90 <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent e0fc766 commit db345a2

File tree

1 file changed

+148
-9
lines changed

1 file changed

+148
-9
lines changed

ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowContent.kt

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,49 @@ import com.intellij.openapi.application.ApplicationManager
44
import com.intellij.openapi.project.Project
55
import com.intellij.ui.components.JBScrollPane
66
import com.intellij.ui.components.JBTextArea
7+
import com.intellij.ui.JBColor
78
import java.awt.BorderLayout
89
import java.awt.Dimension
910
import java.awt.FlowLayout
1011
import javax.swing.*
12+
import javax.swing.text.html.HTMLEditorKit
1113
import java.net.HttpURLConnection
1214
import java.net.URL
1315
import com.google.gson.Gson
1416
import com.google.gson.JsonObject
1517
import com.google.gson.JsonArray
1618

19+
/**
20+
* Custom JEditorPane that tracks viewport width for proper HTML wrapping
21+
*/
22+
class WrappingEditorPane : JEditorPane() {
23+
override fun getScrollableTracksViewportWidth(): Boolean = true
24+
25+
override fun getPreferredSize(): Dimension {
26+
// Let the parent determine the width, we only care about height
27+
val preferredSize = super.getPreferredSize()
28+
29+
// If we're in a scroll pane, use the viewport width
30+
val parent = parent
31+
if (parent is JViewport) {
32+
val viewportWidth = parent.width
33+
if (viewportWidth > 0) {
34+
// Set a temporary size to calculate the proper height
35+
setSize(viewportWidth, Int.MAX_VALUE)
36+
preferredSize.width = viewportWidth
37+
preferredSize.height = super.getPreferredSize().height
38+
}
39+
}
40+
return preferredSize
41+
}
42+
43+
override fun getMaximumSize(): Dimension {
44+
val maxSize = super.getMaximumSize()
45+
maxSize.width = Integer.MAX_VALUE
46+
return maxSize
47+
}
48+
}
49+
1750
/**
1851
* PicoCode RAG Chat Window
1952
* Simple chat interface that communicates with PicoCode backend
@@ -157,24 +190,121 @@ class PicoCodeToolWindowContent(private val project: Project) {
157190
return panel
158191
}
159192

193+
/**
194+
* Convert markdown to HTML for rendering
195+
* Note: Code block backgrounds use light gray which may need adjustment for dark themes
196+
*/
197+
private fun markdownToHtml(markdown: String): String {
198+
var html = markdown
199+
200+
// Process markdown constructs before escaping HTML
201+
// Code blocks (```) - preserve content as-is
202+
val codeBlockPlaceholders = mutableListOf<String>()
203+
html = html.replace(Regex("```([\\s\\S]*?)```")) { matchResult ->
204+
val content = matchResult.groupValues[1]
205+
val placeholder = "###CODEBLOCK${codeBlockPlaceholders.size}###"
206+
codeBlockPlaceholders.add(content)
207+
placeholder
208+
}
209+
210+
// Inline code (`) - preserve content
211+
val inlineCodePlaceholders = mutableListOf<String>()
212+
html = html.replace(Regex("`([^`]+)`")) { matchResult ->
213+
val content = matchResult.groupValues[1]
214+
val placeholder = "###INLINECODE${inlineCodePlaceholders.size}###"
215+
inlineCodePlaceholders.add(content)
216+
placeholder
217+
}
218+
219+
// Escape HTML special characters in remaining text
220+
html = html
221+
.replace("&", "&amp;")
222+
.replace("<", "&lt;")
223+
.replace(">", "&gt;")
224+
225+
// Apply markdown formatting
226+
html = html
227+
// Bold (**text**)
228+
.replace(Regex("\\*\\*([^*]+)\\*\\*"), "<strong>$1</strong>")
229+
// Italic (*text*)
230+
.replace(Regex("\\*([^*]+)\\*"), "<em>$1</em>")
231+
// Headers
232+
.replace(Regex("^### (.+)$", RegexOption.MULTILINE), "<h3>$1</h3>")
233+
.replace(Regex("^## (.+)$", RegexOption.MULTILINE), "<h2>$1</h2>")
234+
.replace(Regex("^# (.+)$", RegexOption.MULTILINE), "<h1>$1</h1>")
235+
// Lists
236+
.replace(Regex("^- (.+)$", RegexOption.MULTILINE), "<li>$1</li>")
237+
.replace(Regex("^\\* (.+)$", RegexOption.MULTILINE), "<li>$1</li>")
238+
239+
// Restore code blocks with proper styling and wrapping
240+
codeBlockPlaceholders.forEachIndexed { index, content ->
241+
val escapedContent = content
242+
.replace("&", "&amp;")
243+
.replace("<", "&lt;")
244+
.replace(">", "&gt;")
245+
html = html.replace("###CODEBLOCK${index}###",
246+
"<pre style='background-color: rgba(127, 127, 127, 0.1); padding: 8px; border-radius: 4px; border: 1px solid rgba(127, 127, 127, 0.2); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word;'><code>$escapedContent</code></pre>")
247+
}
248+
249+
// Restore inline code with proper styling and wrapping
250+
inlineCodePlaceholders.forEachIndexed { index, content ->
251+
val escapedContent = content
252+
.replace("&", "&amp;")
253+
.replace("<", "&lt;")
254+
.replace(">", "&gt;")
255+
html = html.replace("###INLINECODE${index}###",
256+
"<code style='background-color: rgba(127, 127, 127, 0.15); padding: 2px 4px; border-radius: 3px; word-wrap: break-word; overflow-wrap: break-word;'>$escapedContent</code>")
257+
}
258+
259+
// Wrap consecutive list items in <ul> tags
260+
html = html.replace(Regex("(<li>.*?</li>(?:<br/>)?)+")) { matchResult ->
261+
"<ul>${matchResult.value.replace("<br/>", "")}</ul>"
262+
}
263+
264+
// Convert line breaks (but not inside pre/code tags)
265+
html = html.replace("\n", "<br/>")
266+
267+
return "<html><body style='font-family: sans-serif; font-size: 11px; width: 100%; word-wrap: break-word; overflow-wrap: break-word;'>$html</body></html>"
268+
}
269+
160270
private fun renderChatHistory() {
161271
chatPanel.removeAll()
162272

163273
for ((index, msg) in chatHistory.withIndex()) {
164274
val messagePanel = JPanel(BorderLayout())
275+
276+
// Ensure messagePanel respects the container width
277+
messagePanel.maximumSize = Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)
278+
279+
// Use theme-aware colors
280+
val borderColor = if (msg.sender == "You")
281+
JBColor.BLUE
282+
else
283+
JBColor.GRAY
284+
165285
messagePanel.border = BorderFactory.createCompoundBorder(
166286
BorderFactory.createEmptyBorder(5, 5, 5, 5),
167-
BorderFactory.createLineBorder(if (msg.sender == "You") java.awt.Color.BLUE else java.awt.Color.GRAY, 1)
287+
BorderFactory.createLineBorder(borderColor, 1)
168288
)
169289

170-
val textArea = JBTextArea(msg.message)
171-
textArea.isEditable = false
172-
textArea.lineWrap = true
173-
textArea.wrapStyleWord = true
174-
textArea.background = if (msg.sender == "You") java.awt.Color(230, 240, 255) else java.awt.Color.WHITE
290+
// Use JEditorPane for HTML/Markdown rendering with proper width tracking
291+
val editorPane = WrappingEditorPane()
292+
editorPane.contentType = "text/html"
293+
editorPane.editorKit = HTMLEditorKit()
294+
editorPane.text = markdownToHtml(msg.message)
295+
editorPane.isEditable = false
296+
editorPane.isOpaque = true
297+
298+
// Use theme-aware background colors
299+
editorPane.background = if (msg.sender == "You")
300+
JBColor.namedColor("EditorPane.inactiveBackground", JBColor(0xE6F0FF, 0x2D3239))
301+
else
302+
JBColor.namedColor("EditorPane.background", JBColor.background())
303+
304+
editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true)
175305

176306
val headerPanel = JPanel(BorderLayout())
177-
headerPanel.add(JLabel("[$msg.sender]"), BorderLayout.WEST)
307+
headerPanel.add(JLabel("[${msg.sender}]"), BorderLayout.WEST)
178308

179309
// Add delete button for each message
180310
val deleteBtn = JButton("×")
@@ -186,7 +316,15 @@ class PicoCodeToolWindowContent(private val project: Project) {
186316
headerPanel.add(deleteBtn, BorderLayout.EAST)
187317

188318
messagePanel.add(headerPanel, BorderLayout.NORTH)
189-
messagePanel.add(textArea, BorderLayout.CENTER)
319+
320+
// Wrap editorPane in a scroll pane for long messages
321+
val messageScrollPane = JBScrollPane(editorPane)
322+
messageScrollPane.border = null
323+
messageScrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
324+
messageScrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
325+
// Set maximum height to prevent messages from becoming too tall
326+
messageScrollPane.maximumSize = Dimension(Integer.MAX_VALUE, 300)
327+
messagePanel.add(messageScrollPane, BorderLayout.CENTER)
190328

191329
// Add context information if available
192330
if (msg.contexts.isNotEmpty()) {
@@ -196,7 +334,8 @@ class PicoCodeToolWindowContent(private val project: Project) {
196334
}
197335
val contextArea = JBTextArea(contextText.toString())
198336
contextArea.isEditable = false
199-
contextArea.background = java.awt.Color(250, 250, 250)
337+
// Use theme-aware background for context
338+
contextArea.background = JBColor.namedColor("Panel.background", JBColor(0xFAFAFA, 0x3C3F41))
200339
messagePanel.add(contextArea, BorderLayout.SOUTH)
201340
}
202341

0 commit comments

Comments
 (0)