mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): expand markdown code blocks
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show JetBrains markdown code blocks as multiline boxed editors that expand to their full height.
|
||||
+14
@@ -79,6 +79,20 @@ object SessionUiStyle {
|
||||
const val USER_BORDER_HORIZONTAL_PADDING = 12
|
||||
}
|
||||
|
||||
/** Markdown code block geometry inside assistant messages. */
|
||||
object Code {
|
||||
const val BLOCK_GAP = 6
|
||||
const val MIN_ROWS = 1
|
||||
const val BORDER_WIDTH = 1
|
||||
const val VIEWPORT_TOP_PADDING = 6
|
||||
const val VIEWPORT_HORIZONTAL_PADDING = 8
|
||||
const val VIEWPORT_BOTTOM_PADDING = 0
|
||||
const val SCROLLBAR_HEIGHT = 12
|
||||
const val WIDTH_PADDING = 16
|
||||
|
||||
fun topPadding(): Int = VIEWPORT_TOP_PADDING + SCROLLBAR_HEIGHT
|
||||
}
|
||||
|
||||
/** Permission card command preview limits. */
|
||||
object Permission {
|
||||
const val COMMAND_LINES = 3
|
||||
|
||||
+70
-9
@@ -1,7 +1,9 @@
|
||||
package ai.kilocode.client.ui.md
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.fileTypes.FileType
|
||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
@@ -24,6 +26,7 @@ import org.commonmark.node.Node
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
@@ -265,7 +268,7 @@ internal class MdViewHybrid(
|
||||
|
||||
private fun addGap() {
|
||||
if (root.componentCount == 0) return
|
||||
root.add(Box.createVerticalStrut(JBUI.scale(6)))
|
||||
root.add(Box.createVerticalStrut(JBUI.scale(SessionUiStyle.View.Code.BLOCK_GAP)))
|
||||
}
|
||||
|
||||
private fun addBlock(component: JComponent) {
|
||||
@@ -301,20 +304,70 @@ internal class MdViewHybrid(
|
||||
|
||||
private fun codeBlock(text: String, lang: String?): JComponent {
|
||||
val opts = opts()
|
||||
val value = text.trimEnd('\n')
|
||||
val field = runCatching { CodeField(file(lang), opts, text) }.getOrElse { err ->
|
||||
LOG.warn("kind=markdown codeEditor=true failed message=${err.message}", err)
|
||||
textArea(text, opts)
|
||||
}
|
||||
return JBScrollPane(field).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
val height = codeHeight(field, value)
|
||||
val width = codeWidth(field, value)
|
||||
field.preferredSize = Dimension(width, height)
|
||||
field.minimumSize = Dimension(0, height)
|
||||
field.maximumSize = Dimension(Int.MAX_VALUE, height)
|
||||
return object : JBScrollPane(field) {
|
||||
override fun doLayout() {
|
||||
super.doLayout()
|
||||
val view = viewport.view ?: return
|
||||
val size = viewport.extentSize
|
||||
if (size.height <= 0 || view.height == size.height) return
|
||||
view.setSize(view.width.coerceAtLeast(size.width), size.height)
|
||||
}
|
||||
}.apply {
|
||||
border = JBUI.Borders.customLine(opts.tableBorder, SessionUiStyle.View.Code.BORDER_WIDTH)
|
||||
viewportBorder = JBUI.Borders.empty(
|
||||
SessionUiStyle.View.Code.topPadding(),
|
||||
SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING,
|
||||
SessionUiStyle.View.Code.VIEWPORT_BOTTOM_PADDING,
|
||||
SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING,
|
||||
)
|
||||
isOpaque = opts.opaque
|
||||
background = opts.preBg
|
||||
viewport.background = opts.preBg
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
isWheelScrollingEnabled = true
|
||||
setOverlappingScrollBar(false)
|
||||
horizontalScrollBar.preferredSize = Dimension(0, JBUI.scale(SessionUiStyle.View.Code.SCROLLBAR_HEIGHT))
|
||||
horizontalScrollBar.isOpaque = true
|
||||
verticalScrollBar.preferredSize = JBUI.emptySize()
|
||||
val pad = viewportBorder.getBorderInsets(this)
|
||||
val size = height + insets.top + insets.bottom + pad.top + pad.bottom + horizontalScrollBar.preferredSize.height
|
||||
preferredSize = Dimension(0, size)
|
||||
minimumSize = Dimension(0, size)
|
||||
maximumSize = Dimension(Int.MAX_VALUE, size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun codeWidth(component: JComponent, text: String): Int {
|
||||
val metrics = component.getFontMetrics(component.font)
|
||||
val width = text.lineSequence().maxOfOrNull { metrics.stringWidth(it) } ?: 0
|
||||
return width + JBUI.scale(SessionUiStyle.View.Code.WIDTH_PADDING)
|
||||
}
|
||||
|
||||
private fun codeHeight(component: JComponent, text: String): Int {
|
||||
val count = text.lineSequence().count()
|
||||
val rows = count.coerceAtLeast(SessionUiStyle.View.Code.MIN_ROWS)
|
||||
val field = component as? CodeField
|
||||
if (field != null) {
|
||||
field.ensureWillComputePreferredSize()
|
||||
val ed = field.getEditor(false)
|
||||
val line = ed?.lineHeight ?: component.getFontMetrics(component.font).height
|
||||
return maxOf(field.preferredSize.height, line * rows)
|
||||
}
|
||||
val line = component.getFontMetrics(component.font).height
|
||||
return line * rows
|
||||
}
|
||||
|
||||
private fun textArea(text: String, opts: MdStyle) = JBTextArea(text.trimEnd('\n')).apply {
|
||||
isEditable = false
|
||||
lineWrap = false
|
||||
@@ -322,16 +375,23 @@ internal class MdViewHybrid(
|
||||
background = opts.preBg
|
||||
foreground = opts.preFg
|
||||
font = Font(opts.codeFont, Font.PLAIN, style.editorSize)
|
||||
border = JBUI.Borders.empty(6, 8)
|
||||
border = JBUI.Borders.empty(
|
||||
SessionUiStyle.View.Code.VIEWPORT_TOP_PADDING,
|
||||
SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING,
|
||||
)
|
||||
}
|
||||
|
||||
private inner class CodeField(file: FileType, opts: MdStyle, value: String) :
|
||||
com.intellij.ui.EditorTextField(ProjectManager.getInstance().defaultProject, file) {
|
||||
com.intellij.ui.EditorTextField(
|
||||
EditorFactory.getInstance().createDocument(value.trimEnd('\n')),
|
||||
ProjectManager.getInstance().defaultProject,
|
||||
file,
|
||||
true,
|
||||
false,
|
||||
) {
|
||||
init {
|
||||
setFontInheritedFromLAF(false)
|
||||
font = Font(opts.codeFont, Font.PLAIN, style.editorSize)
|
||||
text = value.trimEnd('\n')
|
||||
isViewer = true
|
||||
addSettingsProvider { ed ->
|
||||
style.applyToEditor(ed)
|
||||
ed.setBorder(JBUI.Borders.empty())
|
||||
@@ -342,7 +402,8 @@ internal class MdViewHybrid(
|
||||
ed.scrollPane.viewport.background = opts.preBg
|
||||
ed.settings.isUseSoftWraps = false
|
||||
ed.settings.isAdditionalPageAtBottom = false
|
||||
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
|
||||
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+135
-1
@@ -1,8 +1,11 @@
|
||||
package ai.kilocode.client.ui.md
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@@ -27,11 +30,131 @@ class MdViewHybridTest : BasePlatformTestCase() {
|
||||
assertTrue(view.html().contains("<strong>"))
|
||||
}
|
||||
|
||||
fun `test fenced code block creates horizontal scroll pane`() {
|
||||
fun `test fenced code block shows horizontal scrollbar as needed`() {
|
||||
view.set("```kotlin\nval value = 1\n```")
|
||||
val pane = scrolls().single()
|
||||
|
||||
assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, pane.horizontalScrollBarPolicy)
|
||||
assertEquals(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, pane.verticalScrollBarPolicy)
|
||||
assertTrue(pane.isWheelScrollingEnabled)
|
||||
assertTrue(pane.horizontalScrollBar.preferredSize.height > 0)
|
||||
assertTrue(pane.horizontalScrollBar.isOpaque)
|
||||
assertFalse(pane.isOverlappingScrollBar)
|
||||
assertEquals(0, pane.verticalScrollBar.preferredSize.width)
|
||||
}
|
||||
|
||||
fun `test fenced code block preserves multiline editor text and height`() {
|
||||
view.set("```kotlin\nval one = 1\nval two = 2\nval three = 3\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
val line = editor.getFontMetrics(editor.font).height
|
||||
val ins = pane.insets
|
||||
val pad = pane.viewportBorder.getBorderInsets(pane)
|
||||
val bar = pane.horizontalScrollBar.preferredSize.height
|
||||
|
||||
assertEquals("val one = 1\nval two = 2\nval three = 3", editor.text)
|
||||
assertEquals(editor.preferredSize.height + ins.top + ins.bottom + pad.top + pad.bottom + bar, pane.preferredSize.height)
|
||||
assertTrue(pane.preferredSize.height >= line * 3)
|
||||
}
|
||||
|
||||
fun `test fenced code block horizontal scrollbar has no bottom padding`() {
|
||||
view.set("```kotlin\n${"x".repeat(500)}\n```")
|
||||
val pane = scrolls().single()
|
||||
val pad = pane.viewportBorder.getBorderInsets(pane)
|
||||
|
||||
assertEquals(SessionUiStyle.View.Code.VIEWPORT_BOTTOM_PADDING, pad.bottom)
|
||||
assertTrue(pane.horizontalScrollBar.preferredSize.height > 0)
|
||||
}
|
||||
|
||||
fun `test short code block top padding balances hidden scrollbar space`() {
|
||||
view.set("```text\n[ALICE, ANNA]\n```")
|
||||
val pane = scrolls().single()
|
||||
val pad = pane.viewportBorder.getBorderInsets(pane)
|
||||
|
||||
assertTrue(pad.top > pane.horizontalScrollBar.preferredSize.height)
|
||||
assertEquals(SessionUiStyle.View.Code.VIEWPORT_BOTTOM_PADDING, pad.bottom)
|
||||
}
|
||||
|
||||
fun `test fenced code block height is not capped`() {
|
||||
val code = (1..24).joinToString("\n") { "val value$it = $it" }
|
||||
view.set("```kotlin\n$code\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
val line = editor.getFontMetrics(editor.font).height
|
||||
|
||||
assertTrue(pane.preferredSize.height >= line * 24)
|
||||
}
|
||||
|
||||
fun `test fenced code block lays out to full editor height`() {
|
||||
val code = (1..30).joinToString("\n") { "val value$it = $it" }
|
||||
view.set("```kotlin\n$code\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
|
||||
layout(width = 420)
|
||||
|
||||
assertEquals(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, pane.verticalScrollBarPolicy)
|
||||
assertTrue("scroll pane should use its full preferred height", pane.height >= pane.preferredSize.height)
|
||||
assertTrue("editor should not be clipped vertically", editor.height >= editor.preferredSize.height)
|
||||
}
|
||||
|
||||
fun `test streaming fenced code block grows to full height`() {
|
||||
view.append("```java\nclass Example {\n")
|
||||
val initial = scrolls().single().preferredSize.height
|
||||
|
||||
view.append((1..20).joinToString("\n", postfix = "\n") { "void method$it() {}" })
|
||||
view.append("}\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
layout(width = 420)
|
||||
|
||||
assertTrue("code block should grow after streamed lines", pane.preferredSize.height > initial)
|
||||
assertTrue("streamed editor should not be clipped vertically", editor.height >= editor.preferredSize.height)
|
||||
}
|
||||
|
||||
fun `test java code block with blank lines fits vertically`() {
|
||||
val code = """
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StreamsExample {
|
||||
public static void main(String[] args) {
|
||||
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Anna");
|
||||
|
||||
List<String> result = names.stream()
|
||||
.filter(name -> name.startsWith("A"))
|
||||
.map(String::toUpperCase)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println(result);
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
view.set("```java\n$code\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
|
||||
layout(width = 420)
|
||||
|
||||
val line = editor.getFontMetrics(editor.font).height
|
||||
val rows = editor.text.lineSequence().count()
|
||||
assertTrue("java editor should reserve every document line", editor.preferredSize.height >= line * rows)
|
||||
assertTrue("java editor should not be clipped vertically", editor.height >= editor.preferredSize.height)
|
||||
assertTrue("java code block should not clip vertically", pane.height >= pane.preferredSize.height)
|
||||
}
|
||||
|
||||
fun `test fenced code block width is bounded and boxed`() {
|
||||
view.set("```kotlin\n${"x".repeat(500)}\n```")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
val ins = pane.border.getBorderInsets(pane)
|
||||
|
||||
assertEquals(0, pane.preferredSize.width)
|
||||
assertTrue(editor.preferredSize.width > pane.preferredSize.width)
|
||||
assertTrue(pane.maximumSize.width > 1000)
|
||||
assertTrue(ins.top > 0)
|
||||
assertTrue(ins.left > 0)
|
||||
assertEquals(pane.background, pane.viewport.background)
|
||||
}
|
||||
|
||||
fun `test clear resets source and components`() {
|
||||
@@ -74,4 +197,15 @@ class MdViewHybridTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
private fun scrolls(): List<JBScrollPane> = (view.component as JPanel).components.filterIsInstance<JBScrollPane>()
|
||||
|
||||
private fun editors(): List<EditorTextField> = scrolls().mapNotNull { it.viewport.view as? EditorTextField }
|
||||
|
||||
private fun layout(width: Int) {
|
||||
val host = JPanel(BorderLayout())
|
||||
host.add(view.component, BorderLayout.CENTER)
|
||||
host.setSize(width, view.component.preferredSize.height)
|
||||
host.doLayout()
|
||||
view.component.doLayout()
|
||||
scrolls().forEach { it.doLayout() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user