From 7f0322abb8a1426258526dfb9ce5fd5f564bd80b Mon Sep 17 00:00:00 2001 From: Ilia Permiashkin Date: Wed, 12 Aug 2026 22:58:00 +0000 Subject: [PATCH] [markdown] IJPL-251351 Don't enforce Markdown table cells to be aligned in the editor Merge-request: IJ-MR-217378 Merged-by: Ilia Permiashkin Merge-request: IJ-MR-217525 Merged-by: Ilia Permiashkin GitOrigin-RevId: 3cb54a5034822f4cdfd65a6c780b977796ee975c --- .../core/resources/META-INF/plugin.xml | 13 + .../after.md.template | 3 + .../before.md.template | 3 + .../description.html | 5 + .../ReformatTableIntention/after.md.template | 3 + .../ReformatTableIntention/before.md.template | 3 + .../ReformatTableIntention/description.html | 5 + .../settings/MarkdownCodeInsightSettings.kt | 4 +- .../tables/MarkdownTableColumnExpandTest.kt | 11 + .../tables/MarkdownTableColumnShrinkTest.kt | 11 + ...kdownTableColumnShrinkWithFullWidthTest.kt | 11 + .../editor/tables/MarkdownTableTypingTest.kt | 458 ++++++++++++++++++ ...ctTableFormattingInspectionQuickFixTest.kt | 15 + 13 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/after.md.template create mode 100644 plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/before.md.template create mode 100644 plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/description.html create mode 100644 plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/after.md.template create mode 100644 plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/before.md.template create mode 100644 plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/description.html create mode 100644 plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableTypingTest.kt diff --git a/plugins/markdown/core/resources/META-INF/plugin.xml b/plugins/markdown/core/resources/META-INF/plugin.xml index 14f8627a89ce..c97af1427e6d 100644 --- a/plugins/markdown/core/resources/META-INF/plugin.xml +++ b/plugins/markdown/core/resources/META-INF/plugin.xml @@ -129,6 +129,19 @@ level="WARNING" implementationClass="org.intellij.plugins.markdown.editor.tables.inspections.MarkdownTablePipeInCodeSpanInspection"/> + + Markdown + org.intellij.plugins.markdown.editor.tables.intentions.ReformatTableIntention + messages.MarkdownBundle + markdown.intention.category + + + Markdown + org.intellij.plugins.markdown.editor.tables.intentions.FixCellAlignmentIntention + messages.MarkdownBundle + markdown.intention.category + + Markdown org.intellij.plugins.markdown.editor.tables.intentions.MarkdownInsertTableColumnIntention$InsertBefore diff --git a/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/after.md.template b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/after.md.template new file mode 100644 index 000000000000..fd5695d2e258 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/after.md.template @@ -0,0 +1,3 @@ +| Value | +|------:| +| 1 | diff --git a/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/before.md.template b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/before.md.template new file mode 100644 index 000000000000..d307825d2b73 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/before.md.template @@ -0,0 +1,3 @@ +| Value | +|------:| +| 1 | diff --git a/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/description.html b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/description.html new file mode 100644 index 000000000000..6790cd1d0934 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/FixCellAlignmentIntention/description.html @@ -0,0 +1,5 @@ + + +Adjusts the current table cell to match its column alignment. + + diff --git a/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/after.md.template b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/after.md.template new file mode 100644 index 000000000000..ef04ac5e30e2 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/after.md.template @@ -0,0 +1,3 @@ +| Name | Value | +|-------|-------| +| first | 1 | diff --git a/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/before.md.template b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/before.md.template new file mode 100644 index 000000000000..d72f03730061 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/before.md.template @@ -0,0 +1,3 @@ +| Name | Value | +|---|---| +| first | 1 | diff --git a/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/description.html b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/description.html new file mode 100644 index 000000000000..74d12cdfa3e3 --- /dev/null +++ b/plugins/markdown/core/resources/intentionDescriptions/ReformatTableIntention/description.html @@ -0,0 +1,5 @@ + + +Formats all columns in the current Markdown table. + + diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/settings/MarkdownCodeInsightSettings.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/settings/MarkdownCodeInsightSettings.kt index 5c3f88eb6797..aa76d58e4bed 100644 --- a/plugins/markdown/core/src/org/intellij/plugins/markdown/settings/MarkdownCodeInsightSettings.kt +++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/settings/MarkdownCodeInsightSettings.kt @@ -16,14 +16,14 @@ import org.jetbrains.annotations.Nls storages = [(Storage("markdown.xml"))]) class MarkdownCodeInsightSettings: SimplePersistentStateComponent(State()) { class State: BaseState() { - var reformatTablesOnType: Boolean by property(true) + var reformatTablesOnType: Boolean by property(false) var insertHtmlLineBreakInsideTables: Boolean by property(true) var insertNewTableRowOnShiftEnter: Boolean by property(true) var useTableCellNavigation: Boolean by property(true) var adjustListIndentation: Boolean by property(true) var smartEnterAndBackspace: Boolean by property(true) var renumberListsOnType: Boolean by property(false) - var listNumberingType by enum(ListNumberingType.SEQUENTIAL) + var listNumberingType: ListNumberingType by enum(ListNumberingType.SEQUENTIAL) var enableFileDrop: Boolean by property(true) } diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnExpandTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnExpandTest.kt index e37ade8b0fac..297f753941a7 100644 --- a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnExpandTest.kt +++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnExpandTest.kt @@ -2,6 +2,9 @@ package org.intellij.plugins.markdown.editor.tables import com.intellij.testFramework.LightPlatformCodeInsightTestCase +import com.intellij.openapi.util.Disposer +import org.intellij.plugins.markdown.settings.MarkdownCodeInsightSettings +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -9,6 +12,14 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) @Suppress("MarkdownIncorrectTableFormatting") class MarkdownTableColumnExpandTest: LightPlatformCodeInsightTestCase() { + @Before + fun enableTableReformatting() { + val settings = MarkdownCodeInsightSettings.getInstance() + val old = settings.state.reformatTablesOnType + settings.state.reformatTablesOnType = true + Disposer.register(testRootDisposable) { settings.state.reformatTablesOnType = old } + } + @Test fun `test right after cell content`() { // language=Markdown diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkTest.kt index 2464624c7a23..3b0bb7351c58 100644 --- a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkTest.kt +++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkTest.kt @@ -1,7 +1,10 @@ // 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 +import com.intellij.openapi.util.Disposer import com.intellij.testFramework.LightPlatformCodeInsightTestCase +import org.intellij.plugins.markdown.settings.MarkdownCodeInsightSettings +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -9,6 +12,14 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) @Suppress("MarkdownIncorrectTableFormatting") class MarkdownTableColumnShrinkTest: LightPlatformCodeInsightTestCase() { + @Before + fun enableTableReformatting() { + val settings = MarkdownCodeInsightSettings.getInstance() + val old = settings.state.reformatTablesOnType + settings.state.reformatTablesOnType = true + Disposer.register(testRootDisposable) { settings.state.reformatTablesOnType = old } + } + @Test fun `test right after cell content`() { // language=Markdown diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkWithFullWidthTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkWithFullWidthTest.kt index 05669b55f3af..2cbebbdbe9c2 100644 --- a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkWithFullWidthTest.kt +++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableColumnShrinkWithFullWidthTest.kt @@ -1,7 +1,10 @@ // Copyright 2000-2026 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 +import com.intellij.openapi.util.Disposer import com.intellij.testFramework.LightPlatformCodeInsightTestCase +import org.intellij.plugins.markdown.settings.MarkdownCodeInsightSettings +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -9,6 +12,14 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) @Suppress("MarkdownIncorrectTableFormatting") class MarkdownTableColumnShrinkWithFullWidthTest: LightPlatformCodeInsightTestCase() { + @Before + fun enableTableReformatting() { + val settings = MarkdownCodeInsightSettings.getInstance() + val old = settings.state.reformatTablesOnType + settings.state.reformatTablesOnType = true + Disposer.register(testRootDisposable) { settings.state.reformatTablesOnType = old } + } + @Test fun `test backspace cjk character`() { // language=Markdown diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableTypingTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableTypingTest.kt new file mode 100644 index 000000000000..33873e5fd9a2 --- /dev/null +++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/MarkdownTableTypingTest.kt @@ -0,0 +1,458 @@ +// Copyright 2000-2026 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 + +import com.intellij.application.options.CodeStyle +import com.intellij.application.options.codeStyle.excludedFiles.GlobPatternDescriptor +import com.intellij.openapi.util.Disposer +import com.intellij.psi.PsiFile +import com.intellij.testFramework.LightPlatformCodeInsightTestCase +import org.intellij.plugins.markdown.settings.MarkdownCodeInsightSettings +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +@Suppress("MarkdownIncorrectTableFormatting") +class MarkdownTableTypingTest: LightPlatformCodeInsightTestCase() { + @Before + fun enableTableReformatting() { + val settings = MarkdownCodeInsightSettings.getInstance() + val old = settings.state.reformatTablesOnType + settings.state.reformatTablesOnType = true + Disposer.register(testRootDisposable) { settings.state.reformatTablesOnType = old } + } + + @Test + fun `test typing in non-last column reformats the table`() { + // language=Markdown + val before = """ + | a |malformed| c | + |---|---|---| + | 1 |y| 2 | + """.trimIndent() + + // language=Markdown + val after = """ + | a |malformed| c | + |----|---|---| + | 1x |y| 2 | + """.trimIndent() + doTest(before, after, 1, "x") + } + + @Test + fun `test typing in last column reformats the table`() { + // language=Markdown + val before = """ + | a |malformed| c | + |---|---|---| + | 1 |y| 2 | + """.trimIndent() + + // language=Markdown + val after = """ + | a |malformed| c | + |---|---|----| + | 1 |y| 2x | + """.trimIndent() + doTest(before, after, 1, "x") + } + + @Test + fun `test right after cell content`() { + // language=Markdown + doTest( + """ + | none | none | + |------|------| + | a | asd | + """.trimIndent(), + """ + | none | none | + |-------|------| + | a | asd | + """.trimIndent() + ) + } + + @Test + fun `test between spaces on the right side`() { + // language=Markdown + doTest( + """ + | none | none | + |------|------| + | a | asd | + """.trimIndent(), + """ + | none | none | + |-------|------| + | a | asd | + """.trimIndent() + ) + } + + @Test + fun `test just before right pipe`() { + // language=Markdown + doTest( + """ + | none | none | + |------|------| + | a | asd | + """.trimIndent(), + """ + | none | none | + |-------|------| + | a | asd | + """.trimIndent() + ) + } + + @Test + fun `test in separator`() { + doTest( + """ + | none | none | + |------|------| + | a | asd | + """.trimIndent(), + """ + | none | none | + |-------|------| + | a | asd | + """.trimIndent(), + string = "-" + ) + } + + @Test + fun `test in separator with colon`() { + doTest( + """ + | none | none | + |------|------| + | a | asd | + """.trimIndent(), + """ + | none | none | + |:------|------| + | a | asd | + """.trimIndent(), + string = ":" + ) + } + + @Test + fun `test with right alignment`() { + // language=Markdown + val before = """ + | right | + | ---: | + | some | + """.trimIndent() + // language=Markdown + val after = """ + | right | + |-------:| + | some | + """.trimIndent() + doTest(before, after) + } + + @Test + fun `test typing with right alignment`() { + // language=Markdown + val before = """ + | right | + |------:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | right | + |------:| + | some | + """.trimIndent() + doTest(before, after, string = "some") + } + + @Test + fun `test typing with right alignment and spaces`() { + // language=Markdown + val before = """ + | right | + |------:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | right | + |-------------:| + | some content | + """.trimIndent() + doTest(before, after, string = "some content") + } + + @Test + fun `test typing with left alignment`() { + // language=Markdown + val before = """ + | left | + |-----:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | left | + |-----:| + | some | + """.trimIndent() + doTest(before, after, string = "some") + } + + @Test + fun `test typing with left alignment and spaces`() { + // language=Markdown + val before = """ + | left | + |-----:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | left | + |-------------:| + | some content | + """.trimIndent() + doTest(before, after, string = "some content") + } + + @Test + fun `test typing with center alignment`() { + // language=Markdown + val before = """ + | center | + |:------:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | center | + |:------:| + | some | + """.trimIndent() + doTest(before, after, string = "some") + } + + @Test + fun `test typing with center alignment and ends with space`() { + // language=Markdown + val before = """ + | center | + |:------:| + | | + """.trimIndent() + // language=Markdown + val after = """ + | center | + |:------:| + | ssome | + """.trimIndent() + doTest(before, after, string = "ssome ") + } + + @Test + fun `test typing in empty cell`() { + // language=Markdown + val before = """ + || + |-| + || + """.trimIndent() + // language=Markdown + val after = """ + | | + |------| + | some | + """.trimIndent() + doTest(before, after, string = "some") + } + + @Test + fun `test typing space in cell with two spaces`() { + // language=Markdown + val before = """ + | | + |--| + | | + """.trimIndent() + // language=Markdown + val after = """ + | | + |---| + | | + """.trimIndent() + doTest(before, after, string = " ") + } + + @Test + fun `test no column expand`() { + // language=Markdown + val content = """ + | some | + |------| + | | + """.trimIndent() + // language=Markdown + val expected = """ + | some | + |------| + | some | + """.trimIndent() + configureFromFileText("some.md", content) + runWithDisabledFormatting(file) { + type("some") + checkResultByText(expected) + } + } + + @Test + fun `test no column shrink`() { + // language=Markdown + val content = """ + | some | + |--------------| + | some text | + """.trimIndent() + // language=Markdown + val expected = """ + | some | + |--------------| + | some tex | + """.trimIndent() + configureFromFileText("some.md", content) + runWithDisabledFormatting(file) { + backspace() + checkResultByText(expected) + } + } + + @Test + fun `test no alignment correction`() { + // language=Markdown + val content = """ + | some content | + |:------------:| + | | + """.trimIndent() + // language=Markdown + val expected = """ + | some content | + |:------------:| + | some | + """.trimIndent() + configureFromFileText("some.md", content) + runWithDisabledFormatting(file) { + type("some") + checkResultByText(expected) + } + } + + @Test + fun `test no column expand when reformat on type is disabled`() { + // language=Markdown + val content = """ + | some | + |------| + | | + """.trimIndent() + // language=Markdown + val expected = """ + | some | + |------| + | some | + """.trimIndent() + configureFromFileText("some.md", content) + runWithReformatOnTypeDisabled { + type("some") + checkResultByText(expected) + } + } + + @Test + fun `test no column shrink when reformat on type is disabled`() { + // language=Markdown + val content = """ + | some | + |--------------| + | some text | + """.trimIndent() + // language=Markdown + val expected = """ + | some | + |--------------| + | some tex | + """.trimIndent() + configureFromFileText("some.md", content) + runWithReformatOnTypeDisabled { + backspace() + checkResultByText(expected) + } + } + + @Test + fun `test no alignment correction when reformat on type is disabled`() { + // language=Markdown + val content = """ + | some content | + |:------------:| + | | + """.trimIndent() + // language=Markdown + val expected = """ + | some content | + |:------------:| + | some | + """.trimIndent() + configureFromFileText("some.md", content) + runWithReformatOnTypeDisabled { + type("some") + checkResultByText(expected) + } + } + + private fun runWithReformatOnTypeDisabled(block: () -> Unit) { + val settings = MarkdownCodeInsightSettings.getInstance() + val old = settings.state.reformatTablesOnType + settings.state.reformatTablesOnType = false + try { + block.invoke() + } + finally { + settings.state.reformatTablesOnType = old + } + } + + private fun runWithDisabledFormatting(file: PsiFile, block: () -> Unit) { + val settings = CodeStyle.getSettings(file) + val old = settings.excludedFiles.descriptors.toList() + settings.excludedFiles.addDescriptor(GlobPatternDescriptor("*.md")) + try { + block.invoke() + } + finally { + settings.excludedFiles.apply { + clear() + old.forEach(this::addDescriptor) + } + } + } + + private fun doTest(content: String, expected: String, count: Int = 1, string: String = " ") { + configureFromFileText("some.md", content) + type(string.repeat(count)) + checkResultByText(expected) + } +} diff --git a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/inspections/MarkdownIncorrectTableFormattingInspectionQuickFixTest.kt b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/inspections/MarkdownIncorrectTableFormattingInspectionQuickFixTest.kt index 64c276adf672..e5a88fab1efb 100644 --- a/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/inspections/MarkdownIncorrectTableFormattingInspectionQuickFixTest.kt +++ b/plugins/markdown/test/src/org/intellij/plugins/markdown/editor/tables/inspections/MarkdownIncorrectTableFormattingInspectionQuickFixTest.kt @@ -19,6 +19,21 @@ class MarkdownIncorrectTableFormattingInspectionQuickFixTest: LightPlatformCodeI private val reformatIntentionFixText get() = MarkdownBundle.message("markdown.reformat.table.intention.text") + private val fixCellAlignmentIntentionText + get() = MarkdownBundle.message("markdown.fix.cell.alignment.intention.text") + + @Test + fun `reformat intention is available without inspection`() { + myFixture.configureByText("some.md", "| first | second |\n|---|---|") + assertNotNull(myFixture.availableIntentions.find { it.text == reformatIntentionFixText }) + } + + @Test + fun `fix cell alignment intention is available without inspection`() { + myFixture.configureByText("some.md", "| first |\n|------:|\n| some |") + assertNotNull(myFixture.availableIntentions.find { it.text == fixCellAlignmentIntentionText }) + } + @Test fun `works with incorrectly formatted cell`() { // language=Markdown