Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
343f7e9
feat: Add support for JSX/TSX
tscharke Feb 28, 2024
c24ce8f
chore: Add sponsoring
tscharke Feb 28, 2024
c59a1d9
feat: Automatically create new releases when a corresponding tag is set
tscharke Feb 28, 2024
8ac9db1
Merge pull request #5 from tscharke/main
Flyrell Mar 4, 2024
804c9c3
Release v1.1.0
Flyrell Mar 4, 2024
fab15f8
Release v1.1.1
Flyrell Mar 4, 2024
94592de
docs: Pointed out support of JSX/TSX
tscharke Mar 4, 2024
52f73a9
Merge pull request #7 from tscharke/docs
Flyrell Mar 4, 2024
006a7b0
fix: Update to the latest dependencies
tscharke Aug 18, 2024
8fe1d3a
chore: Increase version
tscharke Aug 18, 2024
f7c9271
doc: Add a list of compatible products
tscharke Aug 18, 2024
7679534
Merge pull request #10 from tscharke/bugs/update-to-latest-intellij-p…
Flyrell Aug 26, 2024
5b0133e
fix: deprecated gradle jvmTarget
Flyrell Aug 26, 2024
6093ffa
fix: Use compilerOptions out of the properties
tscharke Dec 13, 2024
c65dcfe
chore: Update gradle-version
tscharke Dec 13, 2024
0ea4290
feat: Support platform-version 2024.3
tscharke Dec 13, 2024
74c2ed9
doc: Update Compatibility
tscharke Dec 13, 2024
be31989
feat: Prepare new version 1.1.3
tscharke Dec 13, 2024
49a6492
Merge pull request #11 from Flyrell/features/242
Flyrell Jan 27, 2025
9ca9d5f
chore: Update gradle-version
tscharke Apr 17, 2025
77244a9
feat: Support platform-version 2025.x
tscharke Apr 17, 2025
d31b191
feat: Prepare version 1.2.0
tscharke Apr 17, 2025
f94f7fc
doc: Update Compatibility
tscharke Apr 17, 2025
21a0101
feat: Support for RHTML and ERB templates
tscharke Apr 18, 2025
b9807f9
Merge pull request #12 from Flyrell/features/2025x
Flyrell Apr 22, 2025
ef152c3
feat(2025.2): Use latest version of kotlin
tscharke Aug 7, 2025
dcfcb2f
feat(2025.2): Update platform version
tscharke Aug 7, 2025
3a2306a
doc(2025.2): Update platform version
tscharke Aug 7, 2025
fea7a50
chore(2025.2): Prepare version 1.3.0
tscharke Aug 7, 2025
9997731
Merge pull request #13 from Flyrell/feature/2025.2
Flyrell Aug 15, 2025
3d8bf23
fix: publishing gradle
Flyrell Aug 15, 2025
56d8526
feat(2025.3): Update platform version
Dec 16, 2025
dfef7ab
doc(2025.3): Update platform version
Dec 16, 2025
0c3e742
chore(2025.3): Prepare version 1.4.0
Dec 16, 2025
edf9530
docs(changelog): 📚 Update release notes for 1.2.0
github-actions[bot] Dec 16, 2025
d9861f9
chore(2025.3): add optional dependency on JavaScript module
Dec 17, 2025
9be598f
test: Add unit tests for HTML and TSX
Dec 17, 2025
49923e3
fix: fold TSX object literal attributes
Dec 17, 2025
150d60b
Merge pull request #15 from Flyrell/features/253
Mateusz-H Dec 17, 2025
e8c9c82
chore: Backwards compatible since 2025.1
Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CHANGELOG

\* *This CHANGELOG was automatically generated by [auto-generate-changelog](https://github.com/BobAnkh/auto-generate-changelog)*
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2024 Dawid Zbiński

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 changes: 7 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,16 @@

Annoyed with some html attributes and their length?

This simple and lightweight plugin will help you fold them, so you don't need to see them anymore. Any
template written in XML, HTML, XHTML and DTD is supported by the plugin. The plugin will not edit your real code in any way, and you can choose whether to fold
the items by default, placeholder for folded items, and how the folding looks like.
This simple and lightweight plugin will help you fold them, so you don't need to see them anymore. Any template written in RHTML (Ruby HTML), ERB (Embedded
Ruby), JSX, XML, HTML, XHTML and DTD is supported by the plugin. The plugin will not edit your real code in any way, and you can choose whether
to fold the items by default, placeholder for folded items, and how the folding looks like.

You can find Dawid's versions on the [IntelliJ marketplace](https://plugins.jetbrains.com/plugin/19715-html-attribute-folder).
You can find the plugin on [IntelliJ marketplace](https://plugins.jetbrains.com/plugin/19715-html-attribute-folder).

## About this project
## Compatibility

This project is a fork of the IntelliJ Plugin **HTML Attribute Folder** originally created
by [Dawid Zbiński](https://github.com/Flyrell) 😎

Dawids project has gotten a little older 🤷‍ That's why I created this fork and have already made
the following adjustments:

- [x] Make it run with IntelliJ IDEA 2023.3.4 (UE).
- [x] Using the latest version of dependencies.
- [x] Extract most of the version-numbers into `gradle.properties`.
- [x] Increase the version of the plugin itself. Its now/here version `1.1.0`.

## Issues

- [x] It seems like the list of attributes is not being applied correctly.
Most of the time it only works with one attribute definition and not with several.
- [x] Add support for keyboard shortcuts (`ctrl alt e`).
- [ ] Adding support for `JS(X)`/`TS(X)`.
- [ ] Testing on other JetBrains Products (need help here, please 🙏)

## Links

- [Dawid Zbiński's repository](https://github.com/Flyrell/html-attribute-folder).
- Dawid's versions on the [IntelliJ marketplace](https://plugins.jetbrains.com/plugin/19715-html-attribute-folder).
Manually verified with IntelliJ IDEA Unified distribution 2025.3+

## ❤️🙏 Love & Thanks

- [Dawid Zbiński](https://github.com/Flyrell) for creating this project

- [tscharke](https://github.com/tscharke) for contributing
35 changes: 24 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.intellij.platform.gradle.TestFrameworkType

fun properties(key: String) = providers.gradleProperty(key)

plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.9.21"
id("org.jetbrains.intellij") version "1.17.2"
id("org.jetbrains.kotlin.jvm") version "2.2.0"
id("org.jetbrains.intellij.platform") version "2.5.0"
}

group = properties("pluginGroup").get()
version = properties("pluginVersion").get()

repositories {
mavenCentral()

intellijPlatform {
defaultRepositories()
}
}

// Configure Gradle IntelliJ Plugin
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
version.set(properties("platformVersion"))
type.set(properties("platformType"))
plugins.set(listOf(/* Plugin Dependencies */))
dependencies {
intellijPlatform {
val type = properties("platformType").get()
val version = properties("platformVersion").get()
create(type, version)

testFramework(TestFrameworkType.Platform)
bundledPlugin("JavaScript")
}

testImplementation(kotlin("test"))
}

tasks {
Expand All @@ -28,7 +40,9 @@ tasks {
targetCompatibility = properties("javaVersion").get()
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = properties("javaVersion").get()
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(properties("javaVersion").get()))
}
}

patchPluginXml {
Expand All @@ -43,7 +57,6 @@ tasks {
}

publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
// channels.set(listOf("alpha"))
token = providers.gradleProperty("intellijPlatformPublishingToken")
}
}
13 changes: 7 additions & 6 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ org.gradle.caching=true

pluginName=html-attribute-folder
pluginGroup=dev.zbinski
pluginVersion=1.1.0
platformVersion=2023.1.5
platformType=IC
pluginSinceBuild=231
pluginUntilBuild=241.*
javaVersion=17
pluginVersion=1.4.0
platformVersion=2025.3
# @see https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-types.html#IntelliJPlatformType
platformType=IU
# @see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html#platformVersions
pluginSinceBuild=251
javaVersion=21
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
80 changes: 71 additions & 9 deletions src/main/kotlin/dev/zbinski/htmlattributefolder/AttributeFolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,53 @@ class AttributeFolder: FoldingBuilderEx(), DumbAware {
for (item in getAttributes(Array(1) { root })) {
var end: Int
var start: Int

if (settings.foldingMethod == 1) {
end = item.attribute.textRange.endOffset
start = item.attribute.textRange.startOffset
} else {
val len = item.attributeName.length + settings.attributeSeparator.length + settings.attributeWrapper.length
start = item.attribute.textRange.startOffset + len
end = item.attribute.textRange.endOffset - settings.attributeWrapper.length
val text = item.attribute.text
val base = item.attribute.textRange.startOffset

// Find '=' then the first non-space char after it
val eq = text.indexOf(settings.attributeSeparator)
if (eq < 0) continue
var i = eq + 1
while (i < text.length && text[i].isWhitespace()) i++
if (i >= text.length) continue

when (text[i]) {
// name="value" or name='value'
'"', '\'' -> {
val quote = text[i]
val open = i
val close = text.indexOf(quote, startIndex = open + 1)
if (close < 0) continue

start = base + open + 1
end = base + close
}
// name={...} or name={{...}}
'{' -> {
val outerOpen = i
val outerClose = findMatchingBrace(text, outerOpen) ?: continue

val isDouble = outerOpen + 1 < text.length && text[outerOpen + 1] == '{'
if (isDouble) {
val innerOpen = outerOpen + 1
val innerClose = findMatchingBrace(text, innerOpen) ?: continue

// Fold inside INNER braces INCLUDING whitespace so output becomes: style={{__PLACEHOLDER__}}
start = base + innerOpen + 1
end = base + innerClose
} else {
// Fold inside single braces INCLUDING whitespace: name={__PLACEHOLDER__}
start = base + outerOpen + 1
end = base + outerClose
}
}
else -> continue
}
}

if (end > start) {
Expand Down Expand Up @@ -57,24 +97,46 @@ class AttributeFolder: FoldingBuilderEx(), DumbAware {
return settings.collapseByDefault
}

private fun findMatchingBrace(text: String, openIndex: Int): Int? {
if (openIndex !in text.indices || text[openIndex] != '{') return null
var depth = 0
var i = openIndex
while (i < text.length) {
when (text[i]) {
'{' -> depth++
'}' -> {
depth--
if (depth == 0) return i
}
}
i++
}
return null
}

private fun getAttributes(
elements: Array<PsiElement>,
attributes: ArrayList<String> = settings.attributes
): Sequence<Attribute> = sequence {
for (child in elements) {
val t = child.text
for (attributeName in attributes) {
val attributeBeginning = attributeName + settings.attributeSeparator + settings.attributeWrapper
if (child.text.startsWith(attributeBeginning)) {
val startsLikeAttribute =
t.startsWith("$attributeName=\"") ||
t.startsWith("$attributeName='") ||
t.startsWith("$attributeName={")

if (startsLikeAttribute) {
yield(object : Attribute {
override val attribute = child
override val attributeName = attributeName
})
}
}

val items = getAttributes(child.children, arrayListOf(attributeName)).iterator()
while (items.hasNext()) {
yield(items.next())
}
val items = getAttributes(child.children, attributes).iterator()
while (items.hasNext()) {
yield(items.next())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ class AttributeFolderAction : AnAction() {

}

private fun performFoldOperation(fold: FoldRegion) {
fold.isExpanded = !settings.collapse
}

override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR)
val project = e.getData(CommonDataKeys.PROJECT)
if (editor == null || project == null) {
return
}
settings.collapse = !settings.collapse

val foldingModel = editor.foldingModel
val allFoldRegions = foldingModel.allFoldRegions
ApplicationManager.getApplication().runWriteAction {
settings.collapse = !settings.collapse
val foldRegions = editor.foldingModel.allFoldRegions
editor.foldingModel.runBatchFoldingOperation {
getFoldRegionsForRelevantAttributes(foldRegions).forEach { fold -> fold.isExpanded = !fold.isExpanded }
foldingModel.runBatchFoldingOperation {
getFoldRegionsForRelevantAttributes(allFoldRegions).forEach(::performFoldOperation)
}
}
}
Expand Down
15 changes: 9 additions & 6 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name -->
<name>HTML Attribute Folder</name>

<version>1.1.0</version>
<version>1.4.0</version>

<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor email="dawid@zbinski.dev" url="https://zbinski.dev">Dawid Zbiński</vendor>
Expand All @@ -17,17 +17,19 @@
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
<description><![CDATA[
Annoyed with some html attributes and their length?

This simple and lightweight plugin will help you fold them, so you don't need to see them anymore.
Any template written in XML, HTML, XHTML and DTD is supported by the plugin.
The plugin will not edit your real code in any way, and you can choose
whether to fold the items by default, placeholder for folded items, and how the folding looks like.
Any template written in RHTML (Ruby HTML), ERB (Embedded Ruby), JSX, XML, HTML, XHTML and DTD is supported by the
plugin. The plugin will not edit your real code in any way, and you can choose whether to fold the items by default,
placeholder for folded items, and how the folding looks like.
]]></description>

<!-- Product and plugin compatibility requirements.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.xml</depends>
<depends optional="true">com.intellij.modules.javascript</depends>

<!-- Extension points defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
Expand All @@ -39,9 +41,10 @@
<lang.foldingBuilder language="HTML" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="XHTML" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="TypeScript" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="TSX" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="TypeScript JSX" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="JavaScript" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="JSX" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="JavaScript JSX" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>
<lang.foldingBuilder language="RHTML" implementationClass="dev.zbinski.htmlattributefolder.AttributeFolder"/>

<projectConfigurable parentId="tools"
nonDefaultProject="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dev.zbinski.htmlattributefolder

private const val HTML_SNIPPET =
"""<div class="a b" className="c d" style="background-color: red;" data-foo="foo-data" />"""

class AttributeFolderHTMLTest : BaseAttributeFolderTest() {
fun testFoldingHtmlAttributesCollapsed() {
assertContains(document.text, """class="a b"""")
assertContains(document.text, """className="c d"""")
assertContains(document.text, """style="background-color: red;"""")
assertContains(document.text, """data-foo="foo-data"""")

configureAttributeFolder(collapseByDefault = true)

val visualText = applyPluginFoldingAndRender()
assertContains(visualText, """class="__PLACEHOLDER__"""")
assertContains(visualText, """className="__PLACEHOLDER__"""")
assertContains(visualText, """style="background-color: red;"""")
assertContains(visualText, """data-foo="foo-data"""")
}

fun testFoldingHtmlAttributesUncollapsed() {
assertContains(document.text, """class="a b"""")
assertContains(document.text, """className="c d"""")
assertContains(document.text, """style="background-color: red;"""")
assertContains(document.text, """data-foo="foo-data"""")

configureAttributeFolder(collapseByDefault = false)

val visualText = applyPluginFoldingAndRender()
assertContains(visualText, """class="a b"""")
assertContains(visualText, """className="c d"""")
assertContains(visualText, """style="background-color: red;"""")
assertContains(visualText, """data-foo="foo-data"""")
}

override fun setUp() {
super.setUp()
setupDocument("test.html", HTML_SNIPPET)
}
}
Loading