diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdViewHybrid.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdViewHybrid.kt index b8f42a8dabc..1ca17be3614 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdViewHybrid.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/MdViewHybrid.kt @@ -5,8 +5,9 @@ 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.FileTypeRegistry import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType import com.intellij.openapi.project.ProjectManager import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBHtmlPane @@ -42,6 +43,79 @@ internal class MdViewHybrid( ) : MdView { companion object { private val LOG = KiloLog.create(MdViewHybrid::class.java) + + private val FILES = mapOf( + "kt" to "kt", + "kotlin" to "kt", + "js" to "js", + "javascript" to "js", + "jsx" to "jsx", + "ts" to "ts", + "typescript" to "ts", + "tsx" to "tsx", + "java" to "java", + "py" to "py", + "python" to "py", + "sh" to "sh", + "bash" to "sh", + "shell" to "sh", + "json" to "json", + "xml" to "xml", + "html" to "html", + "css" to "css", + "md" to "md", + "markdown" to "md", + "yaml" to "yaml", + "yml" to "yaml", + "toml" to "toml", + "go" to "go", + "golang" to "go", + "rs" to "rs", + "rust" to "rs", + "rb" to "rb", + "ruby" to "rb", + "php" to "php", + "swift" to "swift", + "scala" to "scala", + "sql" to "sql", + "dockerfile" to "dockerfile", + "docker" to "dockerfile", + "gradle" to "gradle", + "kts" to "kts", + "c" to "c", + "h" to "h", + "cpp" to "cpp", + "c++" to "cpp", + "cc" to "cc", + "cxx" to "cxx", + "hpp" to "hpp", + "h++" to "hpp", + "cs" to "cs", + "csharp" to "cs", + "c#" to "cs", + "fs" to "fs", + "fsharp" to "fs", + "f#" to "fs", + "ps1" to "ps1", + "powershell" to "ps1", + "pwsh" to "ps1", + "bat" to "bat", + "batch" to "bat", + "cmd" to "bat", + "makefile" to "makefile", + "make" to "makefile", + "terraform" to "tf", + "tf" to "tf", + "hcl" to "hcl", + "vue" to "vue", + "svelte" to "svelte", + "graphql" to "graphql", + "proto" to "proto", + "ini" to "ini", + "properties" to "properties", + "diff" to "diff", + "patch" to "patch", + ) } private val listeners = mutableListOf() @@ -409,24 +483,11 @@ internal class MdViewHybrid( } private fun file(lang: String?): FileType { - val key = lang?.trim()?.substringBefore(' ')?.lowercase().orEmpty() - val ext = when (key) { - "kt", "kotlin" -> "kt" - "js", "javascript" -> "js" - "ts", "typescript" -> "ts" - "tsx" -> "tsx" - "java" -> "java" - "py", "python" -> "py" - "sh", "bash", "shell" -> "sh" - "json" -> "json" - "xml" -> "xml" - "html" -> "html" - "css" -> "css" - "md", "markdown" -> "md" - else -> "" - } - if (ext.isEmpty()) return PlainTextFileType.INSTANCE - return FileTypeManager.getInstance().getFileTypeByExtension(ext) + val key = lang?.trim()?.split(Regex("\\s+"))?.firstOrNull()?.lowercase().orEmpty() + val ext = FILES[key] ?: return PlainTextFileType.INSTANCE + val type = FileTypeRegistry.getInstance().getFileTypeByExtension(ext) + if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE + return type } private fun dispatch(event: MdView.LinkEvent) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt index 0e9670737c5..1c821b16ea4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt @@ -2,6 +2,10 @@ 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.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.FileTypeRegistry +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.EditorTextField import com.intellij.ui.components.JBScrollPane @@ -143,6 +147,36 @@ class MdViewHybridTest : BasePlatformTestCase() { assertTrue("java code block should not clip vertically", pane.height >= pane.preferredSize.height) } + fun `test fenced code block resolves existing language aliases`() { + view.set("```javascript\nconst value = 1\n```") + + assertSame(type("js"), editors().single().fileType) + } + + fun `test fenced code block ignores whitespace metadata`() { + view.set("```json title=\"sample.json\"\n{\"value\":1}\n```") + + assertSame(type("json"), editors().single().fileType) + } + + fun `test fenced code block resolves new aliases when available`() { + view.set("```yaml\nvalue: 1\n```") + + assertSame(type("yaml"), editors().single().fileType) + } + + fun `test unknown fenced code language uses plain text`() { + view.set("```definitely-not-a-language\nvalue\n```") + + assertSame(PlainTextFileType.INSTANCE, editors().single().fileType) + } + + fun `test code block without language uses plain text`() { + view.set("```\nvalue\n```") + + assertSame(PlainTextFileType.INSTANCE, editors().single().fileType) + } + fun `test fenced code block width is bounded and boxed`() { view.set("```kotlin\n${"x".repeat(500)}\n```") val pane = scrolls().single() @@ -200,6 +234,12 @@ class MdViewHybridTest : BasePlatformTestCase() { private fun editors(): List = scrolls().mapNotNull { it.viewport.view as? EditorTextField } + private fun type(ext: String): FileType { + val type = FileTypeRegistry.getInstance().getFileTypeByExtension(ext) + if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE + return type + } + private fun layout(width: Int) { val host = JPanel(BorderLayout()) host.add(view.component, BorderLayout.CENTER)