diff --git a/plugins/markdown/core/resources/META-INF/plugin.xml b/plugins/markdown/core/resources/META-INF/plugin.xml
index e391045543f1b..0115ad5dc9737 100644
--- a/plugins/markdown/core/resources/META-INF/plugin.xml
+++ b/plugins/markdown/core/resources/META-INF/plugin.xml
@@ -180,6 +180,7 @@
+
diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/MarkdownCharacterGridCustomizer.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/MarkdownCharacterGridCustomizer.kt
new file mode 100644
index 0000000000000..cabc06b49c4ea
--- /dev/null
+++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/MarkdownCharacterGridCustomizer.kt
@@ -0,0 +1,23 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.intellij.plugins.markdown.editor
+
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.openapi.editor.impl.view.DoubleWidthCharacterStrategy
+import com.intellij.openapi.fileEditor.TextEditor
+import com.intellij.openapi.fileEditor.impl.text.TextEditorCustomizer
+import org.intellij.plugins.markdown.editor.tables.TableCharacterWidthUtils
+import org.intellij.plugins.markdown.lang.hasMarkdownType
+
+internal class MarkdownCharacterGridCustomizer : TextEditorCustomizer {
+ override suspend fun execute(textEditor: TextEditor) {
+ if (!textEditor.file.hasMarkdownType()) return
+
+ val editor = textEditor.editor as? EditorImpl ?: return
+ editor.settings.characterGridWidthMultiplier = 1.0f
+
+ val grid = editor.characterGrid ?: return
+ grid.doubleWidthCharacterStrategy = DoubleWidthCharacterStrategy { codePoint ->
+ TableCharacterWidthUtils.isFullWidthCharacter(codePoint)
+ }
+ }
+}
diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtils.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtils.kt
new file mode 100644
index 0000000000000..b22260a28bf8c
--- /dev/null
+++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtils.kt
@@ -0,0 +1,112 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package org.intellij.plugins.markdown.editor.tables
+
+/**
+ * Utility class for calculating character display width in Markdown tables.
+ * Handles proper width calculation for CJK and other full-width characters.
+ */
+internal object TableCharacterWidthUtils {
+
+ /**
+ * Calculates the display width of a string, considering different character widths.
+ * Full-width characters (CJK, etc.) are counted as 2 units,
+ * while half-width characters (ASCII, etc.) are counted as 1 unit.
+ *
+ * @param text The text to measure
+ * @return The display width in character units
+ */
+ fun calculateDisplayWidth(text: String): Int {
+ if (text.isEmpty()) return 0
+
+ var width = 0
+ var i = 0
+ while (i < text.length) {
+ val codePoint = text.codePointAt(i)
+ width += getCharacterWidth(codePoint)
+ i += Character.charCount(codePoint)
+ }
+ return width
+ }
+
+ private fun getCharacterWidth(codePoint: Int): Int {
+ return when {
+ // ASCII printable characters (half-width)
+ codePoint in 0x20..0x7E -> 1
+
+ // Control characters
+ codePoint < 0x20 -> 0
+
+ // Full-width characters (CJK, emoji, etc.)
+ isFullWidthCharacter(codePoint) -> 2
+
+ // Default to 1 for other characters (Latin Extended, Cyrillic, etc.)
+ else -> 1
+ }
+ }
+
+ internal fun isFullWidthCharacter(codePoint: Int): Boolean {
+ return when {
+ // CJK Unified Ideographs
+ codePoint in 0x4E00..0x9FFF -> true
+
+ // CJK Extension A
+ codePoint in 0x3400..0x4DBF -> true
+
+ // CJK Extension B
+ codePoint in 0x20000..0x2A6DF -> true
+
+ // CJK Extension C
+ codePoint in 0x2A700..0x2B73F -> true
+
+ // CJK Extension D
+ codePoint in 0x2B740..0x2B81F -> true
+
+ // CJK Extension E
+ codePoint in 0x2B820..0x2CEAF -> true
+
+ // CJK Extension F
+ codePoint in 0x2CEB0..0x2EBEF -> true
+
+ // CJK Compatibility Ideographs
+ codePoint in 0xF900..0xFAFF -> true
+
+ // CJK Compatibility Ideographs Supplement
+ codePoint in 0x2F800..0x2FA1F -> true
+
+ // CJK Symbols and Punctuation (ideographic space, 、。「」〈〉 etc.)
+ codePoint in 0x3000..0x303F -> true
+
+ // Hiragana
+ codePoint in 0x3040..0x309F -> true
+
+ // Katakana
+ codePoint in 0x30A0..0x30FF -> true
+
+ // Katakana Phonetic Extensions
+ codePoint in 0x31F0..0x31FF -> true
+
+ // Hangul Syllables
+ codePoint in 0xAC00..0xD7AF -> true
+
+ // Hangul Jamo (only leading consonants are Wide per Unicode EAW)
+ codePoint in 0x1100..0x115F -> true
+
+ // Hangul Jamo Extended-A
+ codePoint in 0xA960..0xA97F -> true
+
+ // Fullwidth ASCII variants (FF01-FF60) and Fullwidth brackets (FF5F-FF60)
+ codePoint in 0xFF01..0xFF60 -> true
+
+ // Fullwidth signs (FFE0-FFE6)
+ codePoint in 0xFFE0..0xFFE6 -> true
+
+ // Emoji
+ codePoint in 0x1F600..0x1F64F -> true // Emoticons
+ codePoint in 0x1F300..0x1F5FF -> true // Misc Symbols and Pictographs
+ codePoint in 0x1F680..0x1F6FF -> true // Transport and Map
+ codePoint in 0x1F1E0..0x1F1FF -> true // Regional Indicator Symbols
+
+ else -> false
+ }
+ }
+}
diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableFormattingUtils.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableFormattingUtils.kt
index f6b0a6dbe59b7..f21177df5afd6 100644
--- a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableFormattingUtils.kt
+++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableFormattingUtils.kt
@@ -64,8 +64,10 @@ object TableFormattingUtils {
): Int {
val trimToMaxContent = trimToMaxContent && !cells.all { it.text.isBlank() }
val contentCellsWidth = when {
- trimToMaxContent -> cellsContentsWithCarets.asSequence().map { it.trimmedContentWithoutCarets }.maxOfOrNull { it.length + 2 }
- else -> cells.maxOfOrNull { it.textRange.length }
+ trimToMaxContent -> cellsContentsWithCarets.asSequence().map { it.trimmedContentWithoutCarets }.maxOfOrNull {
+ TableCharacterWidthUtils.calculateDisplayWidth(it) + 2
+ }
+ else -> cells.maxOfOrNull { TableCharacterWidthUtils.calculateDisplayWidth(it.text) }
}
checkNotNull(contentCellsWidth)
return max(contentCellsWidth, separatorCellRange?.length ?: 1)
@@ -93,12 +95,12 @@ object TableFormattingUtils {
) {
val expectedContent = TableModificationUtils.buildRealignedCellContent(
state.trimmedContentWithCarets,
- maxCellWidth + state.caretsInside.size,
+ maxCellWidth,
alignment
)
val range = cell.textRange
val cellContent = document.charsSequence.substring(range.startOffset, range.endOffset)
- if (preventExpand && cellContent.length < maxCellWidth) {
+ if (preventExpand && TableCharacterWidthUtils.calculateDisplayWidth(cellContent) < maxCellWidth) {
return
}
val expectedContentWithoutCarets = expectedContent.replace(TableProps.CARET_REPLACE_CHAR.toString(), "")
diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableModificationUtils.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableModificationUtils.kt
index c8f6739c8b2b8..7e7d40f127bb0 100644
--- a/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableModificationUtils.kt
+++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/editor/tables/TableModificationUtils.kt
@@ -48,7 +48,7 @@ object TableModificationUtils {
}
private fun getCellPotentialWidth(cellText: String): Int {
- var width = cellText.length
+ var width = TableCharacterWidthUtils.calculateDisplayWidth(cellText)
if (!cellText.startsWith(' ')) {
width += 1
}
@@ -84,7 +84,7 @@ object TableModificationUtils {
}
return cells.all {
val selfWidth = getCellPotentialWidth(it.text)
- it.hasCorrectPadding() && selfWidth == it.textRange.length && selfWidth == width
+ it.hasCorrectPadding() && selfWidth == TableCharacterWidthUtils.calculateDisplayWidth(it.text) && selfWidth == width
}
}
@@ -142,12 +142,14 @@ object TableModificationUtils {
}
fun buildRealignedCellContent(cellContent: String, wholeCellWidth: Int, alignment: CellAlignment): String {
- check(wholeCellWidth >= cellContent.length)
+ val contentDisplayWidth = TableCharacterWidthUtils.calculateDisplayWidth(cellContent)
+ check(wholeCellWidth >= contentDisplayWidth)
+ val paddingNeeded = wholeCellWidth - contentDisplayWidth
return when (alignment) {
- CellAlignment.RIGHT -> "${" ".repeat((wholeCellWidth - cellContent.length - 1).coerceAtLeast(0))}$cellContent "
+ CellAlignment.RIGHT -> "${" ".repeat((paddingNeeded - 1).coerceAtLeast(0))}$cellContent "
CellAlignment.CENTER -> {
- val leftPadding = (wholeCellWidth - cellContent.length) / 2
- val rightPadding = wholeCellWidth - cellContent.length - leftPadding
+ val leftPadding = paddingNeeded / 2
+ val rightPadding = paddingNeeded - leftPadding
buildString {
repeat(leftPadding) {
append(' ')
@@ -159,7 +161,7 @@ object TableModificationUtils {
}
}
// MarkdownTableSeparatorRow.CellAlignment.LEFT
- else -> " $cellContent${" ".repeat((wholeCellWidth - cellContent.length - 1).coerceAtLeast(0))}"
+ else -> " $cellContent${" ".repeat((paddingNeeded - 1).coerceAtLeast(0))}"
}
}
@@ -184,7 +186,7 @@ object TableModificationUtils {
val cellRange = textRange
val cellText = documentText.substring(cellRange.startOffset, cellRange.endOffset)
val actualContent = cellText.trim(' ')
- val replacement = buildRealignedCellContent(actualContent, cellText.length, alignment)
+ val replacement = buildRealignedCellContent(actualContent, TableCharacterWidthUtils.calculateDisplayWidth(cellText), alignment)
document.replaceString(cellRange.startOffset, cellRange.endOffset, replacement)
}
diff --git a/plugins/markdown/test/data/editor/tables/format/chinese_table_test.after.md b/plugins/markdown/test/data/editor/tables/format/chinese_table_test.after.md
new file mode 100644
index 0000000000000..1a3a1967df4b4
--- /dev/null
+++ b/plugins/markdown/test/data/editor/tables/format/chinese_table_test.after.md
@@ -0,0 +1,3 @@
+| 名字 | 年龄 |
+|----------|------|
+| 投放账号 | 3 . * |
diff --git a/plugins/markdown/test/data/editor/tables/format/chinese_table_test.before.md b/plugins/markdown/test/data/editor/tables/format/chinese_table_test.before.md
new file mode 100644
index 0000000000000..1e990304599ba
--- /dev/null
+++ b/plugins/markdown/test/data/editor/tables/format/chinese_table_test.before.md
@@ -0,0 +1,3 @@
+| 名字 | 年龄 |
+|------|-------|
+| 投放账号 | 3 . * |
diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTablePostFormatProcessorTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTablePostFormatProcessorTest.kt
index ad4b24ad60a30..1901f5d1c05bc 100644
--- a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTablePostFormatProcessorTest.kt
+++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTablePostFormatProcessorTest.kt
@@ -22,6 +22,9 @@ class MarkdownTablePostFormatProcessorTest: LightPlatformCodeInsightTestCase() {
@Test
fun `table without end newline`() = doTest()
+ @Test
+ fun `chinese table test`() = doTest()
+
private fun doTest() {
val before = getTestName(true) + ".before.md"
val after = getTestName(true) + ".after.md"
diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtilsTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtilsTest.kt
new file mode 100644
index 0000000000000..bcbc9ab14897a
--- /dev/null
+++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/TableCharacterWidthUtilsTest.kt
@@ -0,0 +1,91 @@
+package org.intellij.plugins.markdown.editor.tables
+
+import org.junit.Test
+import org.junit.Assert.*
+
+class TableCharacterWidthUtilsTest {
+
+ @Test
+ fun `test ASCII characters width calculation`() {
+ assertEquals(1, TableCharacterWidthUtils.calculateDisplayWidth("a"))
+ assertEquals(1, TableCharacterWidthUtils.calculateDisplayWidth("A"))
+ assertEquals(1, TableCharacterWidthUtils.calculateDisplayWidth("1"))
+ assertEquals(1, TableCharacterWidthUtils.calculateDisplayWidth("!"))
+ assertEquals(5, TableCharacterWidthUtils.calculateDisplayWidth("hello"))
+ }
+
+ @Test
+ fun `test Chinese characters width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u4E2D"))
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u6587"))
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("\u4E2D\u6587"))
+ assertEquals(8, TableCharacterWidthUtils.calculateDisplayWidth("\u6295\u653E\u8D26\u53F7"))
+ }
+
+ @Test
+ fun `test mixed content width calculation`() {
+ assertEquals(3, TableCharacterWidthUtils.calculateDisplayWidth("a\u4E2D"))
+ assertEquals(3, TableCharacterWidthUtils.calculateDisplayWidth("\u4E2Da"))
+ assertEquals(7, TableCharacterWidthUtils.calculateDisplayWidth("hello\u4E2D"))
+ assertEquals(7, TableCharacterWidthUtils.calculateDisplayWidth("\u4E2Dhello"))
+ }
+
+ @Test
+ fun `test empty string width calculation`() {
+ assertEquals(0, TableCharacterWidthUtils.calculateDisplayWidth(""))
+ }
+
+ @Test
+ fun `test control characters width calculation`() {
+ assertEquals(0, TableCharacterWidthUtils.calculateDisplayWidth("\t"))
+ assertEquals(0, TableCharacterWidthUtils.calculateDisplayWidth("\n"))
+ assertEquals(0, TableCharacterWidthUtils.calculateDisplayWidth("\r"))
+ }
+
+ @Test
+ fun `test emoji width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uD83D\uDE00"))
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uD83D\uDE80"))
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("\uD83D\uDE00\uD83D\uDE80"))
+ }
+
+ @Test
+ fun `test Japanese characters width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u3042")) // Hiragana a
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u30A2")) // Katakana a
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u6F22")) // Kanji
+ }
+
+ @Test
+ fun `test Korean characters width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uD55C")) // Hangul
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uAE00"))
+ }
+
+ @Test
+ fun `test CJK symbols and punctuation width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u3000")) // Ideographic space
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u3001")) // 、
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u3002")) // 。
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u300C")) // 「
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\u300D")) // 」
+ }
+
+ @Test
+ fun `test full-width ASCII variants width calculation`() {
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uFF21")) // Fullwidth A
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uFF11")) // Fullwidth 1
+ assertEquals(2, TableCharacterWidthUtils.calculateDisplayWidth("\uFF01")) // Fullwidth !
+ }
+
+ @Test
+ fun `test table cell content examples`() {
+ // Test cases from the user's problem
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("\u540D\u5B57")) // 名字
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("\u5E74\u9F84")) // 年龄
+ assertEquals(8, TableCharacterWidthUtils.calculateDisplayWidth("\u6295\u653E\u8D26\u53F7")) // 投放账号
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("name"))
+ assertEquals(3, TableCharacterWidthUtils.calculateDisplayWidth("age"))
+ assertEquals(4, TableCharacterWidthUtils.calculateDisplayWidth("demo"))
+ }
+}