@@ -4,16 +4,49 @@ import com.intellij.openapi.application.ApplicationManager
44import com.intellij.openapi.project.Project
55import com.intellij.ui.components.JBScrollPane
66import com.intellij.ui.components.JBTextArea
7+ import com.intellij.ui.JBColor
78import java.awt.BorderLayout
89import java.awt.Dimension
910import java.awt.FlowLayout
1011import javax.swing.*
12+ import javax.swing.text.html.HTMLEditorKit
1113import java.net.HttpURLConnection
1214import java.net.URL
1315import com.google.gson.Gson
1416import com.google.gson.JsonObject
1517import 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(" &" , " &" )
222+ .replace(" <" , " <" )
223+ .replace(" >" , " >" )
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(" &" , " &" )
243+ .replace(" <" , " <" )
244+ .replace(" >" , " >" )
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(" &" , " &" )
253+ .replace(" <" , " <" )
254+ .replace(" >" , " >" )
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